当前位置:网站首页>漫画:有趣的 “切蛋糕“ 问题

漫画:有趣的 “切蛋糕“ 问题

2022-06-11 14:06:00 大数据v

e9b5957772e7859dcb4c56fc6c57a694.gif

导读:一道又烧脑又有趣的ACM比赛题目。

作者:小灰

来源:程序员小灰(ID:chengxuyuanxiaohui)

62ffdf7f38f3f32491db4cb7ea79f3d7.png

18f10eba6bf7cbc0c88900488a6e449e.png

—————  第二天  —————

a6146e89fdbfe4c9378a4cc28aab6614.png

18ce2dc89e90908820d2573bb6681b4d.png

add9abd286c1573a187d7b939e597ac5.png

举个例子:

我们有5块蛋糕,蛋糕的大小分别是 5,17,25,3,15 

640fde67e07dc4d49eae5362ca59db28.png

我们有7位顾客,他们的饭量分别是 2,5,7,9,12,14,20

ec5ef0c2921efa5c97c401de2fdf0702.png

(每个蛋糕大小和顾客食量都是小于1000的整数,蛋糕和顾客的数量不超过1000)

在分发蛋糕时,有一个特殊的规则:蛋糕可分不可合

什么意思呢?

一块较大的蛋糕,可以切分成多个小块,用来满足多个胃口较小的顾客:

eaa34aa2d17f52fa6af79518561bd48f.png

但是,若干块较小的蛋糕,不允许合并成一块大蛋糕,用来满足一个胃口较大的顾客:

96165dd7ac8fcc43aba0b949d0b60e94.png

最后的问题是:给定蛋糕大小的集合cakes,给定顾客饭量的集合mouths,如何计算出这些蛋糕可以满足的最大顾客数量?

  • 比如:输入cakes集合 {2,9};输入mouths集合 {5,4, 2,8}

  • 正确返回:3

de6e54101c49c3226fdc7439015cd53f.png

383c196c805899ecaf57b7de99126250.png

48d85dd89ffacfa7045b90af75518d29.png

2b90c5de83fab1ceb0e3d22ec91dc092.png

小灰的思路:

为了让更多的顾客吃饱,肯定要优先满足食量小的顾客,所以小灰决定使用贪心算法

首先,把蛋糕和顾客从小到大进行排序:

按照上面的例子,排序结果如下:

2e80003a5db2face5c8c6ab51cd82a31.png

接下来,让每一个蛋糕和顾客按照从左到右的顺序匹配。如果蛋糕大于顾客饭量,则切分蛋糕;如果蛋糕小于顾客饭量,则丢弃该蛋糕。

第1块蛋糕大小是3,第1个顾客饭量是2,于是把蛋糕切分成2+1,满足顾客。剩下的1大小蛋糕无法满足下一位顾客,丢弃掉。

a6a008a31acb27517c297372955bc537.png

第2块蛋糕大小是5,第2个顾客饭量是5,刚好满足顾客。

4ba50fe03bd105f62450a104eedb1f04.png

第3块蛋糕大小是15,第3个顾客饭量是7,于是把蛋糕切分成7+8,满足顾客。剩下的8大小蛋糕无法满足下一位顾客,丢弃掉。

04f98fd8f5308dc1b25bb691f80aced6.png

第4块蛋糕大小是17,第4个顾客饭量是9,于是把蛋糕切分成9+8,满足顾客。剩下的8大小蛋糕无法满足下一位顾客,丢弃掉。

7a589bc1b5f3e2e0842ee466b49ddade.png

第5块蛋糕大小是25,第5个顾客饭量是12,于是把蛋糕切分成12+13,满足顾客。剩下的13大小蛋糕无法满足下一位顾客,丢弃掉。

96165e87a834aa9b117601b7e09e07b6.png

这样一来,所有蛋糕都用完了,由贪心算法得出结论,最大能满足的顾客数量是5。

6407b3eed01ff6c6899fa2d6e7fec3b1.png

bc2abb545dae0d5b2f534f6b0f5014e5.png

例子当中:

  • 3的蛋糕满足2的顾客,

  • 5的蛋糕满足5的顾客,

  • 15的蛋糕满足12的顾客,

  • 17的蛋糕满足7和9的顾客,

  • 25的蛋糕满足14的顾客。

显然,面试官随意给出的吃法,满足了6个顾客。

384f500f04ce15e6b7d4a081f2c0b6eb.png

23e27d6e5c084e43538dfca175c4357e.png

————————————

0869f78c1694e5a574b742197c3ed7c2.png

a6dd61428d83d49634498ef37b9e060b.png

f382b3e1624c8c9bf7e25b8233c6bb43.png

e024ad67687333265bb416d2ce5ee44b.png

26a3847d05ba3b03b8fbdd0f05b92ac3.png

9ca912449ca9ddf254958d332382c389.png

这句话听起来有点绕,什么意思呢?我们可以看看下面这张图:

5336e17268c06c13452266ca99d80813.png

其实道理很简单,由于顾客的饭量是从小到大排序的,优先满足饭量小的顾客,才能尽量满足更多的人。

因此,在记录顾客饭量的数组中,必定存在一段从最左侧开始的连续元素,符合当前蛋糕所能满足的最多顾客组合。

这样一来,我们的题目就从寻找最大满足顾客数量,转化成了寻找顾客饭量有序数组中的最大满足临界点:

639215dedec2f9c97121bb30851997a5.png

4a48cb748890870dd1a4310ba11c3913.png

4cd08e7725289a6d69eb944c63225785.png

让我们先来回顾一下二分查找的思路:

c2de462a25627462b5e6c78cdb92abd2.png

1. 选择中间元素,下标mid = (0 + 6)/2 = 3  ,因此中间元素是9:

0a58afaf3e3710853970e51e6150007f.png

2. 判断9>5,选择元素9左侧部分的中间元素,下标mid = (0+2)/2 = 1,因此中间元素是5:

ab14c233db7a59ec4dfc0e1f89fc757d.png

3. 判断5=5,查找结束。

但是,切蛋糕的问题比普通的二分查找要复杂得多,因为我们要寻找的顾客饭量数组临界元素,并不是简单地判断元素是否相等,而是要验证给定的蛋糕能否满足临界元素之前的所有顾客。

如何来实现呢?我们仍旧使用刚才的例子,演示一下详细过程:

b67d286b09dda207a28a1d69940719e1.png

第一步,寻找顾客数组的中间元素。

在这里,中间元素是9:

c6fdef251aea3d492e9767f7f6cb1599.png

第二步,验证饭量从2到9的顾客能否满足。

子步骤1,遍历蛋糕数组,寻找大于9的蛋糕,最终寻找到元素15。

25bd2d2309728ad31613a76200c8620e.png

子步骤2,饭量9的顾客吃掉15的蛋糕,还剩6大小。

9bf30ea53fb2debffc847c1a5ce20e84.png

子步骤3,重新遍历蛋糕数组,寻找大于7的蛋糕,最终寻找到元素17。

c71acc7d02920f03754a2776c8c2c9d6.png

子步骤4,饭量7的顾客吃掉17的蛋糕,还剩10大小。

fedc77e0163e681d0be288db7064d069.png

子步骤5,重新遍历蛋糕数组,寻找大于5的蛋糕,最终寻找到元素5。

3ead8eeed70b31532190600880ebc00f.png

子步骤6,饭量5的顾客吃掉5的蛋糕,还剩0大小。

74089074a6ee726c29e7b3e9e592196f.png

子步骤7,重新遍历蛋糕数组,寻找大于2的蛋糕,最终寻找到元素3。

b8a2dc4ad29d4f3f070b0efc88da952a.png

子步骤8,饭量2的顾客吃掉3的蛋糕,还剩1大小。

f96ac5a7766a93230b57dad30fbc76e4.png

到此为止,从2到9的所有顾客都被满足了,验证成功。

接下来,我们需要引入更多顾客,从而试探出蛋糕满足的顾客上限。

第三步,重新寻找顾客数组的中间元素。

由于第二步验证成功,所以我们要在元素9右侧的区域,重新寻找中间元素。显然,这个中间元素是14:

ba6223765625db0e572e4916609af388.png

第四步,验证饭量从2到14的顾客能否满足。

这一步和第二步的思路是相同的,这里就不详细阐述了。最终的验证结果是,从2到14的顾客能够满足。

接下来,我们还要引入更多顾客,试探出蛋糕满足的顾客上限。

第五步,重新寻找顾客数组的中间元素。

由于第四步验证成功,所以我们要在元素14右侧的区域,重新寻找中间元素。显然,这个中间元素也就是唯一的元素20:

96bec4496b9c9cf56b65474a56a22991.png

第六步,验证饭量从2到20的顾客能否满足。

这一步和第二步、第四步的思路是相同的,这里就不详细阐述了。最终的验证结果是,从2到20的顾客不能够满足。

经过以上步骤,我们找到了最大满足顾客的临界点14,从2到14共有6个顾客,所以给定蛋糕最大能满足的顾客数量是6

49240a289ed05ed59ce67d2e1f9c1e3f.png

e0e6a8c3d283f565eda27a823ad2f6cb.png

//剩余蛋糕数量
static int leftCakes[];
//蛋糕总量(不是数量,而是大小之和)
static int totalCake = 0;
//浪费蛋糕量
static int lostCake = 0;
static boolean canFeed(int[] mouths, int monthIndex, int sum)
{
   if(monthIndex<=0) {
       //递归边界
       return true;
   }
   //如果 蛋糕总量-浪费蛋糕量 小于当前的需求量,直接返回false,即无法满足
   if(totalCake - lostCake < sum) {
       return false;
   }
   //从小到大遍历蛋糕
   for(int i=0;i<leftCakes.length; i++) {
       if (leftCakes[i] >= mouths[monthIndex]) {
           //找到并吃掉匹配的蛋糕
           leftCakes[i] -= mouths[monthIndex];
           //剩余蛋糕小于最小的需求,变为浪费蛋糕
           if (leftCakes[i] < mouths[1]){
               lostCake += leftCakes[i];
           }
           //继续递归,试图满足mid下标之前的需求
           if (canFeed(mouths, monthIndex-1, sum)) {
               return true;
           }
           //无法满足需求,蛋糕状态回滚
           if (leftCakes[i] < mouths[1]) {
               lostCake -= leftCakes[i];
           }
           leftCakes[i] += mouths[monthIndex];
       }
   }
   return false;
}
public static int findMaxFeed(int[] cakes, int[] mouths){
   //蛋糕升序排列,并把0下标空出
   int[] cakesTemp = Arrays.copyOf(cakes, cakes.length+1);
   Arrays.sort(cakesTemp);
   for(int cake: cakesTemp){
       totalCake += cake;
   }
   //顾客胃口升序排列,并把0下标空出
   int[] mouthsTemp = Arrays.copyOf(mouths, mouths.length+1);
   Arrays.sort(mouthsTemp);
   leftCakes = new int[cakes.length+1];
   //需求之和(下标0的元素是0个人的需求之和,下标1的元素是第1个人的需求之和,下标2的元素是第1,2个人的需求之和.....)
   int[] sum = new int[mouths.length+1];
   for(int i=1;i<=mouths.length;i++) {
       sum[i] = sum[i - 1] + mouthsTemp[i];
   }
   //left和right用于计算二分查找的“中点”
   int left=1,right=mouths.length;
   //如果胃口总量大于蛋糕总量,right指针左移
   while(sum[right]> totalCake){
       right--;
   }
   //中位指针,用于做二分查找
   int mid=((left+right)>>1);
   while(left<=right)
   {
       //重置剩余蛋糕数组
       leftCakes = Arrays.copyOf(cakesTemp, cakesTemp.length);
       //重置浪费蛋糕量
       lostCake =0;
       //递归寻找满足需求的临界点
       if(canFeed(mouthsTemp, mid, sum[mid])){
           left=mid+1;
       } else {
           right = mid - 1;
       }
       mid=((left+right)>>1);
   }
   //最终找到的是刚好满足的临界点
   return mid;
}
public static void main(String[] args) {
   int[] cakes = new int[]{3,5,15,17,25};
   int[] mouths = new int[]{2,5,7,9,12,14,20};
   int maxFeed = findMaxFeed(cakes, mouths);
   System.out.println("最大满足顾客数:" + maxFeed);
}

这段代码比较复杂,需要说明几点:

  1. 主流程方法findMaxFeed,执行各种初始化,控制二分查找流程。

  2. 方法canFeed,用于检验某一位置之前的顾客是否能被给定蛋糕满足。

  3. 数组leftCakes,用于临时存储剩余的蛋糕大小,每次重新设置中间下标时,这个数组需要被重置。

c65b93787b52b3d28588b3c9004380e3.png

91900cb737a83400e20c8702fb95ca6f.png

5bed63b5c44d7cc8d8394d2778e06b4e.png

a657b1a4ebb9b39425ffbae0822d74f4.png

add1636d2d02142482bd8c1f24f3b0c1.gif

延伸阅读

403c4e612fee573974138f36d2c1ffe6.png

延伸阅读《算法导论》

干货直达

更多精彩

在公众号对话框输入以下关键词

查看更多优质内容!

读书 | 书单 | 干货 讲明白 | 神操作 | 手把手

大数据 | 云计算 | 数据库 | Python | 爬虫 | 可视化

AI | 人工智能 | 机器学习 | 深度学习 | NLP

5G | 中台 | 用户画像 数学 | 算法 数字孪生

据统计,99%的大咖都关注了这个公众号

原网站

版权声明
本文为[大数据v]所创,转载请带上原文链接,感谢
https://blog.csdn.net/zw0Pi8G5C1x/article/details/125230073