算法初步实现
//剩余蛋糕数量
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,用于临时存储剩余的蛋糕大小,每次重新设置中间下标时,这个数组需要被重置。
在极端情况下,性能不高,有很多优化空间。比如,在 canfeed方法中,同样可以采用二分的方式,而不是一点点去遍历。再者,我们也可以尝试用动态规划的思想来解决问题。
问题
为什么不把蛋糕从大到小排序,而顾客从小到大排序,再从左至右匹配呢,这样被浪费的大蛋糕是最小的,自然满足了最多的顾客了,这样会不会更快呢?
举个反例:有两个蛋糕,大小50和1,有两个顾客,胃口1和50,如果蛋糕从大到小,只能满足一个顾客,浪费了49
第一反应,类似背包问题
一开始小灰也觉得像背包问题,但背包的物品要么选择,要么不选,蛋糕却可以任意拆分,可能性非常多
到第四步的时候,饭量2到9的顾客已经满足了,为什么还要检查2到14的顾客呢?应该是检查12到14的吧?到了第五步,14及之前的顾客已经满足,剩下的蛋糕还有个25的,为什么不能满足饭量为20的顾客呢?
涉及12到14的顾客时,蛋糕需要重新分配,而不能仅仅用之前剩下的蛋糕分配。所以必须重新检查2到14
假设蛋糕为1,3,14,28 食客为1,3,13,14,15 按照这个算法,13的食客会吃掉14的蛋糕,剩余1,最终得到的结果就会是4,而实际上,如果把28的分给13和15,由14的食客吃掉14的蛋糕,结果应该是5
可以用小灰的代码自己测试下,我这边测了结果是5
为什么不从吃蛋糕最多的人开始查找呢?~因为如果是最大的吃蛋糕的人不符合,那么就可以直接递减,如果算法吃的多的人满足~那么吃得少的也会依次计算~还是满足的~这样会不会更好?~
“为什么不从吃蛋糕最多的人开始查找呢?”,你指的是哪一步?是二分查找之前还是二分查找内部?目前二分查找内部已经是从饭量最多的人开始查找了,而外部使用二分查找是更高效的方式。
第一种从左到右的遍历,和第二种二分查找遍历,只是时间复杂度上的不同,为什么会有的结果?
因为第二种思路在验证是否满足顾客时,带走回滚操作,极端情况下会枚举出所有可能性
研究了下代码,代码没问题,讲解的有问题,例子也举的不够好。二分法直接用是图省事了,其实可以排序后计算蛋糕总和S,以此算出最大可能满足的人数n后直接验证人数n能减少资源占用及计算量
漫画讲解的是最核心步骤,所以涉及回滚之类的细节就只体现在代码当中。你所说的计算最大可能满足人数n,并不一定是能通过验证,然后还得继续验证n-1,n-2….这样还不如采用二分法