学习要点
理解贪心算法的概念。
掌握贪心算法的基本要素:
最优子结构性质、贪心选择性质。
理解贪心算法与动态规划算法的差异。
通过应用范例学习动态规划算法设计策略:
活动安排问题;背包问题;哈夫曼编码问题;单源最短路劲问题;最小生成树问题。
出题标准
12分8分给证明,是否可以利用贪心算法求解,证明其具有贪心选择性质和最优子结构性质,也会要求写伪码
引子
假设有四种硬币,它们的面值分别为五角、一角、五分和一分。现找顾客六角三分钱,请给出硬币个数最少的找钱方案。
找硬币问题本身具有最优子结构性质,它可以用动态规划算法来解。但显然贪心算法更简单,更直接且解题效率更高。该算法利用了问题本身的一些特性,例如硬币面值的特性,由面值从大到小给。
再例:如果有一分、五分和一角三种不同的硬币,要找给顾客一角五分钱。
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路径问题,最小生成树问题等。
在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
基本思路
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
存在的问题
不能保证求得的最后解是最佳的;
不能用来求最大或最小解问题;
只能求满足某些约束条件的可行解的范围。
对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能够导致问题的最优解。
通常可以首先证明问题的一个整体最优解是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步作贪心选择,最终可得到问题的一个整体最优解。
其中,证明贪心选择后的问题简化为规模更小的类似子问题的关键在于利用该问题的最优子结构性质。
贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,每步所作的选择依赖于相关子问题的解。贪心算法仅在当前状态下作出局部最优选择,再去解作出这个选择后产生的相应的子问题。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征
活动安排问题:要求高效地安排一系列争用某一公共资源的活动。
活动序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
起始时间 | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
结束时间 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。(临界资源)
每个活动i都有一个要求使用该资源的起始时间 s i s_i si和一个结束时间 f i f_i fi,且 s i < f i s_i
如果选择了活动i,则它在半开时间区间 [ s i , f i ) [s_i,f_i) [si,fi)内占用资源。若区间与 [ s i , f i ) [s_i,f_i) [si,fi)区间 [ s j , f j ) [s_j,f_j) [sj,fj)不相交,则称活动i与活动j是相容的。即,当 s i ≥ f j s_i\ge f_j si≥fj或 s j ≥ f i s_j\ge f_i sj≥fi时,活动i与活动j相容,
问题就是选择一个由互相兼容的活动组成的最大集合
template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
//各活动的起始时间和结束时间存储在数组s和f中
//且按结束时间的非递减排序:f1≤f2≤…≤fn排列。
A[1]=true; //用集合A存储所选择的活动
int j=1;
for(int i=2; i<=n; i++) {
//将与j相容的具有最早完成时间的相容活动加入集合A
if(s[i]>=f[j]) {
A[i]=true; j=i; }
else A[i]=false; }
}
设集合a包含已被选择的活动, 初始时为空。所有待选择的活动按结束时间的非递减顺序排列: f 1 ≤ f 2 ≤ . . . f n f_1 \le f_2 \le...f_n f1≤f2≤...fn
变量j指出最近加入a的活动序号。由于按结束时间非递减顺序来考虑各项活动的,所以 f j f_j fj总是a中所有活动的最大结束时间
由于输入活动是以完成时间的非递减排列,所选择的下一个活动总是可被合法调度的活动中具有最早结束时间的那个,所以算法是一个**“贪心的”选择**,即使得使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。
算法GreedySelector的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间就可安排n个活动,使最多的活动能相容地使用公共资源。
如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。
设集合E={1,2,…,n}为所给的活动集合。由于E中活动按结束时间的非减序排列,故活动1有最早完成时间。
证明I:活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1。
证明II:对集合E中所有与活动1相容的活动进行活动安排求得最优解的子问题。
即需证明:若A是原问题的最优解,则A’=A-{1}是活动安排问题E’={ i∈E:si≥f1}的最优解。
如果能找到E’的一个最优解B’,它包含比A’更多的活动,则将活动1加入到B’中将产生E的一个解B,它包含比A更多的活动。这与A的最优性矛盾。
结论:每一步所做的贪心选择问题都将问题简化为一个更小的与原问题具有相同形式的子问题
与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包。
此问题的形式化描述为,给定 c > 0 , w i > 0 , v i > 0 , 1 ≤ i ≤ n c>0,w_i>0,v_i>0,1≤i≤n c>0,wi>0,vi>0,1≤i≤n,要求找出一个n元0-1向量$ (x_1,x_2, … ,x_n)$,其中 0 ≤ x i ≤ 1 , 1 ≤ i ≤ n 0≤x_i≤ 1, 1≤i≤n 0≤xi≤1,1≤i≤n ,使得对 w i x i w_ix_i wixi求和小于等于c ,并且对 v i x i v_ix_i vixi求和达到最大。
对于0-1背包问题,贪心选择之所以不能得到最优解是因为,在这种情况下,无法保证最终能把背包装满,部分闲置的背包空间会使每千克背包空间的价值降低
有3种物品,背包的容量为50千克。物品1重10千克,价值60元;物品2重20千克,价值100元;物品3重30千克,价值120元。用贪心算法求背包问题。
首先计算每种物品单位重量的价值 v i / w i v_i/w_i vi/wi;
然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。
若将这种物品全部装入背包后,背包内的物品总重量未超过c,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去,直到背包装满为止。
贪心策略:物品1,6元/千克;物品2,5元/千克;物品3,4元/千克。
该算法前提:所有物品在集合中按其单位重量的价值从小到大排列。
void Knapsack(int n, float M, float v[], float w[], float x[]) {
sort(n, v, w);//按照单位价值从小到大排列
int i;
for (i = 1; i <= n; i++) x[i] = 0;
float c = M;
for (i = 1; i <= n; i++) {
if (w[i] > c) break;
x[i] = 1;
c -= w[i];
}
if (i <= n) x[i] = c / w[i];
}
算法Knapspack的主要计算时间在于将各种物品按其单位重量的价值从小到大排序,算法的时间复杂度O(nlogn) 。
7分简答题
哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%~90%之间。
哈夫曼编码算法是用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。
编码目标:给出现频率高的字符较短的编码,出现频率较低的字符以较长的编码,可以大大缩短总码长。
定义:对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。
编码的前缀性质可以使译码方法非常简单。由于任一字符的代码都不是其他字符代码的前缀,从编码文件中不断取出代表某一字符的前缀码,转换为原字符,即可逐个译出文件中的所有字符。
a | b | c | d | e | f | |
---|---|---|---|---|---|---|
频率 | 45 | 13 | 12 | 16 | 9 | 5 |
定长码 | 000 | 001 | 010 | 011 | 100 | 101 |
变长码 | 0 | 101 | 100 | 111 | 1101 | 1100 |
给定序列:001011101,可以唯一的分解为0,0,101,1101,编译为aabe
译码过程需要方便地取出编码的前缀,因此需要一个表示前缀码的合适的数据结构。
用二叉树作为前缀编码的数据结构。在表示前缀码的二叉树中,树叶代表给定的字符,并将每个字符的前缀码看作是从树根到代表该字符的树叶的一条道路。代码中每一位的0或1分别作为指示某结点到其左儿子或右儿子的“路标”。
哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。
编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用以在作贪心选择时有效地确定算法当前要合并的两棵具有最小频率的树。一旦两棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的两棵树的频率之和,并将新树插入优先队列Q中,再进行新的合并。
•由于字符集中有6个字符,优先队列的大小初始为6,总共用5次合并得到最终的编码树T。
• 每次合并使Q的大小减1,最终得到的树就是最优前缀编码:哈夫曼编码树,每个字符的编码由树T的根到该字符的路径上各边的标号所组成。
1️⃣ 算法首先用字符集C中每一个字符c的频率f(c)初始化优先队列Q。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。
2️⃣ 然后不断地从优先队列Q中取出具有最小频率的两棵树x和y,将它们合并为一棵新树z。z的频率是x和y的频率之和。
3️⃣ 新树z以x为其左儿子,y为其右儿子(也可以y为其左儿子,x为其右儿子。不同的次序将产生不同的编码方案,但平均码长是相同的)。经过n-1次的合并后,优先队列中只剩下一棵树,即所要求的树T。
给定带权有向图G=(V,E),其中每条边的权是非负实数。
给定V中的一个顶点,称为源。
现在要计算从源到其他所有各顶点的最短路径长度。这里的路径长度是指路径上各边权之和,这个问题通常称为单源最短路径问题。
考点:画一个迭代矩阵
Dijkstra算法是求解单源最短路径问题的一个贪心算法。
基本思想:设置一个顶点集合S ,并不断地作贪心选择来扩充这个集合。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。
Dijkstra算法通过分步方法求出最短路径。
每一步产生一个到达新的目的顶点的最短路径。
下一步所能达到的目的顶点通过这样的贪心准则选取:在还未产生最短路径的顶点中,选择路径长度最短的目的顶点。
也就是说, Dijkstra算法按路径长度顺序产生最短路径
1️⃣ 设置一个顶点集合S。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。
2️⃣ 初始时,S中仅含有源。
3️⃣ 设u是G的某一个顶点,把从源到u且中间只有经过S中顶点的路称为从源到u的特殊路径,并且用数组dist来记录当前每个顶点所对应的最短特殊路径长度。
4️⃣ Dijkstra算法每次从V-S中取出具有最短特殊路径长度的顶点u,将u添加到 S 中,同时对数组dist作必要的修改。
5️⃣ 一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度。
已知:带权有向图
V = { v1, v2, v3, v4, v5 }
E = { < v1, v2 >, < v1, v4 >, < v1, v5 >, < v2, v3 >, < v3, v5 >, < v4, v3 >, < v4, v5 > }
设为v1源点,求其到其余顶点的最短路径。
其中,没有特殊路径的顶点用maxint表示其最短特殊路径长度
迭代 | S | u | dist[2] | dist[3] | dist[4] | dist[5] |
---|---|---|---|---|---|---|
初始 | {1} | - | 10 | maxint | 30 | 100 |
1 | {1,2} | 2 | 10 | 60 | 30 | 100 |
2 | {1,2,4} | 4 | 10 | 50 | 30 | 90 |
3 | {1,2,4,3} | 3 | 10 | 50 | 30 | 60 |
4 | {1,2,4,3,5} | 5 | 10 | 50 | 30 | 60 |
按长度顺序产生最短路径时,下一条最短路径总是由一条已产生的最短路径加上一条边形成。
设G=(V,E)是无向带权连通图,即一个网络。
E中每条边(v,w)的权为 c [ v ] [ w ] c[v][w] c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。
生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树。
网络的最小生成树在实际中有广泛应用。
例如,在设计通信网络时,用图的顶点表示城市,用边(v,w)的权 c [ v ] [ w ] c[v][w] c[v][w]表示建立城市v和城市w之间的通信线路所需的费用,则最小生成树就给出了建立通信网络的最经济的方案。
根据最优量度标准,算法的每一步从图中选择一条符合准则的边,共选择n-1条边,构成无向连通图的一棵生成树。
贪心法求解的关键:该量度标准必须足够好。它应当保证依据此准则选出n-1条边构成原图的一棵生成树,必定是最小代价生成树。
详解请参考https://blog.csdn.net/yeruby/article/details/38615045
考点:算法思想与生成顺序方法说明
Kruskal算法的贪心准则:按边代价的非减次序考察E中的边,从中选择一条代价最小的边e=(u,v)。这种做法使得算法在构造生成树的过程中,当前子图不一定是连通的。
算法思想——从点出发
void Prim(int n,Type **c){
//c[i][j]为边(i,j)的权值
TE=Ø;
U={
1};
while(U!=V){
(u,v)=u属于U且v属于V-U的最小权边;
TE=TE∪{
(u,v)};
U=U∪{
v};
}
}
Prim算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
详情请看https://www.cnblogs.com/fzl194/p/8723325.html
算法思想——从边出发
1️⃣设连通网 N = (V, E ),令最小生成树初始状态为只有 n 个顶点而无边的非连通图 T=(V, { }),每个顶点自成一个连通分量。
2️⃣在 E 中选取代价最小的边,若该边依附 的顶点落在 T 中不同的连通分量上(即: 不能形成环),则将此边加入到 T 中;否 则,舍去此边,选取下一条代价最小的边。
3️⃣ 依此类推,直至 T 中所有顶点都在同一 连通分量上为止。
Kruskal算法的时间复杂度为 O ( e l o g e ) O(eloge) O(eloge)
多机调度问题要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。约定,每个作业均可在任何一台机器上加工处理,但未完工前不允许中断处理。作业不能拆分成更小的子作业。
这个问题是NP完全问题,到目前为止还没有有效的解法。对于这一类问题,用贪心选择策略有时可以设计出较好的近似算法。
采用最长处理时间作业优先的贪心选择策略可以设计出解多机调度问题的较好的近似算法。
按此策略,当 n ≤ m n\le m n≤m时,只要将机器i的[0, ti]时间区间分配给作业i即可,算法只需要O(1)时间。
当 n > m n>m n>m 时,首先将n个作业依其所需的处理时间从大到小排序。然后依此顺序将作业分配给空闲的处理机。算法所需的计算时间为O(nlogn)。
设7个独立作业{1,2,3,4,5,6,7}由3台机器M1,M2和M3加工处理。各作业所需的处理时间分别为{2,14,4,16,6,5,3}。按算法greedy产生的作业调度如下图所示,所需的加工时间为17。