到底什么叫贪心策略(内含几个经典贪心样例和三大图论算法)
Posted yinbiao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了到底什么叫贪心策略(内含几个经典贪心样例和三大图论算法)相关的知识,希望对你有一定的参考价值。
昨天和前天写完了分治和dp,感觉收获真的挺大的,复习绝不是简单的重复记忆,而是将所学知识融会
贯通的过程,分析各种思想的异同,这些都是在平时学习和刷题的时候没有认真考虑的问题
好了,扯远了
今天分析一下到底什么叫贪心策略
怎么理解贪心:贪心在解决问题上是目光短浅的,仅仅根据当前的已知信息就做出选择,并且一旦做了选择,就不再更改
比如01背包问题,用贪心的话是不可解决的,因为贪心每次只顾眼前最优,即每次选择价值最大的,而忽
略了另外一个变量,物品的重量,如果还考虑物品的重量的话,那就是dp了
贪心和dp的联系是非常紧密的,我们先来分析一下贪心和dp的不同之处:
dp是根据迁移过程的状态去推导下一个过程的状态,是有理论依据的,是讲道理的,通过每次完美的检验
而得到最优解,关键是找最优子结构和重复子问题,书上一句原话:dp的子结构必须的独立的,而且是重
叠的,虽然有点矛盾,但确实是这样,扯远了
而贪心每次都只顾眼前最优,目光短浅,这种方式是不讲道理的,不想dp一样,还根据前面的迁移状态推
导后面的子问题,比如最经典的01背包问题(真的是理解dp和贪心的经典例题啊)
根据贪心策略,每次放进去的都是目前最优的,即目前价值最大的,直到背包装不下,但是这样放的话肯定
是不如人意的,因为没有考虑到背包容量的问题,为什么呢?因为前面说过了,贪心策略只考虑当前最优
解,它才不会去考虑什么背包容量的问题呢,它只管装价值最大的物品,这样是得不到最优解的,必须再加
一个约束条件:背包容量,那么这个做法就变成了dp的做法了
说的再多不如看几个例题,这样才能更好的体会贪心思想
经典样例1:最优装载问题
这个问题很容易,贪心策略就是每次装价值最大的物品即可,因为物品是不考虑重量和箱子容量的
这个问题我就是懒得贴代码了,实在是太容易了
这种贪心的策略一眼就能看出来,没有什么可以讲的,但是下面这个问题的贪心策略一眼可能是看不出来的
经典样例二:活动安排问题(属于安排策略,竞争某一公共资源问题)
活动安排问题就是要在所给的活动集合中选出最大的相容活动子集和
贪心策略:使得剩余的时间最大化,先安排最早结束的活动
为什么是这样呢?可能有朋友会觉得贪心策略应该是:先安排最早开始的活动,但是这样是不行的,你要是
这么贪心的话,如果一个活动是最先开始的,但是它的结束时间超级超级长,那你这样贪心的话,岂不是只
能安排它一个活动了吗?
所以我们要使得剩余的时间最大化,就是先安排最早结束的活动,因为你这个活动最早结束的话,你留给其
他活动的剩余的安排活动时间就最多呀,何况你还安排完了自己,一举两得,何乐而不为呢?
具体做法:每个活动是一个结构体:包含开始时间,结束时间,是否被安排过三个属性,按照结束时间升序
排序,每次都选择可以相容的,结束时间最早的活动(只有想到了贪心策略,代码还是很容易写的)
每次选取一个活动要考虑两个问题:结束时间是目前没有安排的活动中最早的,相容性
相容性:在安排了上一个活动的基础上,这个活动还能安排得进去,这个活动的开始时间大于或者等于上一个已经安排好的活动的结束时间
贴个代码:
#include<bits/stdc++.h> using namespace std; #define max_v 100 struct node { int i; int end_time; int start_time; int flag; }; bool cmp(node a,node b) { return a.end_time<b.end_time; } int main() { struct node p[max_v]; int n; scanf("%d",&n); for(int i=0;i<n;i++) { scanf("%d %d %d",&p[i].i,&p[i].start_time,&p[i].end_time); p[i].i=i+1; p[i].flag=0; } sort(p,p+n,cmp); int sum=1; int end_time; end_time=p[0].end_time; p[0].flag=1; for(int i=1;i<n;i++) { if(p[i].start_time>=end_time) { sum++; p[i].flag=1; end_time=p[i].end_time; } } printf("sum=%d ",sum); printf("活动编号: "); for(int i=0;i<n;i++) { printf("%d ",p[i].i); } printf(" "); }
经典样例三:最小生成树问题(MST问题)
对于一个带权的五向连通图,其每个生成树所有变上的权值之和都可能不同,我们把所有边上权值之和最小的生成树称为最小生成树
1.Kruskal算法
因为这个算法需要用到一种数据结构:并查集
所以我先分析一下什么叫并查集,其实并查集我更愿意叫它查并集
查:查找根结点
并:结点合并
并查集是用来区分图和树的一种数据结构
图:可以有环
树:不可以有环
如果两个树的根结点是同一个的话,则他们属于同一个树,如果他们再合并的话,就会形成环,从而变成一个图
并查集步骤
1.初始化
一开始每个根结点的父结点都是自己
2.查(带路径压缩的查找)
根据根结点的父结点是自己确定这个点是不是根结点
3.并
需要合并的两个结点的根结点不是同一个的话,就可以合并,这样就不会形成环,否则不合并
贴个代码:(题目链接:https://www.cnblogs.com/yinbiao/p/9173699.html)
#include<stdio.h> #include<iostream> using namespace std; #define max_v 50005 int pa[max_v];//pa[x] 表示x的父节点 int rk[max_v];//rk[x] 表示以x为根结点的树的高度 int n,ans; void make_set(int x) { pa[x]=x;//一开始每个节点的父节点都是自己 rk[x]=0; } int find_set(int x)//带路径压缩的查找 { if(x!=pa[x]) pa[x]=find_set(pa[x]); return pa[x]; } void union_set(int x,int y) { x=find_set(x);//找到x的根结点 y=find_set(y); if(x==y)//根结点相同 同一棵树 return ; ans--; if(rk[x]>rk[y]) { pa[y]=x; }else { pa[x]=y; if(rk[x]==rk[y]) rk[y]++; } } int main() { int n,m,j=0; while(~scanf("%d %d",&n,&m)) { if(m+n==0) break; for(int i=1;i<=n;i++) { make_set(i); } ans=n; for(int i=0;i<m;i++) { int x,y; scanf("%d %d",&x,&y); union_set(x,y); } printf("Case %d: %d ",++j,ans); } return 0; }
ok,现在了解了并查集是个什么东西
现在我们可以看Kruskal算法了
Kruskal算法的核心思想:
Kruskal其实就是对边的权值排序,利用贪心的思想,贪心的策略就是:每次选择权值最小的边,在选择该
边之后不构成环的基础上
适用于稀疏图,点多的情况,无向图(可以处理负权变情况,只有迪杰斯特拉算法求单源最短路径的时候不
能处理负权边)
结束条件就是成功的选择了n-1条边,因为只有n个点嘛,n-1条边可以使得这n个点变成连通图,在没有环
的基础上
贴个代码(以上面的图为例)
#include<bits/stdc++.h> using namespace std; #define max_v 10005 struct edge//边的结构体 { int x,y;//两点 int w;//权值 }; edge e[max_v];//边的结构体数组 int rk[max_v]; int pa[max_v]; int sum; bool cmp(edge a,edge b)//结构体排序数组,按照权值升序排序 { return a.w<b.w;//升序 } void make_set(int x) { pa[x]=x; rk[x]=0; } int find_set(int x) { if(x!=pa[x]) pa[x]=find_set(pa[x]); return pa[x]; } void union_set(int x,int y,int w) { x=find_set(x); y=find_set(y); if(x==y) return ; if(rk[x]>rk[y]) { pa[y]=x; }else { if(rk[x]==rk[y]) rk[y]++; pa[x]=y; } sum+=w; printf("%d-->%d 权重:%d ",x,y,w); return ; } int main() { int n,m; while(~scanf("%d %d",&n,&m))//n个点,m条边 { sum=0; if(n==0) break; for(int i=0;i<n;i++) make_set(i);//并查集初始化 for(int i=0;i<m;i++) { scanf("%d %d %d",&e[i].x,&e[i].y,&e[i].w); } sort(e,e+m,cmp);//排序,直接调用函数库里面的sort函数(快速排序) for(int i=0;i<m;i++) { union_set(e[i].x,e[i].y,e[i].w);//两点的合并 } printf("最小的权值之和是:%d ",sum); } } /*按照边的权重排序,每次选择max/min 选择某编的时候如果构成了环,就不选 //解决:加权无向图*/ /* 输入: 7 9 0 1 28 1 2 16 2 3 12 3 4 22 4 5 25 5 0 10 1 6 14 4 6 24 6 3 18 输出: 5-->0 权重:10 2-->3 权重:12 1-->6 权重:14 6-->3 权重:16 3-->4 权重:22 3-->0 权重:25 最小的权值之和是:99 */
2.prim算法
解决稠密图问题,边多的情况
核心思想:
1.先任意选择一点加入s集合
2.从不在s集合中的点里面,选择一个点j使得j于s内的某一点的距离最小
3.重复这个过程,直到每个点都加入s集合
其实总的来说的话,很简单,理解了的话
1.找j
2.松弛(因为有新的点加入了s集合的话,其他没有加入s集合的点到s集合的距离也会随着新点的加入而变
化)
还是这个例子,我觉得好好用啊
#include<bits/stdc++.h> using namespace std; #define INF 1000000 #define max_v 105 int g[max_v][max_v];//g[i][j] 表示i点到j点的距离 int n,sum; void init() { for(int i=0; i<n; i++) for(int j=0; j<n; j++) g[i][j]=INF; } void prim() { int close[n];//记录不在s中的点在s中的最近邻接点 int lowcost[n];//记录不在s中的点到s的最短距离,即到最近邻接点的权值 int used[n];//点在s中为1,否则为0 for(int i=0; i<n; i++) { //初始化,s中只有一个点(0)//任意选择 lowcost[i]=g[0][i];//获取其他点到0点的距离,不相邻的点距离无穷大 close[i]=0;//初始化所有点的最近邻接点都为0点 used[i]=0;//初始化所有点都没有被访问过 } used[0]=1; for(int i=1; i<n; i++) { //找点 int j=0; for(int k=0; k<n; k++) //找到没有用过的且到s距离最小的点 { if(!used[k]&&lowcost[k]<lowcost[j]) j=k; } printf("%d-->%d 权值:%d ",close[j],j,lowcost[j]); sum+=lowcost[j]; used[j]=1;//j点加入到s中 //松弛 for(int k=0; k<n; k++) { if(!used[k]&&g[j][k]<lowcost[k]) { lowcost[k]=g[j][k]; close[k]=j; } } } } int main() { int m; while(~scanf("%d %d",&n,&m)) { init(); for(int i=0; i<m; i++) { int x,y,z; scanf("%d %d %d",&x,&y,&z); g[x][y]=z; g[y][x]=z; } sum=0; prim(); printf("最小生成树的权值之和为:%d ",sum); } } /* 输入: 7 9 0 1 28 1 2 16 2 3 12 3 4 22 4 5 25 5 0 10 1 6 14 4 6 24 6 3 18 输出: 0-->5 权值:10 5-->4 权值:25 4-->3 权值:22 3-->2 权值:12 2-->1 权值:16 1-->6 权值:14 最小生成树的权值之和为:99 */
3.单源最短路径问题
指起点到某点或者所有点的最短路径
迪杰斯特拉算法(跟prim算法真的超级像)
但是迪杰斯特拉算法不能处理负数权边的情况,至于为什么?看看迪杰斯特拉的核心思想就知道了
核心思想:
也是贪心,贪心策略:在没有算过的点中找一个到源点距离最小的点
最重要的数据结构:dis【i】:表示i点到源点的距离
迪杰斯特拉算法的核心就是围绕这dis数组展开的
步骤:
1.初始化
源点到其他点的距离为0,源点到其他点的距离为无穷大,随着边的输入,一些无穷大大数会被权值代替,
这个就是图的构造,同时一开始标记所有的点都没有被算过
2.在没有算过的点里面找到最小的dis,然后标记为算过
3.松弛(最重要的一步)
为什么要进行松弛:因为随着点被用到,被用到的点到源点的距离加上图中该被用到的点到其他点的距离竟
然小于从其他点到源点的距离,就是说其他点到源点的距离随着该点的被用有一个更小的值,将其他点到源
点的距离更新一下即可
老师的ppt真的贼好用啊
样例(还是用它,哈哈哈哈哈哈哈)
贴个代码:
#include<bits/stdc++.h> using namespace std; #define max_v 205 #define INF 99999 int edge[max_v][max_v]; int n,m; int used[max_v]; int dis[max_v]; void init()//初始化 { memset(used,0,sizeof(used)); for(int i=1; i<=n; i++) { for(int j=1; j<=n; j++) { edge[i][j]=INF; } dis[i]=INF; } } void Dijkstra(int s) { for(int i=1; i<=n; i++) { dis[i]=edge[s][i];//构图 } dis[s]=0; for(int i=1; i<=n; i++)//找到源点到每个点的最短路径 { int index,mindis=INF; for(int j=1; j<=n; j++) { if(used[j]==0&&dis[j]<mindis)//找到没有用过的dis值最小的j点 { mindis=dis[j]; index=j; } } used[index]=1;//j加入 for(int j=1; j<=n; j++)//松弛 { if(dis[index]+edge[index][j]<dis[j]) dis[j]=dis[index]+edge[index][j]; } } for(int i=1;i<=n;i++)//输出结果 printf("%d到%d的最短路径是:%d ",s-1,i-1,dis[i]); } int main() { while(~scanf("%d %d",&n,&m))//n个点,m条边 { init(); for(int i=0; i<m; i++) { int a,b,c; scanf("%d %d %d",&a,&b,&c);//边 edge[a+1][b+1]=edge[b+1][a+1]=c; } int s; scanf("%d",&s);//源点 Dijkstra(s+1); } } /* 输入: 7 9 0 1 28 1 2 16 2 3 12 3 4 22 4 5 25 5 0 10 1 6 14 4 6 24 6 3 18 0 输出: 0到0的最短路径是:0 0到1的最短路径是:28 0到2的最短路径是:44 0到3的最短路径是:56 0到4的最短路径是:35 0到5的最短路径是:10 0到6的最短路径是:42 */
总结:其实这些算法还有很多可以优化的地方,比如prim算法可以采用堆优化,迪杰斯特拉算法能用二叉堆优化呀,还能用斐波那契堆优化啊,因为时间有限,所以没有一一例举出来
以上是关于到底什么叫贪心策略(内含几个经典贪心样例和三大图论算法)的主要内容,如果未能解决你的问题,请参考以下文章