算法设计与分析第3章 贪心算法

第4章 贪心算法

贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。

贪心算法的基本要素

1、贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2、最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。


例1.活动安排问题:在所给的活动集合中选出最大的相容活动子集合
测试样例
input:
4
1 2
1 3
2 4
4 5
output:
3
input:
5
1 3
2 4
3 6
3 5
5 6
output:
3

分析:
由于输入的活动以其完成时间的非减序排列,所以算法每次总是选择具有最早完成时间的相容活动加入集合A中。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。
当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。

代码:

#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct seg
{
    int s,e;
} s[10010];
int cmp(seg x,seg y)
{
    return x.e<y.e;
}
int main()
{
    int i,n,num;
    while(~scanf("%d",&n))
    {
        num=1;
        for(i=0; i<n; i++)
        {
            scanf("%d%d",&s[i].s,&s[i].e);
        }
        sort(s,s+n,cmp);
        int time=s[0].e;
        for(i=1; i<n; i++)
        {
            if(time<=s[i].s)
            {
                time=s[i].e;
                num++;
            }
        }
        printf("%d\n",num);
    }
    return 0;
}

例2.活动安排问题的拓展:有若干个活动,第i个开始时间和结束时间是[Si,fi),活动之间不能交叠,要把活动都安排完,至少需要几个教室?
思路:求活动叠加的最大值
分析:将时间标记为1个时间结点(flag标记其为开始还是结束时间),然后将所有时间结点按时间大小排序,初始化交叠次数为0,接下来开始遍历时间结点,若为开始时间,则交叠次数+1,若为结束时间,则交叠次数-1,记录交叠次数的最大值,即为所求。
测试样例
input:
5
1 3
2 4
3 5
3 6
5 6
output:
3
对测试样例的处理过程
(1)n个活动,给所有时间标志结束还是开始,形成2*n个时间结点
1 0
3 1
2 0
4 1
3 0
5 1
3 0
6 1
5 0
6 1
(2)给时间结点排序
1 0
2 0
3 1 //时间值为3,标记为结束时间的排在前
3 0 //时间值为3,标记为结束时间的排在后
3 0
4 1
5 1
5 0
6 1
6 1

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<algorithm>
using namespace std;
struct seg //活动
{
	int s;
	int e;
}a[10010];
int cmp1(seg x,seg y) //先将活动按结束时间排序
{
	return x.e<y.e;
}
struct node //时间结点,flag标记:0代表开始时间,1代表结束时间
{
	int vlue;
	int flag;
}b[20010];
int cmp2(node x,node y) //时间结点排序
{
	return x.vlue<y.vlue;
}
int main()
{
	int n,sum;
	while(cin>>n)
	{
		sum=0;
		for(int i=0; i<n; i++)
		{
			cin>>a[i].s>>a[i].e;
		}
		sort(a,a+n,cmp1);  //先对活动按先结束时间排序,这样就可以使得下面的时间结点排序中,
		int k=0;           //两个活动:一个的开始时间与另一个的结束时间相同时,先将结束时间放前面
		for(int i=0; i<n; i++)
		{
			b[k].vlue=a[i].s; b[k++].flag=0;//0代表开始时间
			b[k].vlue=a[i].e; b[k++].flag=1;//1代表结束时间
		}
		sort(b,b+k,cmp2); //给时间结点排序,由于上面活动的排序,时间结点排序时,会将值相同的两个时间结点,结束标记的先排在前面
		int Max=0;        //以便于下面记录次数交叠时,先对该时间结束的活动记录交叠次数-1,再对该时间开始的活动记录交叠次数+1
		for(int i=0; i<k; i++)
		{
			if(b[i].flag==0) //活动开始
			{
				sum++; //交叠次数+1
				if(Max<sum)
				{
					Max=sum;
				}
			}
			else
			{
				sum--; //活动结束,交叠次数-1
			}
		}
		cout<<Max<<endl;
	}
}

例3.最优装载(背包)问题
给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

用贪心算法解背包问题的基本步骤:
1.首先计算每种物品单位重量的价值Vi/Wi;
2.然后依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包;
3.若将这些物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包
4.依此策略一直地进行下去,直到背包装满为止。

代码:

//C++

#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct item
{
    double weight;
    double value;
} a[1010];
int cmp(item x,item y)
{
    return (x.value/x.weight)>(y.value/y.weight);
}
int main()
{
    int i,n;
    double v;
    while(~scanf("%d%lf",&n,&v))
    {
        double sum=0;
        for(i=0; i<n; i++)
        {
            scanf("%lf%lf",&a[i].weight,&a[i].value);
        }
        sort(a,a+n,cmp);
        for(i=0; i<n; i++)
        {
            if(a[i].weight<v)
            {
                v-=a[i].weight;
                sum+=a[i].value;
            }
            else break;
        }
        if(v>0&&i!=n)
        {
            sum+=(a[i].value/a[i].weight)*v;
        }
        printf("%lf\n",sum);
    }
    return 0;
}

//C
/*
#include<stdio.h>
#include<math.h>
int wi[1010],val[1010];
void Sort(int wi[],int val[],int n)
{
    for(int i=0; i<n-1; i++)
    {
        for(int j=0; j<n-i-1; j++)
        {
            double t1=val[j]*1.0/wi[j];
            double t2=val[j+1]*1.0/wi[j+1];
            if(t1<t2)
            {
                int temp1,temp2;
                temp1=wi[j];wi[j]=wi[j+1];wi[j+1]=temp1;
                temp2=val[j];val[j]=val[j+1];val[j+1]=temp2;
            }
        }
    }
}
int main()
{
    int i,n;
    double v;
    while(~scanf("%d%lf",&n,&v))
    {
        double sum=0;
        for(i=0; i<n; i++)
        {
            scanf("%d%d",&wi[i],&val[i]);
        }
        Sort(wi,val,n);
        for(i=0; i<n; i++)
        {
            if(wi[i]<v)
            {
                v-=wi[i];
                sum+=val[i];
            }
            else break;
        }
        if(v>0&&i!=n)
        {
            sum+=(val[i]*1.0/wi[i])*v;
        }
        printf("%lf\n",sum);
    }
    return 0;
}
*/

例4.哈夫曼编码
哈夫曼编码算法用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。
给出现频率高的字符较短的编码,出现频率较低的字符以较长的编码,可以大大缩短总码长。
前缀码:对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其它字符代码的前缀。这种编码称为前缀码。

代码:

//哈夫曼编码简单实现(对应切割木棒问题)
#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
struct cmp
{
    bool operator() (int &a,int &b)
    {
        return a>b;
    }
};
int main()
{
    priority_queue<int, vector<int>, cmp>que;
    int i,n,x,sum;
    while(cin>>n)
    {
        sum=0;
        for(i=0; i<n; i++)
        {
            cin>>x;
            que.push(x);
        }
        while(que.size()!=1)
        {
            int a=que.top();
            que.pop();
            int b=que.top();
            que.pop();
            int c=a+b;
            que.push(c);
            sum+=c;
            cout<<a<<" + "<<b<<" = "<<c<<endl;
        }
        while(!que.empty())
        {
            que.pop();
        }
        cout<<sum<<endl;
    }
    return 0;
}

例5.最小生成树问题
设G =(V,E)是无向连通带权图,即一个网络。E中每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树。

最小生成树性质
设G=(V,E)是连通带权图,U是V的真子集。如果(u,v)属于E,且u属于U,v属于V-U,且在所有这样的边中,(u,v)的权c[u][v]最小,那么一定存在G的一棵最小生成树,它以(u,v)为其中一条边。这个性质有时也称为MST性质。

1、Prim算法
设G=(V,E)是连通带权图,V={1,2,…,n}。

构造G的最小生成树的Prim算法的基本思想是:
首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:
选取满足条件i属于S,j属于V-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。

按Prim算法顺序得到的最小生成树上的边如下图所示:
在这里插入图片描述

时间复杂度:O(n2)

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
#define Max 0x3f3f3f3f
int ch[1005][1005],low[1005],vis[1005]; //分别记录边权、当前最小边权、标记访问 
int n;
int prim()
{
	int pos=1,sum=0;
	memset(vis,0,sizeof(vis));
	vis[pos]=1;  //先将1加入集合 
	for(int i=2; i<=n; i++)
	{
		low[i]=ch[pos][i];
	}
	for(int i=1; i<n; i++)  //将n-1个顶点加入集合 
	{
		int Min=Max;
		for(int j=1; j<=n; j++) //找出不在集合中且边最小的顶点 
		{
			if(!vis[j]&&Min>low[j])
			{
				Min=low[j];
				pos=j;
			}
		}
		sum+=Min;
		vis[pos]=1;
		for(int j=1; j<=n; j++)  //随着新顶点的加入而更新其他顶点的最小边权 
		{
			if(!vis[j]&&low[j]>ch[pos][j])
			{
				low[j]=ch[pos][j];
			}
		}
	}
	return sum;
}
int main()
{
	int m,s,e,w;
	while(cin>>n>>m)
	{
		memset(ch,Max,sizeof(ch));
		for(int i=0; i<m; i++)
		{
			cin>>s>>e>>w;
			ch[s][e]=ch[e][s]=w;
		}
		int ans=prim();
		cout<<ans<<endl;
	}
	return 0;
}

2、Kruskal算法(避环法)
Kruskal算法构造G的最小生成树的基本思想是:
(1)首先将G的n个顶点看成n个孤立的连通分支。
(2)将所有的边按权从小到大排序。
(3)然后从第一条边开始,依边权递增的顺序查看每一条边
(4)按下述方法连接2个不同的连通分支:
当查看到第k条边(v,w)时,如果端点v和w分别是当前2个不同的连通分支T1和T2中的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边;
如果端点v和w在当前的同一个连通分支中,就直接再查看第k+1条边。这个过程一直进行到只剩下一个连通分支时为止。

按Kruskal算法顺序得到的最小生成树上的边如下图所示:
在这里插入图片描述

算法复杂度:
当图的边数为e时,Kruskal算法所需的计算时间是O(eloge)。当时,Kruskal算法比Prim算法差,但当时,Kruskal算法却比Prim算法好得多。(即边少时Kruskal算法优于Prim)

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int f[1010];
int find(int x)
{
    return f[x]==x?x:f[x]=find(f[x]);
}
struct city
{
    int a;
    int b;
    int c;
}ch[6000];
int cmp(city x,city y)
{
    return x.c<y.c;
}
int main()
{
    int i,n,m;
    while(cin>>n>>m)
    {
        int sum=0;
        for(i=1; i<=n; i++)
        {
            f[i]=i;
        }
        for(i=1; i<=m; i++)
        {
            cin>>ch[i].a>>ch[i].b>>ch[i].c;
        }
        sort(ch+1,ch+1+m,cmp);
        for(i=1; i<=m; i++)
        {
            int xx=find(ch[i].a);
            int yy=find(ch[i].b);
            if(xx!=yy)
            {
                if(xx>yy)
                {
                    int temp;
                    temp=xx;xx=yy;yy=temp;
                }
                f[yy]=xx;
                sum+=ch[i].c;
            }
        }
        cout<<sum<<endl;
    }
    return 0;
}

例6.最短路径问题

1.Dijkstra(迪杰斯特拉)算法

典型的最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。

基本思想:设置顶点集合S并不断地作贪心选择来扩充这个集合。初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dis记录当前每个顶点所对应的最短路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dis作必要的修改。一旦S包含了所有V中顶点,dis就记录了从源到所有其它顶点之间的最短路径长度。

算法复杂度:
时效性较好,时间复杂度为O(V2+E)
源点可达的话,O(V*lgV+E*lgV)=> O(E*lgV)
当是稀疏图的情况时,此时E=V2/lgV,所以算法的时间复杂度可为O(V2
若是斐波那契堆作优先队列的话,算法时间复杂度,则为O(V*lgV + E)

代码:

/*
无法判断是否存在负权边,非负边权图中效率稳定
使用范围:求单源(单源点的最短路径问题是指:给定一个加权有向图G和源点s,对于图G中的任意一点v,求从s到v的最短路径。)、无负权的最短路。权值非负时使用
*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define INF 0x3ffffff
using namespace std;
int ch[505][505];
int dis[505];
int vis[505];
//int path[505];
int main()
{
    int n,m,i,j,x,y,l,source,des;
    while(cin >> n >>m)
    {
        //cin>>source>>des;
        for(i=0; i<n; i++)
        {
            for(j=0; j<n; j++)
            {
                ch[i][j] = INF;
            }
        }
        for(i=0; i<n; i++)
        {
            dis[i] = INF;
			vis[i] = 0;
            //path[i]=source;
        }
        //dis[source]=0;
        dis[0] = 0;
        for(i=0; i<m; i++)
        {
            scanf("%d%d%d",&x,&y,&l);
            ch[x][y] = min(ch[x][y],l);
            ch[y][x] = min(ch[y][x],l);
        }
        for(i=0; i<n; i++) //遍历n个顶点
        {
            int Min = INF;
            int s=-1;
            for(j=0; j<n; j++) //每次找一个最小权顶点 
            {
                if(Min > dis[j] && !vis[j])
                {
                    s = j;
                    Min = dis[j];
                }
            }
            if(s!=-1)
            {
                vis[s] = 1;
                for(j=0; j<n; j++)
                {
                    dis[j] = min(dis[s] + ch[s][j],dis[j]);
                   /* if(dis[j]>dis[s]+ch[s][j])
                    {
                        dis[j]=dis[s]+ch[s][j];
                        path[j]=s;
                    }
                    */
                }
            }

        }
        for(i=0; i<n; i++)
        {
            for(j=0; j<n; j++)
            {
                printf("%d ",ch[i][j]);
            }
            printf("\n");
        }

        for(j=0; j<n; j++)
        {
            printf("%d ",dis[j]);
        }
        cout << endl;
       /*
        for(i=0; i<n; i++)//求具体路径
        {
            if(i==s) continue;
            printf("从%d到%d最短路为 :%d\t",s,i,dis[i]);
            int next;
            next=path[i];
            printf("%d--",i);
            while(next!=path[next])
            {
                printf("%d--",next);
                next=path[next];
            }
            printf("%d\n",s);

        }*/
        return 0;
}

2.Bellman-Ford算法

求解单源最短路径问题的一种算法。求单源最短路,可以判断有无负权回路(若有,则不存在最短路),与Dijkstra算法不同的是,在Bellman-Ford算法中,边的权值可以为负数。

Bellman-Ford算法的流程如下:
给定图G(V, E)(其中V、E分别为图G的顶点集与边集),源点s,
数组dis[i]记录从源点s到顶点i的路径长度,初始化数组dis[n]为MAX,dis[s]为0;
以下操作循环执行至多n-1次,n为顶点数:
对于每一条边e(u, v),如果dis[u] + w(u, v) < dis[v],则令dis[v] = dis[u]+w(u, v)。w(u, v)为边e(u,v)的权值;
若上述操作没有对dis进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。否则执行下次循环;
为了检测图中是否存在负环路,即权值之和小于0的环路。对于每一条边e(u, v),如果存在dis[u] + w(u, v) < dis[v]的边,则图中存在负环路,即是该图无法求出单源最短路径。否则数组dis[n]中记录的就是源点s到各顶点的最短路径长度。

时间复杂度:O(V*E)。

代码:

/*
求含负权图的单源最短路径算法,可检测并输出负圈效率较低
使用范围:当权值有负值,且可能存在负圈时使用
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#define MAX 200000000    //表示无穷远
using namespace std;
struct node              //结构体存边
{
    int x,y;
    int l;
} a[10005];
int n,m;                 //n个点,m条边
int dis[105];            //到每个点的最短距离
int main()
{
    int i,j,x,y,l;
    while(~scanf("%d%d",&n,&m)&&(n||m))
    {
        for(i=1; i<=n; i++)
 	  	{
        	dis[i]=MAX;
   		}
        j=0;
        for(i=0; i<m; i++)
        {
            scanf("%d%d%d",&x,&y,&l);
            a[j].x = x;
            a[j].y = y;
            a[j++].l = l;
            a[j].x = y;
            a[j].y = x;
            a[j++].l = l;
        }
        dis[1]=0;                      //起点标记为0
        bool flag = 1;
        for(i=0; i<n-1; i++)
        {
            if(flag)
            {
                flag=0;
                for(j=0; j<2*m; j++)
                {
                    if(dis[a[j].y] > dis[a[j].x]+a[j].l)
                    {
                        dis[a[j].y] = dis[a[j].x]+a[j].l;
                        flag = 1;
                    }
                }
            }
            else
                break;
        }
       /* flag = 0;                                       //检验是否存在负环
        for(i=0; i<2*m; i++)
        {
            if(dis[a[i].y] > dis[a[i].x]+a[i].l)          //如果if句成立 表明上循环完成后再循环中出现负环
            {
                dis[a[i].y] = dis[[i].x]+a[i].l;
                flag = 1;
            }
        }
        */
        printf("%d\n",dis[n]);
    }
    return 0;
}

3.SPFA算法
Bellman-Ford的队列优化,与Bellman-ford算法类似,SPFA算法采用一系列的松弛操作以得到从某一个节点出发到达图中其它所有节点的最短路径。不同的是,SPFA算法通过维护一个队列,使得一个节点的当前最短路径被更新之后没有必要立刻去更新其他的节点,从而大大减少了重复的操作次数。

与Dijkstra算法与Bellman-ford算法都不同,SPFA的算法时间效率是不稳定的,即它对于不同的图所需要的时间有很大的差别。
SPFA算法在负边权图上可以完全取代Bellman-ford算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法,以及它的使用堆优化的版本。

时间复杂度:O(kE)(k<<V)。
在最好情形下,每一个节点都只入队一次,则算法实际上变为广度优先遍历,其时间复杂度仅为O(E)。如果每一个节点都被入队(V-1)次,算法退化为Bellman-ford算法,其时间复杂度为O(VE)。

代码:

/*
Bellman的队列优化  可检测但不能输出负圈
使用范围:当权值有负值且没有负圈时使用SPFA 
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <queue>
#include <vector>
#define MAX 20000000
using namespace std;
struct node
{
    int x;
    int l;
};
vector<node> a[105];     //邻接表存图
queue<int> q;
int n,m;
int vis[105];            //标记是否在队列中
int dis[105];
void init()
{
    int i,j;
    for(i=1; i<=n; i++)
    {
        a[i].clear();
        vis[i]=0;
        dis[i] = MAX;
    }
    while(!q.empty())
    {
        q.pop();
    }
}
int main()
{
    int i,j,x,y,l;
    while(~scanf("%d%d",&n,&m)&&(n||m))
    {
        init();
        for(i=0; i<m; i++)
        {
            scanf("%d%d%d",&x,&y,&l);
            node t1,t2;
            t1.x = y;
            t1.l = l;
            t2.x = x;
            t2.l = l;
            a[x].push_back(t1);
            a[y].push_back(t2);
        }
        dis[1] = 0;
        q.push(1);           //入队的是下标
        vis[1]=1;            //标记在队中
        while(!q.empty())
        {
            int s = q.front();
            q.pop();
            vis[s] = 0;
            x = s;
            for(i=0; i<a[s].size(); i++)
            {
                y = a[s][i].x;
                if(dis[y]>dis[x]+a[s][i].l)
                {
                    dis[y]=dis[x]+a[s][i].l;
                    if(!vis[y])
                        q.push(y);
                }
            }
        }
        printf("%d\n",dis[n]);
    }
    return 0;
}
  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值