导引问题:硕鼠的交易
-
任务:硕鼠准备了M 磅猫粮与看守仓库的猫交易奶酪。仓库有 N 个房间,每只猫看守一个房间。第 i 个房间有 J[i] 磅奶酪并需要 F[i] 磅猫粮交换,硕鼠可以交换得到这个房间的全部奶酪,也可以按比例来交换,不必交换所有的奶酪。计算硕鼠最多能得到多少磅奶酪。先输入 M 和 N 表示猫粮数量和房间数量,随后输入 N 个房间,每个房间包括奶酪数量和要交换的猫粮数量;
-
Sample Input:
5 3 // 表示有5磅猫粮和3个房间 7 2 // 第一个房间有7磅奶酪,要2磅猫粮才可以交换 4 3 // 第一个房间有4磅奶酪,要3磅猫粮才可以交换 5 2 // 第一个房间有5磅奶酪,要2磅猫粮才可以交换 -1 -1
-
Sample Output:
13.333
-
-
解法:
- 选择性价比最高的房间去做交易,用 可以交换的奶酪数 / 需要交换的猫粮数 即可计算出:单位猫粮可以得到的奶酪,
- 显然性价比:第一个房间 > 第三个房间 > 第二个房间;
- 计算过程:
- 用 2 磅猫粮换得到第一个房间的所有 7 磅奶酪,还剩 3 磅猫粮,目前所得奶酪磅数:7 磅;
- 用 2 磅猫粮换得到第三个房间的所有 5 磅奶酪,还剩 1 磅猫粮,目前所得奶酪磅数:12 磅;
- 用 1 磅猫粮换得到第二个房间的所有 4 / 3 * 1 磅奶酪,还剩 0 磅猫粮,目前所得奶酪磅数:13.333 磅。
初识“贪心算法”
- 在对问题求解时,总是作出在当前看来是最好的选择;
- 也就是说,不从整体上加以考虑,所作出的仅仅是在某种意义上的局部最优解;
- 但是否是全局最优,需要证明;
- 所以若要用贪心算法求解某问题的整体最优解,必须首先证明贪心思想在该问题的应用结果就是最优解!
例1:田忌赛马
-
田忌与齐威王赛马,谁输了谁给对方两百块钱;
- 绿色圆圈中的数字是田忌的马的战力,红色圆圈中的数字是齐威王的马的战力;
- 如果田忌按照左边的顺序选择马匹出战,是把把都输;
- 如果田忌按照右边的顺序选择马匹出战,还可以赢200块钱;
-
注意:不可以按照以下的想法去解题
- 用自己最差的和对方最好的比;
- 用自己最好的和对方第二好的比;
- 用自己第二好的和对方第三好的比;
- ……
- 但实际上:只要能比对方好,就没必要退而求其次;
-
用贪心算法捋一遍赛马的思路:假设齐威王的马匹出战顺序保持不变,都是 95,87,74
- 齐威王派出战力为 95 的马,田忌发现自己最好的马都打不过,就用最差的和齐威王的比;
- 因为反正都是输,就用自己最差的马去输,进而可以保留更好的战力;
- 齐威王派出战力为 87 的马,田忌发现自己最好的马可以打过,那就派出战力最好的马;
- 齐威王派出战力为 74 的马,田忌发现自己最好的马可以打过,那就继续派出战力最好的马。
- 齐威王派出战力为 95 的马,田忌发现自己最好的马都打不过,就用最差的和齐威王的比;
经典贪心:时间序列问题
-
已知 N 个事件的发生时刻和结束时刻:
-
一些在时间上没有重叠的事件,可以构成一个事件序列,如2号事件、8号事件和10号事件,这三个事件就构成了一个事件序列;
-
事件序列中包含的事件数目,称为该事件序列的长度(前面讲的事件序列的长度就是 3)。请编程找出一个最长的事件序列;
-
能不能每次选择时间最短的事件?
-
假设只有三个事件:
事件编号 0 1 2 发生时刻 11 0 12 结束时刻 13 12 24 -
显然,如果选择时间最短的事件——事件0,就不能选择事件1和事件2,但是事件1和事件2组合起来的事件序列才是最优解;
-
-
那还有哪些贪心的思路?比如:至少存在一个最长事件序列,这个序列中一定包含最早结束的事件
- 那么如何证明这个贪心思路是正确的呢?用反证法,即假设所有的最长事件序列都不包含最早结束的事件,下面就用反证法证明:
- 现在任选一个所谓的最长事件序列{x1, x2,x3,x4,x5},这五个事件在时间上没有重叠;
- 因为假设所有的最长事件序列都不包含最早结束的事件,那么 x1 就不是最早结束的事件,所以将其替换成最早结束的事件;
- 可以发现,替换完后,该事件序列依旧是最长的,且事件之间在时间上不冲突,但事件序列中却已经有最早结束的事件,所以假设不成立;
- 所以这个贪心思路就可以应用到解题中。每次选择一个和已经选择的事件在时间上不冲突的事件,且是最早结束的,流程如下:
- 选择事件0;
- 选择事假1;
- 事件2冲突,事件3冲突,事件4冲突,选择事件5;
- 事件6冲突,事件7冲突,选择事件8;
- 事件9冲突,选择事件10;
- 事件11冲突;
- 最后得到的结果:{0, 1, 5, 8, 10}
- 可以发现结果不唯一,因为最后一个事件选10或者11都可以。
- 那么如何证明这个贪心思路是正确的呢?用反证法,即假设所有的最长事件序列都不包含最早结束的事件,下面就用反证法证明:
例2:搬桌子
- 一个公司在一个写字楼里租了一层楼,该楼层的平面图如下图。中间是走廊,左右各两百个房间,房间的编号一边是奇数,一边是偶数。现在这个公司的人要将桌子从一个房间搬到另一个房间,但是走廊很窄,同时只允许一张桌子搬运,每次搬运不管远近都需要十分钟的时间。现在给出几组数据,求出每组数据需要的最短搬运时间(其实也是求最少搬运次数);
-
Sample Input:
3 // 3组数据 4 // 要搬4张桌子 10 20 // 从10号房搬到20号房 30 40 // 从30号房搬到40号房 50 60 // 从50号房搬到60号房 70 80 // 从70号房搬到80号房 2 1 3 2 200 3 10 100 20 80 30 50
-
Sample Output:
- 忽略这种情况:10搬到100,当搬运的人经过20后,20就出发;
- 这也是贪心策略提现:每次搬运桌子,不管其他搬运任务,先把当前搬运任务所经过的走廊段的占用次数记录下来。从贪心角度看,就是每一次只关注当前这个搬运任务对走廊的占用情况,不考虑整体的复杂调度,先把局部信息统计好;
10 20 30
-
解法:
-
在一组数据中,列出每一次搬运的路线,路线重叠几次就要搬运几次;
-
以下表为例:
房间号 1/2 3/4 5/6 7/8 1->5,重叠次数 1 1 1 0 3->7,重叠次数 0 2 2 1
-
-
代码:
#include <bits/stdc++.h> using namespace std; int main() { int t, i, j, N, P[200]; int s, d, temp, k, MAX; cin >> t; // 几组数据 for(i = 0; i < t; i++) { for(j = 0; j < 200; j++) // 刚开始重叠此处赋初值为0 P[j] = 0; cin >> N; // 要搬几张桌子 for(j = 0; j < N; j++) { cin >> s >> d; s = (s - 1) / 2; // 让奇数房间和偶数房间可以共用一个索引 d = (d - 1) / 2; if(s > d) // 如果是从大编号的房间搬到小编号的房间,调转一下“方向” { temp = s; s = d; d = temp; } for(k = s; k <= d; k++) // 记录搬运过程中,在路线上的房间会被重叠几次 P[k]++; } MAX = -1; for(j = 0; j < 200; j++) if(P[j] > MAX) MAX = P[j]; // 取出P数组中的最大值,就是最多搬运的次数 cout << MAX * 10 << endl; } return 0; }
例3:删数字
-
已知一个长度不超过240位的正整数n(其中不含有数字0),去掉其中任意s(s小于n的长度)个数字后,将剩下的数字按原来的左右次序组成一个新的正整数。给定n和s,请编程输出最小的新正整数;
-
Sample Input
178543 4
-
Sample Output
13
-
-
解法:
- 每次选一个最大的数删掉?反例:输入
213 1
,删掉3,得到结果是21。但是显然将2删掉,得到13是更好的结果; - 仔细思考一下可以发现,要使剩下的数字按原来的左右次序组成的一个新正整数最小,就要让左边的数越小越好,那么就可以将原来的数,从左到右依次两两比较,若左边的数大于右边的数,就删掉左边的数。以例题为例:
- 原数:178543,第一次删,删掉8,得到17543;
- 原数:17543,第二次删,删掉7,得到1543;
- 原数:1543,第三次删,删掉5,得到143;
- 原数:143,第四次删,删掉4,得到13;
- 每次选一个最大的数删掉?反例:输入
-
特殊情况:
123 1
- 左边没有比右边大的,怎么办?
- 删最大(最右边那个)。
例4:青蛙的邻居
-
一个城市中有若干个湖泊,每个湖泊都住着一只青蛙。只要两个湖泊之间有水路相连,就可以认为这两个湖泊中住的青蛙是邻居。现在给定一组数据,每组数据包含有几个湖泊,每个湖泊的青蛙有几个邻居,问能否绘制出这些湖泊的分布情况和水路连接情况;
-
比如有三个湖泊:
- 2 2 2,说明三个湖泊中的青蛙都互为邻居,可以绘制出一个三角形;
- 2 2 1,就无法绘制出;
-
Sample Input
3 // 3组数据 7 // 有7个湖泊 4 3 1 5 4 2 1 // 住在第1个湖泊的青蛙有4个邻居,住在第2个湖泊的青蛙有3个邻居…… 6 4 3 1 4 2 0 6 2 3 1 1 2 1
-
Sample Output
YES NO YES
-
-
解法:
- 用度数(一个顶点在图中的度为与这个顶点相连接的边的数目)的总和是否为奇偶来判断?
- 偶数可绘制例子:2 2 2;
- 偶数不可绘制(包含0)例子:2 2 0;
- 偶数可绘制(包含0)例子:1 1 0;
- 其实这道题的接法是离散数学的一个知识点——可图性判定;
- 用度数(一个顶点在图中的度为与这个顶点相连接的边的数目)的总和是否为奇偶来判断?
-
先讲解两个概念:
- 度序列:若把图 G 所有顶点的度数排成一个序列 S,则称 S 为图 G 的度序列;
- 序列是可图的:一个非负整数组成的有限序列如果是某个无向图的度序列,则称该序列是可图的;
-
可图性判定的算法有很多,此处讲解一个比较经典的——Havel-Hakimi定理:
- 由非负整数组成的非增序列 S S S: d 1 , d 2 , . . . , d n ( n ≥ 2 , d 1 ≥ 1 ) d_1, d_2,..., d_n (n \geq 2, d_1 \geq 1) d1,d2,...,dn(n≥2,d1≥1) 是可图的,当且仅当序列 S 1 S_1 S1: d 2 − 1 , d 3 − 1 , . . . , d d 1 + 1 − 1 , d d 1 + 2 , . . . , d n d_2 - 1, d_3 - 1,..., d_{d1 + 1} - 1, d_{d1 + 2},..., d_n d2−1,d3−1,...,dd1+1−1,dd1+2,...,dn是可图的;
- 其中,序列 S S S中有 n n n个非负整数,序列 S 1 S_1 S1中有 n − 1 n - 1 n−1个非负整数, S S S序列中 d 1 d_1 d1后的前 d 1 d_1 d1个度数(即 d 2 ∼ d d 1 + 1 d_2 \sim d_{d1 + 1} d2∼dd1+1)减1后构成 S 1 S_1 S1中的前 d 1 d_1 d1个数;
-
用这个定理解例4:
- 以第一组可图的数据:
4 3 1 5 4 2 1
为例;- 由于
S
S
S是非增序列,所以第一步先排序,得到
5 4 4 3 2 1 1
; - 删掉第一个数,然后后面的前 5 个数依次减1,得到新序列:
3 3 2 1 0 1
;- 根据上面定理中的第一句话,即只要
3 3 2 1 0 1
可图,那么5 4 4 3 2 1 1
就可图;
- 根据上面定理中的第一句话,即只要
- 新序列排序得到
3 3 2 1 1 0
,删掉第一个数,然后后面的前 3 个数依次减1,得到新序列:2 1 0 1 0
; - 新序列排序得到
2 1 1 0 0
,删掉第一个数,然后后面的前 2 个数依次减1,得到新序列:0 0 0 0
; - 只要得到一个全 0 序列,那么说明这个序列可图;
- 由于
S
S
S是非增序列,所以第一步先排序,得到
- 以第二组不可图的数据:
4 3 1 4 2 0
为例;- 由于
S
S
S是非增序列,所以第一步先排序,得到
4 4 3 2 1 0
; - 删掉第一个数,然后后面的前 4 个数依次减1,得到新序列:
3 2 1 0 0
; - 新序列排序得到
3 2 1 0 0
,删掉第一个数,然后后面的前 3 个数依次减1,得到新序列:1 0 -1 0
; - 只要序列中出现负整数,那么这个序列就不可图。
- 由于
S
S
S是非增序列,所以第一步先排序,得到
- 以第一组可图的数据:
贪心算法的常见前提操作——排序
-
关于
sort()
函数:- 头文件:
#include<algorithm>
; - 参数:
sort(首地址,尾地址+1,[cmp函数])
- 第一个参数是要排序的区间首地址(一般是数组名);
- 第二个参数是区间尾地址的下一地址(一般是数组名+数组长度);
- 排序区间就是
[数组首地址, 数组尾地址 + 1)
;
- 排序区间就是
- 第三个参数可以不写,若不写则默认为递增排序。
- 头文件:
-
sort
函数对int
类型的数组排序(从大到小):int num[100]; bool cmp(int a, int b) // 从大到小排序 { return a > b; } sort(num, num + 100, cmp);
- 对于定义了小于运算(
<
运算符 )的数据类型,其数组使用sort
函数进行排序的方式是类似的。比如上面中的int
类型,通过自定义比较函数来指定排序规则; - 对于
string
类型(字符串类 )也可以类似地操作,因为string
类重载了小于运算符,能直接用于比较大小; - 但字符数组(
char[]
)本身不支持直接用小于运算符来比较大小,所以在对字符数组进行排序时,需要使用strcmp
函数来实现比较功能,进而完成排序;
- 对于定义了小于运算(
-
sort
函数对结构体类型的数组排序:struct node { int a; double b; }arr[100]; // 先按a值升序排列,如果a值相同,再按b值降序排列 bool cmp(node x, node y) { if(x.a != y.a) return x.a < y.a; return x.b > y.b; } sort(arr, arr + 100, cmp);
-
sort
函数排序的一个综合示范:struct student { char name[11]; int age; double score; }stu[100]; bool cmp(student x, student y) { // 按x学生和y的学生的成绩降序排序 if(fabs(x.score - y.score) > 0.00001) // 浮点型千万不要用双等号比大小 return x.score > y.score; // 按x学生和y的学生的年龄升序排序 if(x.age != y.age) return x.age < y.age; // 要用strcmp函数对字符数组排序 return strcmp(x.name, y.name) < 0; }
-
特别注意:如果数组的元素是从下标1的位置开始存储,不要忘记修改
sort
函数的第一个参数;- 同时引申出:
sort
函数可以对数组的某一端进行排序。
- 同时引申出: