到底什么叫贪心策略(内含几个经典贪心样例和三大图论算法)

昨天和前天写完了分治和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\n",sum);
    printf("活动编号:\n");
    for(int i=0;i<n;i++)
    {
        printf("%d ",p[i].i);
    }
    printf("\n");
}

经典样例三:最小生成树问题(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\n",++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\n",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\n",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\n",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\n",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\n",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算法可以采用堆优化,迪杰斯特拉算法能用二叉堆优化呀,还能用斐波那契堆优化啊,因为时间有限,所以没有一一例举出来

原文地址:https://www.cnblogs.com/yinbiao/p/9220291.html

时间: 2024-10-12 20:25:19

到底什么叫贪心策略(内含几个经典贪心样例和三大图论算法)的相关文章

哈夫曼编码--贪心策略

哈夫曼编码还是在暑假时候看的,那时候并没有看懂因为比较菜(虽然现在也是很菜的),在<趣学算法>一书中这个问题讲解十分到位,我这篇博客真的是难以望其项背,只能对其进行一点借鉴和摘抄吧 哈夫曼编码是一棵树,权值越大的节点越靠近树根,越小的节点就越远离树根,从他的定义来看,首先想到的应该是贪心策略吧,没错就是贪心算法 虽然说是贪心算法,但是还要知道它 的实现方式啊,他的贪心策略是:每次从树的集合中取出没有双亲且权值最小的两棵树作为左右子树,并合并他们 步骤 : 1 :确定合适的数据结构(要知道他的左

一次性弄懂到底什么叫做分治思想(含有大量经典例题,附带详细解析)

期末了,通过写博客的方式复习一下算法,把自己知道的全部写出来 分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了 但是我们应该在什么时候用分治呢?这个问题也困扰了我很久,做题的时候就不知道用什么算法 能用分治法的基本特征: 1.问题缩小到一定规模容易解决 2.分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质 3.分解而成的小问题在解决之后要可以合并 4.子问题是相互独立的,即子问题之间没有公共的子问题 第一条大多数问

【贪心策略】USACO 越野跑

问题 H: 越野跑[贪心策略] [题面] 为了能在下一次跑步比赛中有好的发挥,贝茜在一条山路上开始了她的训练.贝茜希望能在每次训练中跑得尽可能远,不过她也知道农场中的一条规定:奶牛独自进山的时间不得超过M秒(1< =M< =10,000,000). 整条山路被贝茜划分成T个长度相同的小段(1< = T< = 100,000),并且,贝茜用S_i表示第i个小段的路况.S_i为u,f,d这3个字母之一,它们分别表示 第i个小段是上坡.平地,或是下坡. 贝茜要花U秒(1< =U&l

一类区间选择问题的贪心策略

有一类问题在区间上的选择问题有如下特征: 1. 每个选择之间不交叉 2. 区间元素可以在某个选择中,也可以不在任何选择中 3. 最大化选择的个数 对于这类问题,我们可以考虑采取贪心策略.具体来说,对于区间从左到右考虑,每次发现一个可行的组合就选择,保证右区间尽可能小. Codechef CHEFPRAD [给a,b数组,数组内部元素相对位置不变,但ab之间的元素可以同时位移一个距离.每个元素可以匹配和它距离小于y的对面的点,求最大匹配] [观察到最优匹配方案一定是落在b数组某个元素的上边界上(如

LeetCode--Best Time to Buy and Sell Stock (贪心策略 or 动态规划)

Best Time to Buy and Sell Stock Total Accepted: 14044 Total Submissions: 45572My Submissions Say you have an array for which the ith element is the price of a given stock on day i. If you were only permitted to complete at most one transaction (ie, b

CF10E Greedy Change 判断硬币系统是否能用贪心策略

Billy investigates the question of applying greedy algorithm to different spheres of life. At the moment he is studying the application of greedy algorithm to the problem about change. There is an amount of n coins of different face values, and the c

The - Modcrab——使用贪心策略

一.题目信息 The - Modcrab 简单翻译一下:Vova有生命值h1,每次攻击值为a1,每瓶药水恢复生命值c1;Modcrab有生命值h2,每次攻击值为a2.在每个关卡开始,Vova有两种选择,要么攻击怪兽Modcrab,要么喝一瓶药水(Modcrab的生命值允许超过h2),然后,如果战斗没有结束,Modcrab会攻击Vova.战斗结束的标志是Vova(或Modcrab)的生命值降至0,或者更低. 注:1.选择喝药水,Modcrab也会攻击Vova.2.攻击是有先后顺序的 二.算法描述

浅谈贪心策略——相邻交换

浅谈贪心策略——相邻交换 题解主要写贪心的考虑方法:相邻交换法. 我们在平时的贪心题几乎都可以正确的贪心方法. 主要思想 设交换前对答案的贡献为x,交换后对答案的贡献为y l  若x>y则不交换 l  若x<y则需交换 l  若x==y则不交换(交换反而增加时间复杂度) 作为题目,需要建立数学模型设置未知数表示x和y得到不等式从而得出排序的关键字. 例题:皇后游戏(Luogu OJ P2123) 网址: https://www.luogu.org/problemnew/show/P2123 题

背包问题(贪心策略)

原创 给定n种物品和一个背包.物品i的重量是Wi,其价值为Vi,背包的容量为C.应如何选择装入背包的物品, 使得装入背包中物品的总价值最大?物品时可以拆分的,比如可以将物品的三分之一放入背包. 使用优先放入[价值/重量]最大的物品的贪心策略解题. 1 import java.util.Scanner; 2 class sack{ //背包类 3 private double c; //背包容量 4 private double n; //物品个数 5 private double w[]; //