贪心算法的特征规律
贪心算法,“贪心”二字顾名思义,因此其规律特征就是更加注重当前的状态,贪心法做出的选择是对于当前所处状态的最优选择,它的解决问题的视角是微观的“局部”,而不是从全局宏观的角度思考和看待问题,根据这样的性质,要求贪心法解决的问题有“无后效性”——当前的决策不会影响到后续的决策,因为如果问题前后勾连紧密的话,会造成求解过程十分混乱。贪心算法常常用于组合优化问题,它的求解过程是多步判断的过程。
如果一个待求解的问题具有以上的特征,很有可能可以使用贪心算法解决。
算法设计策略
贪心算法设计的核心是——“贪心选择的标准”,结合《算法设计与分析》书中的“活动安排问题”,该问题有“最早开始时间”“持续时间最短”“结束时间最早”三种贪心选择的标准。
1.如果按照“最早开始时间”
![b391b0ce89f4c4e8a26a8228f1c05d03.png](https://i-blog.csdnimg.cn/blog_migrate/80261a05e8661ae2d7365ad0cd5a9318.jpeg)
选择了活动1,就无法选择活动2和活动3
2.如果按照“持续时间最短”
![b0733d87db6eb64fb260d3959c35eaa2.png](https://i-blog.csdnimg.cn/blog_migrate/6e39f7befe5c04501c972b5fa44b5459.jpeg)
选择了活动2.就无法选择活动1和活动3
3.综合上述分析该问题应该选择“结束时间最早”的贪心选择标准
通过上面的例子可知,对于贪心算法,核心是通过分析找到合适的“贪心选择标准”。
此外,还需要对贪心算法的合理性进行证明,通常采用反证法证明,如果想要证明某一贪心策略不正确,可以寻找反例(如“活动安排问题”的开始时间最早、持续时间最长这两种贪心策略都是通过举反例的方法排除的)。
当选定了“贪心选择的标准”之后,要按照这个对已知的数据信息进行预处理,通常的预处理是“排序”。本题中就要按照结束时间从小到大的顺序进行排序。
将数据进行预处理(排序)之后,一次按顺序遍历,并根据条件进行选取,构建两个集合,其中一个集合用于装符合条件的元素,另一个集合用于装未进行判断的元素,这是一个分步完成的过程。(有这个特征的典型应用就是在《数据结构》课程中曾经学习的求解最小生成树使用的prim算法、kruskal算法,两者都是分步的将符合条件的边收纳进入一个集合,再从另一个集合中挑选出符合“贪心标准”的边放入最小生成树集合)。
下面用“活动安排问题”来描述贪心算法的一般过程:
输入:活动集合S, si(每个活动开始时间),fi(每个活动的结束时间)
其中i=1,2,3……,n, f1<f2<f3<……<fn;
输出:选中活动的结合A(A是S的子集)
算法主要框架:
- nlength[S];
- A{1}
- J1
- For i2 to n do
- If(si>fi)
- Then AA{ i }//符合条件的纳入集合
- J
- Return A
贪心算法的局限性
贪心算法不能保证最后求得的解是最优的,同时,贪心算法只能求满足某些约束条件的可行解的范围。
下面通过TSP问题来说明贪心算法的局限性
题目描述:一个旅行家要旅行经过n个城市,要求各个城市经历且仅经历一次然后回到出发的城市,并要求所走的路线长度最短。
给定一个矩阵,表示一个图的邻接矩阵,假设从顶点1出发,按照贪心策略,每次决策时都找到相邻的权值最小的边,此时得到的路径是1-4-3-5-2-1,总代价是14,然而最优解1-2-5-4-3-1的总代价是13,总代价14虽然不是最优解,但是近似接近最优解13,但是贪心法有他的不足——无法确定得到的近似最优解是以何种程度近似接近最优解。
下面是用贪心法求TSP问题的代码:
int TSP(int arc[n][n], int w)//从顶点w出发
{
int edgeCount = 0, TSPlength = 0;
int min, u, v;
int flag[n] = { 0 };//此时所有的顶点都没有被收入到哈密顿回路中
u = w, flag[w] = 1;//flag用于标记是否已经经过该点
while (edgeCount < n - 1)
{
min = 100;
for (int j = 0; j < n; j++)
{
if (flag[j] == 0 && arc[u][j] != 0 && (arc[u][j] < min))
{ //遍历,如果周围有邻接的、未被访问过的、且符合贪心策略的
v = j; //用临时变量记录该点
min = arc[u][j];//更新最小值
}
}
TSPlength += arc[u][v];
flag[v] = 1;//将该点收入到集合
edgeCount++;
u = v;
}
return (TSPlength + arc[u][w]);
}
贪心算法的优点
如果一个问题的最优解只能用蛮力法穷举得到,则贪心法不失为寻找问题近似最优解的一种较好的方法。
典型题目
1.跳跃问题
题目描述:给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
贪心算法解题分析:
这道算法题作为引入,主要体现了贪心算法的最直接的特点——“贪婪”,每一步都选择都选择从当前所在位置出发可以到达的最远距离,如给出一组数据list=[2,3,1,1,4],对于位置0,list[0]=2,即使可以选择跳0,1,2步,但是我们“贪心的”只关注最远的mostlong的值,遍历整个数组,循环用mostlong=max(mostlong,i+nums[i]);更新mostlong的值,直到找到可以达到的最右端的位置。
代码实现:
class Solution
{
public:
bool canJump(vector<int>& nums)
{
int mostlong=0;
for(int i=0;i<nums.size()-1;i++)
{
mostlong=max(mostlong,i+nums[i]);
//贪心选择右端可达最大值
if(mostlong<i+1)
return false;
}
return true;
}
};
时间、空间复杂度分析:
时间复杂度:O(n),其中 n为数组的大小。整个程序把 nums 数组遍历了一遍。
空间复杂度:O(1),程序运行过程中没有额外的空间开销。
2.Prim算法
与prim算法相关的——公路村村通问题:
题目描述:现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
贪心算法解题分析:
Prim算法是非常典型的贪心算法应用,几乎体现了贪心法的全部特点,prim算法的贪心策略是每次以选取距离已经生成的部分权值最小的边作为“贪心选择的标准”,bool visited[ ]作为标记数组,当图中的一个顶点 i被选入集合时就将其标记为true,即visited[i]=true,表示顶点 i被收入符合“贪心标准”的集合,标记为false的顶点表示不满足“贪心选择标准”或者仍处于候选集合,这种“标记”的方式将集合的逻辑结构转换为计算机的存储结构,可以让我们用计算机实现程序。Prim算法相关的操作为解决贪心算法其他问题提供了一般性的解题方法和相应的数据结构。
代码实现:
int Prim()
{
bool visited[Max] = { false };
visited[1] = true;
int cost = 0;
int min;
int nVex1, nVex2;
int minTreeEdgeNum = mGraph.VexNum - 1;
for (int k = 0; k < minTreeEdgeNum; k++)
{
min = INT16_MAX;
for (int i = 1; i <= mGraph.VexNum; i++)
{
if (visited[i]) //判断是否被访问过
{
for (int j = 1; j <= mGraph.VexNum; j++)
{
if (!visited[j] && mGraph.Matrix[i][j] != 0 && mGraph.Matrix[i][j] < min)
//没有被访问过的、邻接的且符合贪心策略的
{
nVex1 = i;
nVex2 = j;
min = mGraph.Matrix[i][j];//更新最小值
}
}
}
}
cost = cost + min;
visited[nVex1] = true;
visited[nVex2] = true;
}
return cost;
}
3.排队问题
题目描述:
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:
总人数少于1100人。
示例
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
贪心算法解题分析:
这道题主要体现了贪心算法中的“预处理”的思想和作用,在我们选定了“贪心选择标准”之后,要对原有的给定的数据进行“预处理”(通常是对数据按照一定的规则进行排序),比如在“活动安排问题”中,按照每个活动的结束时间的先后顺序进行排序,再进行后续的操作。本题中应该先将个子更高的人先进行安排(贪心策略),再安排个子低的人,这样可以保证后插入的人不会影响到先插入人的相对次序,即先按身高的降序排列,对于相同的身高按照前面人数k进行升序排列。这个过程都体现了贪心的思想以及数据预处理的一般步骤。
代码实现:
class Solution {
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
// 排序
sort(people.begin(), people.end(),
[](const vector<int>& lhs, const vector<int>& rhs)
{return lhs[0] == rhs[0] ? lhs[1] <= rhs[1] : lhs[0] > rhs[0];});
int len = people.size();
list<vector<int>> tmp;
// 循环插入
for(int i = 0; i < len; ++i){
auto pos = tmp.begin();
advance(pos, people[i][1]);
tmp.insert(pos, people[i]);
}
// 重建vector返回
return vector<vector<int>>(tmp.begin(), tmp.end());
}
};
4.员工分配问题
题目描述
公司计划面试 2N 人。第 i 人飞往 A 市的费用为 costs[i][0],飞往 B 市的费用为 costs[i][1]。返回将每个人都飞到某座城市的最低费用,要求每个城市都有 N 人抵达。
示例:
输入:[[10,20],[30,200],[400,50],[30,20]]
输出:110
解释:
第一个人去 A 市,费用为 10。
第二个人去 A 市,费用为 30。
第三个人去 B 市,费用为 50。
第四个人去 B 市,费用为 20。
最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。
贪心算法解题分析:
假设全部的2N个员工都被派去了A市,一共需要费用是sum,如果中途调用N个人去了B市,此时这被调到B市的员工中节省了去A市的费用,增加了去B市的费用,此时的费用可以用公式表示,要使result值最小,只需要尽可能大,体现了贪心选择标准——每次都减掉去A市和去B市花费相差最大的值,选定了贪心选择标准之后,在进行数据的预处理(排序)——对(cost[i][o]-cost[i][1])进行从大到小的排列,再进行后续操作,本题每次选择(cost[i][o]-cost[i][1])最大者体现了贪心策略,也体现了预处理在解题过程中的重要作用。
代码实现:
class Solution {
public:
int twoCitySchedCost(vector<vector<int>>& costs) {
int sum=0;
int*temp=new int[2*N];
for(int i=0;i<2*N;i++)
{
sum=sum+costs[i][0];
temp[i]=costs[i][0]-costs[i][1];
}
sort(temp); //对temp数组进行从大到小的排序
for(int i=0;i<N;i++)
{
sum=sum-temp[i];
}
return sum;
}
};
收获与体会
通过这次课程报告,我详细的、系统性的从贪心算法的特征规律、算法设计策略、贪心法的主要适用场景、局限性以及贪心法在“图问题”、“组合优化问题”等角度,枚举了包括
“活动安排问题”——体现了贪心选择标准选取的重要性;
“跳跃问题”“分配员工”——体现了贪心法的“贪婪特性”,找最值;
“排队问题”——体现了预处理的重要性;
“prim算法”“公路村村通”——分步的将符合条件的边收纳进入一个集合,再从另一个集合中挑选出符合“贪心标准”的放入该集合的思想;
“TSP问题”——无法确定得到的近似最优解是以何种程度近似接近最优解(体现贪心算法局限性)。
这些具体题目,突出贪心算法中“贪心思想”“贪心标准选择”“预处理(排序)”“分集合分步判断”等重要方法的应用,从而完整全面的理解了——什么情况下应该采用贪心算法以及如何使用贪心算法。
七. 参考文献
1.《算法设计与分析》王红梅,胡明
2. 《数据结构(第二版)》高等教育出版社,主编陈越
3.贪心算法——百度百科
https://baike.baidu.com/item/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95/5411800?fr=aladdin
4.LeetCode官网
https://leetcode-cn.com/problems/queue-reconstruction-by-height/
- 程序设计类实验辅助教学平台
https://pintia.cn/problem-sets/15/problems/718