什么是贪心?
贪心顾名思义,是作出在当前看来最好的选择,例如在背包问题中,如果每件物品可以分割,我们肯定会选择性价比最高的物品装入背包,然后选择性价比次高的物品......也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,我们希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心的基本要素
1.最优子结构
过于最优子结构的更详细叙述,可通过动态规划总结文章查看。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
2.贪心选择性
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
贪心与动态规划
贪心与动态规划的相同之处,即可解决的问题都必须具有最优子结构的性质。
对于不同之处,一个直观的看法,在动态规划中,每个步骤都要进行一次选择,但是选择通常依赖于子问题的解。在贪心算法中,我们直接作出在当前问题中看来最优的选择,而不必考虑子问题的解。
动态规划首先寻找子问题的最优解,然后在其中进行选择出一个最优解。
贪心算法则是首先做出一次“贪心”选择-----在当时看起来是最优的选择,然后求解选出的子问题,为不必费心的求解所有可能相关的子问题。
在贪心算法中,我们总是做出当时看起来最佳的选择,然后选出的唯一子问题,而动态规划是在求解之后的子问题选择出最好的那个子问题。
贪心算法进行选择时可能依赖之前做出的选择,但不依赖任何将来的选择或是子问题的解。
算法实例
1.活动安排问题
将活动按照结束时间进行从小到大排序。然后用i代表第i个活动,s[i]代表第i个活动开始时间,f[i]代表第i个活动的结束时间。按照从小到大排序,挑选出结束时间尽量早的活动,并且满足后一个活动的起始时间晚于前一个活动的结束时间,全部找出这些活动就是最大的相容活动子集合。事实上系统一次检查活动i是否与当前已选择的所有活动相容。若相容活动i加入已选择活动的集合中,否则,不选择活动i,而继续下一活动与集合A中活动的相容性。若活动i与之相容,则i成为最近加入集合A的活动,并取代活动j的位置。
2.哈夫曼编码
贪心选择性:设C是编码字符集,x和y是C中具有最小频率的两个字符,则存在C的最优前缀码使得x和y具有相同码长且仅最后一位编码不同。
每次取出频率最小的两个字符,通过一个新的结点作为根,将两个结点连接在一起,直到只剩一个结点,即构造了一棵二叉树;树中仅叶子节点代表字符,通过对树进行遍历即可得到每个字符的哈夫曼编码;构造树的过程中每次找最小的两个结点O(n),共需n-1次,时间复杂度O(n^2);但找最小的两个结点可用优先队列优化,每次查找O(logn),总复杂度O(nlogn)。
3.单源点最短路
贪心选择性:设V为结点集合,图中的已经确定了最短路径的结点的集合为S,则从V-S中选择具有最短特殊路径的顶点u,从而确定从源到u的最短路径长度dis[u];
Dijkstra算法可以求出单源点最短路中源点到其他点的最短路径长度,通过每次选择距源点最近的结点对其他结点进行松弛操作;找最近结点可用优先队列优化,每次查找O(logv),总复杂度O((v+e)logv);
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node2{
int to,nex,dis;
}edge[200010];
int head[10010],cnt;
int dis[10010];
bool vis[10010];
struct node{
int id,dis;
node(int a,int b):id(a),dis(b){}
node(){}
//重载运算符
bool operator <(const node &a)const{return dis>a.dis;}
};
//链式前向星加边
void addedge(int x,int y,int k){
edge[++cnt].to=y;
edge[cnt].dis=k;
edge[cnt].nex=head[x];
head[x]=cnt;
}
void dji(int s){
memset(dis,0x3f,sizeof(dis));
priority_queue<node> q; //优先队列存当前遍历到的点及其距源点的距离
q.push(node(s,0));
dis[s]=0;
while(!q.empty()){
node now=q.top(); q.pop();
int p=now.id;
if(vis[p]) continue;
vis[p]=true;
for(int i=head[p];i!=-1;i=edge[i].nex){
int y=edge[i].to;
if(vis[y]) continue;
//松弛
if(dis[y]>dis[p]+edge[i].dis){
dis[y]=dis[p]+edge[i].dis;
q.push(node(y,dis[y]));
}
}
}
}
int main()
{
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
memset(head,-1,sizeof(head));
int n,m,x,y,z,s;
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
addedge(x,y,z);
addedge(y,x,z);
}
dji(s);
for(int i=1;i<=n;i++){
if(dis[i]==0x3f3f3f3f) dis[i]=-1;
cout<<dis[i]<<" ";
}
return 0;
}
注:本博客仅为个人对于《计算机算法设计与分析》课程所学知识点的整理及个人的一些理解,可能会有不严谨的地方,请读者自酌。