贪心学习笔记
定义:
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
例题一:合并费用问题
题解
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
int main()
{
priority_queue<int,vector<int>,greater<int> > p;
int n;
while(~scanf("%d",&n))
{
if(p.size()) p.pop();
for(int i=0; i<n; i++)
{
int temp;
scanf("%d",&temp);
p.push(temp);
}
int sum=0;
while(p.size()>1)
{
int x,y;
x=p.top();
p.pop();
y=p.top();
p.pop();
sum+=(x+y);
p.push(x+y);
}
printf("%d\n",sum);
}
return 0;
}
从上面的例子可以发现,贪心思想中的一种办法就是利用局部最优,将n转化至n-1,n-1转化到n-2……以此类推,直至问题解决
例题二:区间调度
题目描述
李旭琳发现小墨老师在班上是最顽劣的学生(没有之一),但他也有安静的时候,例如在看电视的时候。像什么“谍战剧”啊,“翻拍剧”啊,“婆媳戏”啊,“后宫剧”啊都是他的最爱。他甚至会事先查询所有喜欢看的电视节目的转播时间表并煞有介事的用红蓝铅笔列出计划,然后合理安排,以看到尽量多的完整节目。
输入
输入数据包含多个测试实例,每个测试实例的第一行只有一个整数n(n≤100),表示喜欢看的节目的总数,然后是n行数据,每行包括两个数据Ti_s,Ti_e (1≤i≤n),分别表示第i个节目的开始和结束时间,为了简化问题,每个时间都用一个正整数表示。n=0表示输入结束,不做处理。
输出
对于每个测试实例,输出能完整看到的电视节目的个数,每个测试实例的输出占一行。
样例输入
12
1 3
3 4
0 7
3 8
15 19
15 20
10 15
8 18
6 12
5 10
4 14
2 9
0
样例输出
5
考虑使用贪心算法的解法。为了方便,我们用不同颜色的线条代表每个活动,线条的长度就是活动所占据的时间段,蓝色的线条表示我们已经选择的活动;红色的线条表示我们没有选择的活动。
如果我们每次都选择开始时间最早的活动,不能得到最优解:
如果我们每次都选择持续时间最短的活动,不能得到最优解:
认真思考后不难发现,由于题目要求的是最多的活动的数量,一开始应该安排最先结束的活动,为后面的活动留下尽可能多的时间。
下图所示的活动集合S,其中各项活动按照结束时间单调递增排序
题解:
#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
using namespace std;
const int N=100;
struct Time
{
int start,End;
}pro[N];
bool cmp(Time a,Time b)
{
if(a.End!=b.End)
return a.End<b.End;
else return a.start<b.start;
}
int main()
{
int n;
while(scanf("%d",&n),n)
{
for(int i=0;i<n;i++)
scanf("%d %d",&pro[i].start,&pro[i].End);
sort(pro,pro+n,cmp);
int now=0,ans=0;
for(int i=0;i<n;i++)
{
if(pro[i].start>=now)
{
now=pro[i].End;
ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
例题三:均分纸牌
分析:
0)假设纸牌数量可以是负数
1)对于最左边的纸牌,为了使它的纸牌数达到平均,只要还没有达到平均无论其余子情况如何移动,一定有一步是把自己多余的纸牌移动到右边,或者是从右边移动进来自己差了多少张纸牌
2)第一堆牌只有和右边进行交互是合法的,步骤1是必须的
3)处理好第一堆后,其余操作一定不涉及第一堆,否则答案更劣(经过前一堆是没有意义的)
4)无视第一堆,于是现在又是情况1了
5)对于一个会出现负数的方案,通过调整移动顺序,一定可以转变为一个不出现负数的方案。
题解
#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
using namespace std;
const int N=100;
int card[N];
int main()
{
int n;
int ave=0;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&card[i]);
ave+=card[i];
}
ave/=n;
int sum=0;
for(int i=0;i<n-1;i++)
{
if(card[i]<ave)
{
sum++;
card[i+1]-=(ave-card[i]);
}
else if(card[i]>ave)
{
sum++;
card[i+1]+=(card[i]-ave);
}
}
printf("%d\n",sum);
return 0;
}
例题四:最大整数问题
题目:
有n个正整数,将它们依次连成在一排,组成一个多位数,现在要求可能组成的多位数中最大的多位数是什么?
样例输入:
3
13,312,343
样例输出:
343-312-13
很容易想到一个策略:把数字按照从大到小的顺序连接(大的在前面)。
但是这种想法是错误的。按这种贪心标准,我们很容易找到反例:12,121 应该组成12-121而非121-12,那么是不是相互包含的时候就从小到大呢?也不一定,如:12,123 就是123-12而非12-123,这样情况就有很多种了。
事实上,如果两个字符串A,B满足A+B>B+A那么显然有A串放在前面时A和B组成的字典序最大,而前面想法的错误的根源就是错以为如果A>B则有A+B>B+A 。
所以,自定义一种字符串的比较规则:
如果A+B>B+A,则我们认为A比B更值得放前面。
且可以证明:如果A+B>=B+A,B+C>=C+B,则一定有:A+C>=C+A。
这样一来,程序就很简单了。分3步:
(1)先把n个数字转换成字符串存储;
(2)按照自定义的规则把n个字符串从大到小排序;
(3)依次输出这些字符串。
一些简易的贪心题目往往通过短暂的思考能够很快的接近正确的策略,但是注意证明策略的正确性。
常见的证明方法有:
1.微扰(临项交换):
证明在任意局面下,任何对局部最优策略的微小改变都会使整体结果变差。经常用于以“排序”为贪心策略的证明。
2.范围缩放:
证明任何对局部最优策略作用范围的扩展都不会造成整体结果变差。
3.反证法
4.数学归纳法
5.决策包容性:
这个局部最优策略提供的可能性包含其他所有策略提供的可能性
例题五:决策包容性
证明:
每瓶防晒霜是否可用,会被minSPF与maxSPF两个条件限制。因为奶牛已被按照minSPF递减排序,所以每一个不低于当前奶牛minSPF的防晒霜,都不会低于后面奶牛的minSPF。
也就是说,对于当前奶牛可用的任意两瓶防晒霜x和y,如果SPF[x]<SPF[y],那么后面的奶牛可能出现的情况有:
1.x,y都能用
2.x,y都不能用
3.x能用,y不能用
因此当前奶牛选择SPF较大的y去使用,对整体问题的影响显然比选择SPF较小的x更好。
另外,每头奶牛对答案的贡献至多是1.即使让当前这头奶牛放弃日光浴,留下防晒霜给后面某一头奶牛用,对答案的贡献也不会变得更大。
所以,尽量满足当前的奶牛,并选择SPF值尽量大的防晒霜是一个正确的贪心策略
例题六:国王游戏
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,
使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。
贪心策略:
找最大值的最小值,乍一看像二分,但是仔细想想不满足单调性,所以二分不行。
可以这样思考,相邻两个的人交换对于前面的人答案没影响,而且对于后面的人答案也没有影响。也就是说相邻两人的位置交换只会对这两个人产生影响。那么我们就先考虑这两个人。
设这两个人分别为i和i+1。左手数字为a[i]和a[i+1],右手数字为b[i]和b[i+1]。两人的金币数为w[i]和w[i+1]。
记P[i]=a[1]*a[2]a[3]…*a[i]
可得:
w[i]=P[i-1]/b[i];
w[i+1]=P[i]/b[i+1];
又P[i]=P[i-1]*a[i]
那么 w[i+1]=P[i-1]*a[i]/b[i+1]=w[i]*a[i]*b[i]/b[i+1]
不难看出,在这个相邻的二元组中,前面的数不受后面的影响,而后面的金币数决定于w[i],a[i],b[i]。
推广到整个排队方案,前面的金币数和a[i],b[i]都会影响后面的答案。贪心原则便出来了:按a[i]*b[i]为关键字从小到大排序,相同的顺序无所谓。最后再扫一遍,算出答案即可。