0%

算法大作业笔记(五)

算法初步实现

//剩余蛋糕数量

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….这样还不如采用二分法