算法优点
容易理解:生活常见
操作简单:在每一步都选局部最优
效率高:复杂度常常是O(1)的
算法缺点
局部最优不一定是全局最优
贪心算法(Greedy algorithm),又称贪婪算法。是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而使得问题得到全局最优解。
贪心的算法的设计就是要遵循某种规则,不断地选取当前最优解的算法设计方法。
贪心算法基本概念
贪心算法与枚举法的不同之处在于每个子问题都选择最优的情况,然后向下继续进行,且不能回溯。
枚举法是将所有情况都考虑然后选出最优的情况。
贪心算法,在解决问题时,不从整体考虑,而是采用一种局部最优解的选择方式。并且,贪心算法没有固定的模板可以遵循,每个题目都有不同的贪心策略,所以算法设计的关键在于贪心策略的选择。
贪心算法有一个必须注意的事情。贪心算法对于问题的要求是,所有的选择必须是无后效性的,即当前的选择不能影响后续选择对于结果的影响 。
贪心算法主要适用于最优化问题,例如:最小生成树问题。有时候贪心算法并不能得到最优答案,但是能得到精确答案的近似结果。有时可以辅助其他算法得到不那么精确的结果。
- 贪心选择性质:贪心选择性质是指通过局部最优的选择,可以构造出局最优解。换句话说,一个问题的最优解可以通过一系列局部最优的选择得到,这些局部最优解最终叠加起来形成全局最优解。贪心算法的每一步都遵循贪心选择性质,即每一步都做出当前看起来最优的选择。
- 最优子结构:最优子结构是指问题的最优解包含其子问题的最优解。这意味着可以通过组合子问题的最优解来构造原问题的最优解。动态规划算法通常依赖于最优子结构性质,但贪心算法也可以利用这一点,尤其是在子问题相互独立的情况。
- 无后效性: 无后效性是指一个问题的状态一旦确定,就不受这个状态之前决策的影响。在贪心算法中,这意味着一旦做出选择,这个选择就不会影响未来步骤中的选择。无后效性保证了贪心算法的每一步都是独立的,可以单独考虑,而不需要考虑之前的步骤。
适用范围
符合贪心策略:
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
贪心选择性质就是指,该问题的每一步选择都在选择最优的情况下能够导致最终问题的答案也是最优。
或者说是无后效性,如果该问题的每一步选择都对后续的选择没有影响,就可以应用贪心算法。
最少硬币问题
硬币面值1、2、5。支付13元,要求硬币数量最少
贪心:
(1)5元硬币,2个
(2)2元硬币,1个
(3)1元硬币,1个
硬币面值1、2、4、5、6。支付9元。
贪心:
(1)6元硬币,1个
(2)2元硬币,1个
(3)1元硬币,1个
错误!
答案是:5元硬币+4元硬币
不可以使用贪心,如果支付九元的话,先选一个六块的,再选一个三块的,6,2,1。然而实际是5+4,
不能够选择当前最优
每次选当前最优解是推不出整体最优的
所以这个题是动态规划
在比赛中不需要严格证明,只需要举出一个反例,证明这个题不是贪心就可以了
一般的话,可以去看时间复杂度,贪心的复杂度取决于sort排序,是 O ( n log n ) O(n\log n) O(nlogn),一般这个时间复杂度刚好通过这个题
可以这样分析:是不是贪心,是不是二分,这样去判断这个题是什么题目,而不是去证明
贪心算法的设计步骤
按照定义设计:
- 证明原问题的最优解之一可以由贪心选择得到。
- 将最优化问题转化为这样一个问题,即先做出选择,再解决剩下的一个子问题。
- 对每一子问题一一求解,得到子问题的局部最优解;
- 将子问题的解局部最优解合成原问题的一个解。
伪代码:
关于 Question Q:
java
while(Q.hasNextStep)
{
Select(Q.nowBestSelect);
Q.NextStep
}
Select(Q.nowBestSelect);
活动安排问题
1、活动安排问题(区间调度问题)
有很多电视节目,给出它们的起止时间。有些节目时间冲突。问能完整看完的电视节目最多有多少?
在一个时间段内,安排多个起止活动,使得能看完的电视节目最多
解题的关键在于选择什么贪心策略,才能安排尽量多的活动。由于活动有开始和结束时间,考虑三种贪心策略
-
最早开始时间L
-
最早结束时间R
-
用时最少T
把几种贪心策略都写出来,带几个例子进去算,看能不能符合策略
通过暴力的方法去试
-
最早开始时间,错误,因为如果一个活动迟迟不终止,后面的活动就无法开始
-
最早结束时间,合理,一个尽快终止的活动,可以容纳更多的后续活动
-
用时最少,错误
2、区间覆盖问题
给定一个长度为n的区间,再给出m条线段的左端点(起点)和右端点(终点)。问最少用多少条线段可以将整个区间完全覆盖。
贪心策略:尽量找出更长的线段。
解题步骤是:
- 把每个线段按照左端点递增排序。
- 设已经覆盖的区间是
[L,R]
,在剩下的线段中,找所有左端点小于等于R,且右端点最大的线段,把这个线段加入到已覆盖区间里,并更新已覆盖区间的[L,R]
值。 - 重复步骤(2),直到区间全部覆盖。
3、最优装载问题
有n种药水,体积都是V,浓度不同。把它们混合起来得到浓度不大于w%的药水。问怎么混合,才能得到最大体积的药水?注意一种药水要么全用,要么都不用,不能只取一部分。
贪心策略:要求配置浓度不大于w%的药水
贪心思路:尽量找浓度小的药水。
- 先对药水按浓度从小到大排序
- 药水的浓度不大于w%就加入,如果药水的浓度大于w%,计算混合后总浓度,不大于w%就加入,否则结束判断。
4、最优装载问题2
有n种药水,体积不同浓度不同。把它们混合起来,得到浓度不大于w%的药水。问怎么混合,才能得到最大体积的药水?注意一种药水可以只取一部分。
贪心策略:要求配置浓度不大于w%的药水,
贪心思路:尽量找浓度小的药水
- 先对药水按浓度从小到大排序
- 药水的浓度不大于w%就加入,如果药水的浓度大于w%,计算混合后总浓度,不大于w%就加入,加不完可以加一部分,否则结束判断。
5、多机调度问题
有n个独立的作业,由m台相同的机器进行加工
作业i的处理时间为ti,每个作业可在任何一台机器上加工处理,但不能间断、拆分。
要求给出一种作业调度方案,在尽可能短的时间内由m台机器加工处理完成这n个作业。
贪心策略:最长处理时间的作业优先,即把处理时间最长的作业分配给最先空闲的机器。让处理时间长的作业得到优先处理,从而在整体上获得尽可能短的处理时间。
翻硬币
【题目描述】
小明玩"翻硬币"游戏。桌上放着排成一排的若干硬币。用*
表示正面,用o表示反面(是小写字母,不是零)。比如,可能情形是**oo***oooo
,如果同时翻转左边的两个硬币,则变为oooo***oooo
。小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
约定:把翻动相邻的两个硬币叫做一步操作。
【输入格式】两行等长的字符串,分别表示初始状态和要达到的目标状态。每行的长度<1000。
【输出格式】一个整数,表示最小操作步数,
【输出样例】
**********
o****o****
【输出样例】
5
本题求从初始状态到目标状态的最短路径,非常符合BFS的特征
但是本题的状态太多,用BFS肯定超时
如果让小学生做这个游戏,他会简单地模拟翻动的过程:从左边开始,:每遇到和目标状态不同的硬币就操作一步,翻动连续两个硬币,直到最后一个硬币。这是贪心法,但是这题用贪心对吗?下面进行分析和证明。
首先分析翻动的具体操作
- 只有一个硬币不同。例如位置a的硬币不同,那么翻动它时,会改变它相邻的硬币b,现在变成了硬币b不一样,回到了"只有一个硬币不同"的情况。也就是说,如果只有一个硬币不同,无法实现。
- 有两个硬币不同。这两个硬币位于任意两个位置,从左边的不同硬币开始翻动,一直翻动到右边的不同硬币,结束。
总结这些操作,得到结论:
- 有解的条件。初始字符串s和目标字符串t必定有偶数个字符不同,
- 贪心操作。从头开始遍历字符串,遇到不同的字符就翻动,直到最后一个字符。
证明这个贪心操作,是局部最优也是全局最优。
首先找到第一个不同的字符。从左边开始找第一个不同的那个字符(记为Z),Z左边的字符都相同,不用再翻动。从z开始,右边肯定有偶数个不同的字符。Z必定要翻动,不能不翻;它翻了之后,就不用再翻动。所以从左到右的翻动过程,每次翻动都是必须的,也就是说这个翻动z的局部最优操作,也是全后最优操作。贪心是正确的。
快乐司机
【题目描述】
话说现在当司机光有红心不行,还要多拉快跑。多拉不是超载,是要让所载货物价值最大特别是在当前油价日新月异的时候。司机所拉货物为散货,如大米、面粉、沙石、泥土...现在知道了汽车核载重量为w,可供选择的物品的数量n。每个物品的重量为gi,价值为pi。
求汽车可装载的最大价值。(n<10000,w<10000,0<gi<100,0≤pi≤100)。
【输入描述】输入第一行为由空格分开的两个整数n,w。
第二行到第n+1行,每行有两个整数,由空格分开,分别表示gi和pi。
【输出描述】最大价值(保留一位小数)。
按照pi除以gi,从大到小排序
#include <bits/stdc++.h>
using namespace std;
double sum;
struct object{
double wg;
double va;
double scale;
}ct[10000];
bool guize(object a, object b)
{
return a.scale > b.scale;
}
int main()
{
int n, w;
cin >> n >> w;
for (int i = 0; i < n; i ++)
{
cin >> ct[i].wg >> ct[i].va;
ct[i].scale = ct[i].va / ct[i].wg;
}
sort (ct, ct + n, guize);
for (int i = 0; i < n; i ++)
{
if (w >= ct[i].wg)
{
sum += ct[i].va;
w -= ct[i].wg;
}
else
{
sum += w*ct[i].scale;
break;
}
printf ("%.1lf", sum);
return 0;
}
}
防御力
【题目描述】
小明最近在玩一款游戏。对游戏中的防御力很感兴趣。直接影响防御的参数为防御性能,记作d,而面板上有两个防御值A和B,与d成对数关系,A=2^d,B=3^d(注意任何时候上式都成立)。
在游戏过程中,可能有一些道具把防御值A增加一个值,有另一些道具把防御值B增加一个值。现在小明身上有 n 1 n_{1} n1个道具增加A的值和 n 2 n_{2} n2个道具增加B的值,增加量已知。
现在已知第i次使用的道具是增加A还是增加B的值,但具体使用那个道具是不确定的,请找到一个字典序最小的使用道具的方式,使得最终的防御性能最大。初始时防御性能为0,即d=0,所以A=B=1。
【输入格式】
输入的第一行包含两个数 n 1 n_{1} n1, n 2 n_{2} n2,空格分隔。
第二行 n 1 n_{1} n1个数,表示增加A值的那些道具的增加量。
第三行 n 2 n_{2} n2个数,表示增加B值的那些道具的增加量。
第四行一个长度为 n 1 + n 2 n_{1}+n_{2} n1+n2的字符串,由0和1组成,表示道具的使用顺序。0表示使用增加A值的道具,1表示使用增加B值的道具。输入数据保证恰好有 n 1 n_{1} n1个0, n 2 n_{2} n2个1。
【输出格式】
对于每组数据,输出 n 1 + n 2 + 1 n_{1}+n_{2}+1 n1+n2+1行,前 n 1 + n 2 n_{1}+n_{2} n1+n2行按顺序输出道具的使用情况,若使用增加A值的道具,输出Ax,x为道具在该类道具中的编号(从1开始)。若使用增加B值的道具则输出Bx。最后一行输出一个大写字母E。
【测试】对于20%的数据,字符串长度<=10000;对于70%的数据,字符串长度<=200000;对于100%的数据,字符串长度<=2000000,输入的每个增加值不超过 2 30 2^{30} 230
【输入样例】
1 2
4
2 8
101
【输出样例】
B2
A1
B1
E
操作 | |||
---|---|---|---|
初始 | A=1,B=1,d=0 | ||
B2 | B=1+8=9 | 根据 B = 3 d B=3^d B=3d,算出 d=2 | 根据 d=2 和 A = 2 d A=2^d A=2d,算出 A=4 |
A1 | A=4+4=8 | 根据 A = 2 d A=2^d A=2d,算出 d=3 | 根据 d=3 和 B = 3 d B=3^d B=3d,算出 B=27 |
B1 | B=27+2=29 | 根据 B = 3 d B=3^d B=3d,算出 d = log 3 29 d=\log_{3}29 d=log329 | 根据 d = log 3 29 d=\log_{3}29 d=log329 和 A = 2 d A=2^d A=2d,算出 A = 2 log 3 29 A=2^{\log_{3}29} A=2log329 |
A、B、d都在增加 | |||
只增加 A或B。例如连续增加 A,得 d = log 2 ( 1 + A 1 + A 2 + ... ) d=\log_{2}(1+A_{1}+A_{2}+\dots) d=log2(1+A1+A2+...)与A的道具的顺序没有关系。 | |||
交替增加 A 和 B 。如何决定 A 和 B 的道具的顺序? | |||
A=2d B=3d | |||
A使B增加快,越大的A对B越有利:A的道具从小到大,后面更大的A更有利于B | |||
B使A增加慢:B的道具从大到小,前面更大的B对A影响小 | |||
简单证明: |
A = 2 d B = 3 d A=2^{d\qquad}B=3^d A=2dB=3d
log ( a + b ) = log ( a ∗ 1 + b a ) = log a + log ( 1 + b a ) \begin{array}{} \log(a+b)=\log\left( a*\frac{{1+b}}{a} \right) \\ =\log a+\log\left( 1+\frac{b}{a} \right) \end{array} log(a+b)=log(a∗a1+b)=loga+log(1+ab)
增大A,A=A+X
{} D = log 2 ( A + x ) = log 2 A + log 2 ( 1 + x A ) 转嫁给 B , B = 3 log 2 A + log 2 ( 1 + x A ) = 3 log 2 A ∗ 3 log 2 ( 1 + x / A ) \begin{array}{} D=\log_{2}(A+x)=\log_{2}A+\log_{2}\left( 1+\frac{x}{A} \right) \\ 转嫁给B,B=3^{\log_{2}A+\log_{2}\left( 1+\frac{x}{A} \right)} \\ =3^{\log_{2}A}*3^{\log_{2}(1+x/A)} \end{array} D=log2(A+x)=log2A+log2(1+Ax)转嫁给B,B=3log2A+log2(1+Ax)=3log2A∗3log2(1+x/A)
3 1 o g 2 ( 1 + x / A ) 3^{1og_{2}(1+x/A)} 31og2(1+x/A)什么时候才能大, x A \frac{x}{A} Ax大的时候 ,x是当前的加数是我们的选择,A是历史的和,那就说明我们要在选x的时刻,让之前选择的和最小,这样X/A才能最大。
若给B增加Y
B= B+Y
D = log 3 ( B + Y ) = log 3 B + log 3 ( 1 + Y B ) 转嫁给 A , A = 2 D = 2 log 3 B + log 3 ( 1 + Y B ) = 2 log 3 B ∗ 2 log 3 ( 1 + Y B ) \begin{array}{} D=\log_{3{}}(B+Y)=\log_{3}B+\log_{3}\left( 1+\frac{Y}{B} \right) \\ 转嫁给A,A=2^D=2^{\log_{3}B+\log_{3}\left( 1+\frac{Y}{B} \right)} \\ =2^{\log_{3}B}*2^{\log_{3}\left( 1+\frac{Y}{B} \right)} \end{array} D=log3(B+Y)=log3B+log3(1+BY)转嫁给A,A=2D=2log3B+log3(1+BY)=2log3B∗2log3(1+BY)
我们会发现同样的是从小到大B的排序,D会增加,但是B增大时候会无形增大A,我们又要求两个都从小到大。
所以冲突了,我们只能牺牲一个,所以要比较那个增加对整体的影响大。 log 2 \log_{2} log2的影响比 log 3 \log_{3} log3大,所以我们要维护A,所以按照A从小到大,B从大到小。
这里用数学公式证明的话相当的复杂,我个人认为较快的方法是用具体的数带进去进行估算。然后
我们得出当A排序由小到大,B排序由大到小这样得出的d是最大的。
对Ai进行结构体排序,先对Ai按增加量的从小到大排序,再按下标(字典序)排序。
对Bi进行结构体排序,先对Bi按增加量的从大到小排序,再按下标(字典序)排序。
然后按题目要求的顺序,输出Ai和Bi,
#include <bits/stdc++.h>
using namespace std;
struct nodea{int id, w;} a[100005]; //id是道具,w是道具的增加量
struct nodeb{int id, w;} b[100005];
bool cmp1(nodea a, nodea b)
{
if (a.w != b.w) //先对A的增加量排序,从小到大
return a.w < b.w; //再按字典序id排序
else
return a.id < b.id; //先对B的增加量排序,从大到小
}
bool cmp2(nodeb a, nodeb b)
{
if (a.w != b.w)
return a.w > b.w;
else
return a.id < b.id;
}
int main()
{
int n1, n2;
cin >> n1 >> n2;
for (int i = 1; i <= n1; i ++) cin >> a[i].w, a[i].id = i;
for (int i = 1; i <= n2; i ++) cin >> b[i].w, b[i].id = i;
sort(a+1, a+n1+1, cmp1);
sort(b+1, b+n2+1, cmp2);
string s;
cin >> s;
int idx1 = 1, idx2 = 1;
for (int i = 0; i < s.length(); i ++)
{
if (s[i] == '1')
{
cout << "B";
cout << b[idx1++].id << "\n";
}
else
{
cout << "A";
cout << a[idx2++].id << "\n";
}
}
cout << "E" << "\n";
return 0;
}
做题的时候不需要特别证明这一套过程,可以各种贪心策略都试一下
答疑
【题目描述】
有n位同学同时找老师答疑。每位同学都预先估计了自己答疑的时间。老师可以安排答疑的顺序,同学们要依次进入老师办公室答疑。一位同学答疑的过程如下:
首先进入办公室,编号为i的同学需要 s i s_{i} si毫秒的时间。
然后同学问问题老师解答,编号为i的同学需要a;毫秒的时间。答疑完成后,同学很高兴,会在课程群里面发一条消息,需要的时间可以忽略
最后同学收拾东西离开办公室,需要 e i e_{i} ei毫秒的时间。一般需要10 秒、20 秒或 30 秒,即 e i e_{i} ei取值为 10000,20000或30000。
一位同学离开办公室后,紧接着下一位同学就可以进入办公室了。
答疑从0时刻开始。老师想合理的安排答疑的顺序,使得同学们在课程群里面发消息的时刻之和最小。
【输入描述】
输入第一行包含一个整数n,表示同学的数量。接下来n行,描述每位同学的时间。其中第i行包含三个整数 s i , a i , e i s_{i},a_{i},e_{i} si,ai,ei,意义如上所述。
其中有,1≤n≤1000,1≤ s i s_{i} si≤60000,1≤ a i a_{i} ai≤ 1 0 6 10^6 106, e i e_i ei=10000/20000/30000,即 e i e_{i} ei一定是10000、20000、30000 之一。
【输出描述】输出一个整数,表示同学们在课程群里面发消息的时刻之和最小是多少
【输入样例】
3
10000 10000 10000
20000 50000 20000
30000 20000 30000
【输出样例】
280000
设第i位同学进门到出门花费时间为 T i = s i + a i + e i T_{i}=s_{i}+a_{i}+e_{i} Ti=si+ai+ei
那么第i位同学的发消息时刻位 M i M_{i} Mi
M i = ( T 1 + . . T i − 1 ) + s i + a i M_{i}=(T_{1}+..T_{i-1})+s_{i}+a_{i} Mi=(T1+..Ti−1)+si+ai
本题目求 S u m ( M i ) Sum(M_{i}) Sum(Mi)最小
S u m ( M i ) = M 1 + . . + M n Sum(M_{i})=M_{1}+..+M_{n} Sum(Mi)=M1+..+Mn
S u m ( M i ) = ( s 1 + a 1 ) + ( T 1 + s 2 + a 2 ) + ( T 1 + T 2 + s 3 + a 3 ) + . . . + ( T 1 + T 2 + . . . + T n − 1 + s n + a n ) \begin{array}{} Sum(M_{i}){}=(s_{1}+a_{1})+(T_{1}+s_{2}+a_{2})+(T_{1}+T_{2}+s_{3}+a_{3})+... \\ +(T_{1}+T_{2}+...+T_{n-1}+s_{n}+a_{n}) \end{array} Sum(Mi)=(s1+a1)+(T1+s2+a2)+(T1+T2+s3+a3)+...+(T1+T2+...+Tn−1+sn+an)
S u m ( M i ) = ( n − 1 ) ∗ T 1 + ( n − 2 ) ∗ T 2 + . . . + T n − 1 + ( s 1 + a 1 + s 2 + a 2 + s 3 + a 3 + . . + s n + a n ) \begin{array}{} Sum(M_{i})=(n-1)*T_{1}+(n-2)*T_{2}+... \\ +T_{n-1}+(s_{1}+a_{1}+s_{2}+a_{2}+s_{3}+a_{3}+..+s_{n}+a_{n}) \end{array} Sum(Mi)=(n−1)∗T1+(n−2)∗T2+...+Tn−1+(s1+a1+s2+a2+s3+a3+..+sn+an)
由于无论什么顺序,这个 ( s 1 + a 1 + s 2 + a 2 + s 3 + a 3 + . . + s n + a n ) (s_{1}+a_{1}+s_{2}+a_{2}+s_{3}+a_{3}+..+s_{n}+a_{n}) (s1+a1+s2+a2+s3+a3+..+sn+an)值不会变
那么 S u m ( M i ) Sum(M_{i}) Sum(Mi)最小就是使得 ( n − 1 ) ∗ T 1 + ( n − 2 ) T 2 + . . + T n − 1 (n-1)*T_{1}+(n-2)T_{2}+..+T_{n-1} (n−1)∗T1+(n−2)T2+..+Tn−1最小
显然易得在前面出现的 T i T_{i} Ti的系数更大,应该使得Ti从小到大排序
则贪心策略即得。
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n;
struct Stu
{
int inD;
//进门所需时间
int answQ;
//答疑所需时间
int outD;
//收拾东西所需时间
int sum1;
//贪心准则1=进门时间+答疑时间+收拾东西时间
int sum2;
} stu[N];
//贪心准则
bool cmp (Stu st1, Stu st2)
{
return st1.sum1 < st2.sum1;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
{
scanf("%d%d%d", &Stu[i].inD, &Stu[i].answQ, &Stu[i].outD);
//标准生成
stu[i].sum1 = stu[i].inD + stu[i].answQ + stu[i].outD;
stu[i].sum2 = stu[i].inD + stu[i].answQ;
}
//贪心过程及结果计算
sort (stu, stu + n, cmp);
long long res = 0, t = 0;
for (int i = 1; i <= n; i ++)
{
t += stu[i].sum2;
res += t;
t += stu[i].outD;
}
cout << res << endl;
return 0;
}