前言
本文是作者自己在进行算法实验时看到的一道题,觉得有意思,所以写文记录。格式可能不是很规范,比如没有测试数据,只是以函数的形式给出代码,如果有读者对程序感兴趣,可以私博主寻求代码和测试数据,回应不即时还望见谅。
有与作者不同的想法也欢迎评论区留言讨论!
另:本文分析过程较为详细,如只寻求结论看高亮部分即可。
原问题
题目描述
六一儿童节快到了,作为老师的你想要给班里的孩子们分发一些蛋糕,但是考虑到经费问题,每个孩子最多只能分到一个蛋糕。班里面共有 n 个孩子,第 i 个孩子的胃口值是 g[i]。你提前买到了 m 个蛋糕,第 j 个蛋糕的尺寸是 s[j]。第 i 个孩子感到满足的条件是他分配到的蛋糕尺寸大于等于他的胃口值,即 s[j] >= g[i]。你能找到一种合理分配蛋糕的方案使得感到满足的孩子数量最多吗?请输出这个数量。
题目分析
看到这个题目我们便会想到这题最好是能够把比较小的蛋糕给到那些能够刚好满足的孩子,这样更大一些的蛋糕就可以用来满足那些胃口更大点的孩子,这也就是所谓的贪心算法。
所以我们在这里需要拿着小蛋糕(当前最小的蛋糕)按照胃口值从大到小的顺序来询问每个孩子。详细步骤如下:
“拿到”最小的蛋糕(s[0])去询问胃口最小的孩子,如果可以满足,毫不犹豫地给他,这时连最小的蛋糕都有人要,对那些更大的蛋糕就可以用来满足胃口值更高的孩子,此时下一个胃口稍大的孩子就成为了胃口最小的孩子,也就是我们下一块蛋糕的第一个询问对象;如果连胃口最小的孩子都无法满足,那么这个蛋糕就无法满足任何孩子,我们就换下一块稍大一点的蛋糕(此时它变成了最小的蛋糕),对它继续进行上面的操作。按照当我们的蛋糕可以满足某个孩子时,那么这个孩子一定是当前胃口值最小的孩子。
我们用一个变量child来记录满足了多少孩子。
伪代码示例
按照上面的思想,我们可以写出如下的伪代码
Algorithm 1 Assign-Cake-Ⅰ
1: function Assign-Cake-Ⅰ(s,g)
2: child=i=j=0
3: sort s and g
4: while i<g.length and j<s.length
5: if g[i]<s[j]
6: i++, j++, child++
7: else
8: j++
9: return child
演示代码
这里的代码并没有给出测试数据,只是给出处理数据的函数,重要的是解决问题时贪心算法的应用,如果想要可以私信博主要测试数据和测试代码。
这里的代码是一个处理这种问题的函数,就像伪代码中给出的那样。
1 int assign_cake_1(vector<int> g, vector<int> s) {
2 int child=0,i=0,j=0;
3 sort(g.begin(),g.end());
4 sort(s.begin(),s.end());
5 while ((i<g.size())&&(j<s.size())){
6 if (g[i]<=s[j]){
7 i++;
8 j++;
9 child++;
10 }else{
11 j++;
12 }
13 }
14 return child;
15 }
蛋糕分配问题的一个变式
题目描述
在本题中,每个孩子的胃口值是一个区间 g[i][0] ∼ g[i][1],只有他收到的蛋糕尺寸 x 满足 g[i][0] ≤ x ≤ g[i][1] 时,才会感到满足。现在老师买了 m 种蛋糕,第 j 种蛋糕的尺寸为 s[j][0],共买了 s[j][1] 个。
你能找到一种合理分配蛋糕的方案使得感到满足的孩子数量最多吗?请输出这个数量。
题目分析
从观感上说这道题和上面的那道题是非常相似的,所以我们直觉上会感觉这两道题的解法差别不大,事实上呢,前面的方法确实可以解决这个问题。
当我解决这个问题的时候,我的想法和前面的算法的想法完全一致,就把尽量小的蛋糕给到胃口相对较小的孩子。但是这种想法遇到了问题,比如我们考虑这样一种极端的情况(这种情况不一定真的会在代码执行的过程中发生,只是为了给读者展示前面的算法照搬过来会遇到的窘境),假设只有两个孩子甲乙,他们的胃口值分别为 g[0][0] ∼ g[0][1] 和 g[1][0] ∼ g[1][1] ,假设只有两个蛋糕,其尺寸为s[0],s[1],这6个值的关系为 g[0][0] ≤ g[1][0] ≤ s[0] ≤ g[1][1] ≤ s[1] ≤ g[0][1]。按照前面的想法,先把s和g数组进行排序,一般我们对一个vector类型的值进行排序的时候会首先考虑以vector的第一个值为标准进行排序,就是说对小孩的胃口值,我们以它的下界为标准进行排序;对蛋糕我们以它的尺寸为标准排序。
排序过后,有趣的地方来了,如果我们选择像上一题的贪心策略一样,把较小的蛋糕给胃口比较小的孩子会发现,这个时候我们做不到全局的最优。为了便于理解和讲解,我假设了一种比较简单的情况的简易示意图如下:
红色和黄绿色的框框分别表示小孩甲和小孩乙的胃口值,蓝色和黄色的小标是蛋糕A和蛋糕B的尺寸。从全局最优的角度,我们作为人很容易可以看出,蛋糕A给小孩乙,蛋糕B给小孩甲,总共两个孩子被满足是最优解。但是如果按照上一题的贪心策略,那么蛋糕A就要先给小孩甲,蛋糕B没人要,总共一个孩子被满足,这显然是不正确的。
那么问题到底出在哪里了呢?可以发现,孩子们的胃口区间的下确界是递增的,而上确界是没有规律的,我们的上一题的贪心策略可以成功在于上一题满足了上确界没有递减的情况出现。而这一题中我们保证的是下确界的递减,那么我就想到,其实本题并不是胃口越大的孩子越难以满足,而是这些胃口区间比较小的孩子难以满足(嘴刁),因为这些孩子们的合适的蛋糕很容易被胃口大的孩子抢走,这就和我们的生活常识有相通之处。而且我们定义的嘴刁的孩子并不是胃口区间长度的大小,而是它是否被另一个孩子的胃口区间覆盖,如果为前者,那他就是嘴刁的孩子。
而如何把嘴刁的孩子和其他孩子分开呢?这就要想到我们的排序方式了,我们是按胃口下确界进行排序的,也就是说,那些嘴刁的孩子胃口区间被覆盖只需要验证它的上确界小就可以了。也就是说,在我们的排序方式中,需要重点验证的不是下确界,而是上确界。
那么如何详细探索上确界呢?拿更大的蛋糕去试探嘛。从算法的角度说,就是改变遍历的方向咯。
于是在这一题中,我们有了理论的解法,把蛋糕从大到小遍历,按胃口区间下确界从大到小的顺序去挨个满足孩子们。
为了证实算法的可行性,我列出了两个蛋糕两个孩子的简单版本的所有情况,读者可自行验证。
算法伪代码
Algorithm 3 Assign-Cake-Ⅱ
1: function Assign-Cake-Ⅱ(s,g)
2: child=0,i=s.length-1
3: sort s and g
4: while i≥0
5: j=s.length-1
6: while j≥0
7: if s[i]<1
8: break
9: elseif g[i][0]≤s[j][0]≤g[i][1]
10: child++, s[i][1]—
11: delete g[i][] //the child has been fed
12: else j-- //the cake is too small so ask child with less appetite
13: i-- //take a smaller cake
14: return child
演示代码
1. int assign_cake(vector<vector<int>>& g, vector<vector<int>>& s) {
2. sort(g.begin(),g.end(),cmp);
3. sort(s.begin(),s.end(),cmp);
4. int i=s.size()-1,j;
5. int child=0;
6. while (i >= 0)
7. {
8. j=g.size()-1;
9. while (j >= 0)
10. {
11. if (s[i][1]<1)
12. {
13. break;
14. }
15. else if ((s[i][0] >= g[j][0])&&(s[i][0] <= g[j][1]))
16. {
17. child++;
18. g.erase(g.begin()+j);
19. s[i][1]--;
20. }else {
21. j--;
22. }
23. }
24. i--;
25. }
26. return child;
27. }
28. static bool cmp(vector<int> <h, vector<int> &rth) {
29. return lth[0] == rth[0] ? lth[1] < rth[1] : lth[0] < rth[0];
30. }