Foreward:
Greed is human nature, curiosity is human motivation.
贪婪是人类的本性,求知是人类的动力。
今天要介绍的算法是贪心算法(Greedy),又名贪婪算法
整体会比较基础易懂
所以 What’s the greedy algorithm?
Definition:
A greedy algorithm is a simple, intuitive algorithm that is used in optimization problems. The algorithm makes the optimal choice at each step as it attempts to find the overall optimal way to solve the entire problem.
Wiki
简单来说,就是在每一步寻求当前最优解,不断追求局部最优,与之后会讲到的考虑全局最优的动态规划恰恰相反
使用贪心算法解决问题时,会将问题分为若干个子问题,利用贪心的原则从内向外依次求出当前子问题的最优解,也就是说会从小利益出发,从局部考虑问题,求得当前子问题的最优解,才能继续寻求下一个子问题的最优解,所以前一个子问题的最优解会是下一个子问题最优解的一部分,一直不断的一步步求子问题的最优解最终解决问题。
贪心算法最关键的部分在于贪心策略的选择,当贪心算法可以解决一个个子问题进而最终得到全局的最优解时,这就是一个不错的贪心算法。
需要注意的是,贪心算法的设计必须有“无后效性”的特点。
什么是无后效性呢?就是对于某个状态,也就是对于某个子问题的抉择,不会影响之前求得的局部最优解,否则就必须通过全局的考虑很分析来求得最终答案。
还有需要注意的是,贪心算法没有固定的解决方案,需要多加练习熟能生巧,当然用某个蒟蒻(就是彩笔)的话来讲,这是真正意义上开始学的第一个最基础的算法
贪心正确性的证明
方法一:数学归纳法
首先我们需要构造一个贪心的算法P(n)
我们使用第一或第二数学归纳法证明
P
(
n
)
P(n)
P(n)。
- 第一数学归纳法:
证明: P ( 1 ) P(1) P(1)为真;若 P ( n ) P(n) P(n)为真,则 P ( n + 1 ) P(n+1) P(n+1)为真。 - 第二数学归纳法:
证明: P ( 1 ) P(1) P(1)为真;若对所有 k < n k<n k<n,有 P ( k ) P(k) P(k)为真, 则 P ( n ) P(n) P(n)为真。
方法二:交换论证法
- 分析一般最优解与贪心法的解的区别,然后定义一种转换规则,使得从任意一个最优解出发,经过不断对解的某些成分的排列次序进行交换或者用其他元素替换,将这个解最终能够转变成贪心法的解。
- 证明在上述转换中解得优化函数值不会变坏。
- 证明上述转换在有限步结束。
最后总结一下贪心的基本做法
- 建立数学模型来描述问题
- 把求解的问题分成若干个子问题
- 对每个子问题求解,得到子问题的局部最优解
- 把子问题的解局部最优解合成原来问题的一个解
记得不是一眼题的话稍微证明一下正确性哦
好啦理论结束咯
我们来看几个非常简单的例子开始~
1. 教室调度问题
若有一个课表,作为一个好学的SCIE三好青年,你希望做出最佳的选课方案(将尽可能多的课程安排在课表之上)
课程 | 开始时间 | 结束时间 |
---|---|---|
ADO社🔥1 | 9AM | 10AM |
Computer Science | 9:30AM | 10:30AM |
ADO社🔥2 | 10AM | 11AM |
Sailing Lesson | 10:30AM | 11:30 |
ADO社🔥3 | 11AM | 12 PM |
很显然最佳选择是ADO社🔥+ADO社🔥+ADO社🔥
不草率的来分析一下,你不能让所有课都在课表上,因为有一些课程是由冲突的
你希望在这间教室上尽可能多的课,很显然这不是我的最优策略 (大雾)。如何选出尽可能多且时间不冲突的课程呢?
看似是一个有一些复杂的问题,但实际上简单到出乎意料
做法是这样的:
- 选出结束最早的课,作为选中的第一节课
- 接下来,在第一节课结束后开始的课中,选出结束最早的课,就是要选的第二节课。
- 重复这个操作即可
我们来手玩一下这个过程
我们发现“ADO社🔥1”是这之中结束时间最早的,所以说第一个选择就是ADO社🔥1,然后剩余的课必须在10AM之后,于是你就愉快的不用去上Computer Science的课啦
课程 | 开始时间 | 结束时间 | 是否选择 |
---|---|---|---|
ADO社🔥1 | 9AM | 10AM | 是 |
Computer Science | 9:30AM | 10:30AM | 否 |
ADO社🔥2 | 10AM | 11AM | |
Sailing Lesson | 10:30AM | 11:30 | |
ADO社🔥3 | 11AM | 12 PM |
接下来,重复这个操作,根据贪心算法以及你好学生的优秀品质,你再次 很遗憾的失去了1h的划水时间(懂的都懂),前去ADO社🔥2好好学习[Fighting]
课程 | 开始时间 | 结束时间 | 是否选择 |
---|---|---|---|
ADO社🔥1 | 9AM | 10AM | 是 |
Computer Science | 9:30AM | 10:30AM | 否 |
ADO社🔥2 | 10AM | 11AM | 是 |
Sailing Lesson | 10:30AM | 11:30 | 否 |
ADO社🔥3 | 11AM | 12 PM |
最终你发现你的第三节课将继续留在ADO教室~
于是,你又将生命中的一个上午奉献 在了ADO。
好的,手玩完毕,相信很多人想问,这么草率的吗??!
其实这个题,确实只能说证明显然
实在要证明正确性的话:
从贪心算法得到的结果集进行倒推,去掉第一个活动,在剩下的活动中结束时间最早的活动B的结束时间一定不小于A,那么A活动一定合理的在最优解之中,同理可判断剩下的所有活动。
代码有需要的随时找我交流啦~ 之后一些比较重要的题会放上代码供参考的!
好的编了很久的故事讲完了,我们来看下一个题
2. 均分纸牌为题
这是一个非常经典的问题
Description:
有
N
N
N堆纸牌,编号分别为
1
,
2
,
…
,
n
1,2,…,n
1,2,…,n。每堆上有若干张, 但纸牌总数必为
n
n
n的倍数.可以在任一堆上取若干张纸牌,然后移动。移牌的规则为:在编号为
1
1
1上取的纸牌,只能移到编号为
2
2
2的堆上;在编号为
n
n
n的堆上取的纸牌,只能移到编号为
n
−
1
n-1
n−1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
例如:
n
=
4
,
4
n=4,4
n=4,4堆纸牌分别为:
①
9
②
8
③
17
④
6
① 9 ② 8 ③ 17 ④ 6
①9②8③17④6
移动三次可以达到目的:
从③取4张牌放到④ 再从③取3张放到②然后从②取1张放到①。
给定纸牌堆数
N
N
N以及每一堆纸牌的数量
a
1
.
.
.
n
a_1...n
a1...n, 求即所有堆均达到相等时的最少移动次数
#输入1:
4
9 8 17 6
#输出1:
3
我们拿到这个题,注意到题中的一句话
纸牌总数必为N的倍数
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
那么也就是说, 每一堆纸牌还需要的纸牌的数量或者需要移出纸牌堆的数量就是用每堆的纸牌数减去平均数
我们注意到
在编号为 1 1 1上取的纸牌,只能移到编号为 2 2 2的堆上;在编号为 n n n的堆上取的纸牌,只能移到编号为 n − 1 n-1 n−1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
那么,显而易见的思路就是
用贪心算法,按照从左到右的顺序移动纸牌。如第
i
i
i堆的纸牌数不等于平均值,则移动一次,分两种情况移动:
1.若
a
[
i
]
>
a
v
g
a[i]>avg
a[i]>avg,则将
a
[
i
]
−
a
v
g
a[i]-avg
a[i]−avg张从第
i
i
i堆移动到第
i
+
1
i+1
i+1堆
2.若
a
[
i
]
<
a
v
g
a[i]<avg
a[i]<avg,则将
a
v
g
−
a
[
i
]
avg-a[i]
avg−a[i]张从第
i
+
1
i+1
i+1堆移动到第
i
i
i堆
在这里 a [ i ] a[i] a[i]表示每一堆纸牌的数量, a v g avg avg表示纸牌的平均数,也就是期望移成的样子.
我们再次来手玩一下样例
4
9 8 17 6
我们发现平均数是
(
9
+
8
+
17
+
6
)
/
4
=
10
(9+8+17+6)/4=10
(9+8+17+6)/4=10
于是显而易见的,
8
8
8需要移一张给
9
9
9,这样就变成了
107176
10 7 17 6
107176,移动纸牌数为
1
1
1
接下来,第三堆移动3张牌给第二堆,变成
1010146
10 10 14 6
1010146,移动纸牌次数为
2
2
2
最后显而易见,第三堆移到第四堆,总共移动次数为
3
3
3
我们来证明一下这个算法的正确性虽然也有点显然
我们假设纸牌数量可以是负数,然后对于最左边的纸牌,为了使它的纸牌数达到平均,只要还没有达到平均无论其余子情况如何移动,一定有一步是把自己多余的纸牌移动到右边,或者是从右边移动进来自己差了多少张纸牌。
于是我们可以知道第一堆牌只有和右边进行交互是合法的,所以第一堆纸牌往旁边的一堆纸牌移动或移入纸牌是必须的
处理好第一堆后,其余操作一定不涉及第一堆,否则答案更劣(经过前一堆是没有意义的)于是我们可以无视第一堆,于是现在又是就又回到了之前的情况,所以说最优的算法肯定是按照从左到右的顺序移动纸牌。
对于处理纸牌堆中纸牌个数不能为负的问题,调整纸牌移动顺序即可
其实这个问题还有一个环形的版本,但是环形均分纸牌问题是可以证明可链化,具体过程较复杂,有兴趣的大佬来找我讨论!
3. 摆动序列问题
Description:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如,
[
1
,
7
,
4
,
9
,
2
,
5
]
[1,7,4,9,2,5]
[1,7,4,9,2,5] 是一个摆动序列,因为差值
(
6
,
−
3
,
5
,
−
7
,
3
)
(6,-3,5,-7,3)
(6,−3,5,−7,3) 是正负交替出现的。相反,
[
1
,
4
,
7
,
2
,
5
]
[1,4,7,2,5]
[1,4,7,2,5]和
[
1
,
7
,
4
,
5
,
5
]
[1,7,4,5,5]
[1,7,4,5,5]不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
样例:
输入:
[
1
,
7
,
4
,
9
,
2
,
5
]
[1,7,4,9,2,5]
[1,7,4,9,2,5]
输出: 6
显然,这整个数列都是摆动序列
题目分析
我们来分析一下这个题目,题目需要我们求的是这个序列的摇摆序列长度。并且可以删除一些部分。那么现在的问题的关键就是使用什么方法判断这个序列是不是摇摆序列,如果不是的话我们该如何删除,删除哪些元素才能是当前子序列是摇摆子序列。我们看一下下面这张图,这是个折线图,但是在这线图中有一些连续上升或者连续下降的节点,我们需要把这些节点删除之后才能摇摆序列
于是我们现在思考的问题就变成了删除这些持续上升或者下降节点只保留其中的一个,具体保留哪一个。
这个保留的这个节点需要有最大的概率使得下一个节点也是摇摆序列中的节点这样最后求出来的才能是最长摇摆子序列。于是乎这个问题就完美的解决了。
4. 国王游戏
最后分享一个稍稍有一点点难度的题目,可以跳过
Description:
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。
首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。
然后,让这 n 位大臣排成一排,国王站在队伍的最前面。
排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:
排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。
注意,国王的位置始终在队伍的最前面。
输入格式
第一行包含一个整数 n,表示大臣的人数。
第二行包含两个整数 a 和 b,之间用一个空格隔开,分别表示国王左手和右手上的整数。
接下来 n 行,每行包含两个整数 a 和 b,之间用一个空格隔开,分别表示每个大臣左手和右手上的整数。
输出格式
输出只有一行,包含一个整数,表示重新排列后的队伍中获奖赏最多的大臣所获得的金币数。
数据范围
1
≤
n
≤
1000
1≤n≤1000
1≤n≤1000
0
<
a
,
b
<
10000
0<a,b<10000
0<a,b<10000
我们还是着重关注这题的做法
我们其实会显然发现我们总是希望队列中前面的的元素左手的元素尽可能的小,队列中后面的元素右手的元素尽可能的大
于是我们得到一个贪心做法,使得左右手数字乘积更小的排在前面
我们来证明一下它的正确性:
利用反证法
如果不是按照这种这种贪心做法(乘积从小到大),那么必定有一个元素使得
A
i
∗
B
i
>
(
A
i
+
1
)
∗
(
B
i
+
1
)
A_i*B_i > (A_i+1)*(B_i+1)
Ai∗Bi>(Ai+1)∗(Bi+1)(不然的话就是按照乘积从小到大排序了)
于是在
i
i
i和
i
+
1
i+1
i+1位置上能获得的奖励就是
i
i
i位置:
A
1
∗
.
.
.
∗
A
i
−
1
/
B
i
A_1*...*A_{i-1}/B_i
A1∗...∗Ai−1/Bi
i
+
1
i+1
i+1位置:
A
1
∗
.
.
.
∗
A
i
−
1
∗
A
i
/
B
i
+
1
A_1*...*A_{i-1}*A_i/B_{i+1}
A1∗...∗Ai−1∗Ai/Bi+1
我们把
i
i
i和
i
+
1
i+1
i+1两个位置交换一下,那么
i
i
i位置:
A
1
∗
.
.
.
∗
A
i
−
1
/
B
i
+
1
A_1*...*A_{i-1}/B_{i+1}
A1∗...∗Ai−1/Bi+1
i
+
1
i+1
i+1位置
A
1
∗
.
.
.
∗
A
i
−
1
∗
A
i
+
1
/
B
i
A_1*...*A_{i-1}*A_{i+1}/B_i
A1∗...∗Ai−1∗Ai+1/Bi
我们让交换前后都 除以 A 1 ∗ . . . ∗ A i − 1 A1*...*A_{i-1} A1∗...∗Ai−1 乘以 B i ∗ B i + 1 B_i*B_{i+1} Bi∗Bi+1
交换前 i i i: B i + 1 B_{i+1} Bi+1 j j j: A i ∗ B i A_i*B_i Ai∗Bi
交换后 i: B i B_i Bi j j j: A i + 1 ∗ B i + 1 A_{i+1}*B_{i+1} Ai+1∗Bi+1
那么
m
a
x
(
B
i
+
1
,
A
i
∗
B
i
)
>
m
a
x
(
B
i
,
A
i
+
1
∗
B
i
+
1
)
max(B_{i+1}, A_i*B_i) > max(B_i, A_{i+1}*B_{i+1})
max(Bi+1,Ai∗Bi)>max(Bi,Ai+1∗Bi+1)
所以交换后能使得结果更优,于是我们就得到了这个题的证明