算法思想:在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
思维导图:
具体问题:
1.活动安排问题:假设要在足够多的会场里安排一批活动,并希望使用尽可能少的会场。(这个问题实际上是著名的图着色问题。若将每一个活动作为图的一个顶点,不相容活动间用边相连。使相邻顶点有不同颜色的最小着色数,相应于要找的最小会场数。)
①.按开始时间排序
设计思想: 找到开始时间最早的然后尽可能安排多的活动在这个会场。
#include<bits/stdc++.h> using namespace std; int main(){ int m,n,i,j,k,min=0,count=1; cin>>n; int a[100][4]; for(i = 0 ;i < n ;i ++){ for(j = 0 ; j < 2 ; j++){ cin>>a[i][j]; } } for(j = 0 ; j < n ; j++){ if(a[j][0]>=0){ min=j; for(i = 0 ; i < n;i++){ if(a[i][0]<a[min][0]&&a[i][0]>=0) min=i; } a[min][0]=-1; for(i = 0 ; i < n ;i++){ if(a[i][0]>=a[min][1]){ min=i; a[i][0]=-1; } } count++; } } count--; cout<<count; } |
②.按结束时间排序
设计思想: 按照结束时间排序,然后遍历如果开始时间晚于前一个的结束时间,则在一个会场,再按照这个的结束时间继续遍历。
#include<bits/stdc++.h> using namespace std; struct stu{ int x,y; int group=0; }; bool cmp(stu a, stu b){ if(a.y>b.y){ return a.y<b.y; } else if(a.y==b.y) return a.y>b.y; } int greedy(stu a[],int n){ int i,j,count=1; for(i = 0 ; i < n ;i++){ int end = a[i].y; if(a[i].group==0){ a[i].group = count; } if(a[i].group==count){ for(j = i ; j < n ;j++){ int start = a[j].x; if(end<=start&&a[j].group==0){ a[j].group=count; end = a[j].y; } } count++; } } count--; return count; } int main(){ int n,i,j,k,m; cin>>n; stu a[100]; for(i = 0 ; i < n ;i++){ cin>>a[i].x>>a[i].y; } sort(a,a+n,cmp); m=greedy(a,n); cout<<m; return 0; } |
2.最优装载问题:有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
证明贪心选择性质:设集装箱已依其重量从大到小排序,(x1,x2.......xn)是最优装载问题的一个最优解。又设k=min{i|xi=1}{ 1<=i<=n}.易知,如果给定的最优装载问题有解,则1<=k<=n;
#include<iostream>
#include<algorithm>
#define MAXN 1000005
using namespace std;
int w[MAXN];
int main()
{
int c,n;
int sum = n;
int tmp = 0;
cin >> c >> n;
for(int i= 1; i <= n; ++i)
cin >> w[i];
sort(w+1,w+1+n);
for(int i = 1; i <= n; ++i)
{
tmp += w[i];
if(tmp >= c)
{
if(tmp == c)
sum = i;
else
sum = i-1;
break;
}
}
cout << sum << endl;
return 0;
}
3.哈夫曼编码
实现思想:实现Huffman树从集合中找到两个权最小的节点并将其合并。优先队列基于小顶堆实现,能满足较快找到权重最小两节点的要求。
代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
struct Node{
char c;
int weight;
int parentNode;
int leftrChild;
int rightChild;
friend bool operator< (Node n1, Node n2)
{
return n2.weight < n1.weight;
}
}node[200],tnode;
bool cmp(const Node &p,const Node& q)
{
return p.weight<=q.weight;
}
void dfs(int n,string str)
{
if(node[n].c<='z'&&node[n].c>='a')
{
cout<<node[n].c<<" 的编码是 "<<str<<endl;
return ;
}
int le=node[n].leftrChild;
int ri=node[n].rightChild;
dfs(le,str+'0');
dfs(ri,str+'1');
}
int main()
{
int n;
cout<<"输入字符个数"<<endl;
cin>>n;
cout<<"输入每个叶节点的权值"<<endl;
priority_queue<Node>que;
for(int i=1;i<=2*n+1;++i)
{
node[i].weight=10000;
node[i].leftrChild=0;
node[i].rightChild=0;
node[i].parentNode=0;
node[i].c='1';
}
for(int i=1;i<=n;++i)
{
int temp;
cin>>temp;
node[i].weight=temp;
char t=(i-1)+'a';
node[i].c=t;
node[i].parentNode=0;
node[i].leftrChild=0;
node[i].rightChild=0;
que.push(node[i]);
}
sort(node+1,node+1+n,cmp);
for(int j=n+1;j<=2*n-1;++j)
{
int weizhi1=0;
int weizhi2=0;
int weight1,weight2;
weight1=que.top().weight;
que.pop();
weight2=que.top().weight;
que.pop();
for(int i=1;i<=j;++i)
{
if(node[i].parentNode==0)
{
if((!weizhi1)&&node[i].weight==weight1)
{
weizhi1=i;
}
if((!weizhi2)&&node[i].weight==weight2)
{
weizhi2=i;
}
}
}
node[weizhi1].parentNode=j;
node[weizhi2].parentNode=j;
node[j].leftrChild=weizhi1;
node[j].rightChild=weizhi2;
node[j].weight=node[weizhi1].weight+node[weizhi2].weight;
que.push(node[j]);
}
string str="";
dfs(2*n-1,str);
}
4.最小生成树
问题描述:设G =(V,E)是无向连通带权图,即一个网络。E中每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树。
①.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的一棵最小生成树。每新加入一条边后,将所有已连接的边看成一个整体。
②.
Kruskal算法
Kruskal算法构造G的最小生成树的基本思想是,首先将G的n个顶点看成n个孤立的连通分支。将所有的边按权从小到大排序。然后从第一条边开始,依边权递增的顺序查看每一条边,并按下述方法连接2个不同的连通分支:当查看到第k条边(v,w)时,如果端点v和w分别是当前2个不同的连通分支T1和T2中的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边;如果端点v和w在当前的同一个连通分支中,就直接再查看第k+1条边。这个过程一直进行到只剩下一个连通分支时为止。
例如:对于下列带权图,两种算法分别如下:
Prim算法:
Kruskal算法:
5.多级调度问题
问题描述:
设有n个独立的作业,由m台相同的机器进行加工处理。作业i所需的处理时间为t[i]。 任何作业可以在任何一台机器上面加工处理,但未完工之前不允许中断处理。任何作业不能拆分成更小的作业。 要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。这个问题是NP完全问题,到目前为止还没有有效的解法,但是可以用贪心选择策略设计出较好的近似算法。
设计思想:采用最长处理时间作业优先的贪心选择策略,可以设计出解多机调度问题较好的近似算法。
当n<=m(作业数小于机器数)时,只要将机器 i 的 时间区间分配给作业 i 即可;
当n>m时,首先将n个作业从大到小排序,然后依此顺序将作业分配给空闲的处理机。也就是说从剩下的作业中,选择需要处理时间最长的,然后依次选择处理时间次长的,直到所有的作业全部处理完毕,或者机器不能再处理其他作业为止。如果我们每次是将需要处理时间最短的作业分配给空闲的机器,那么可能就会出现其它所有作业都处理完了只剩所需时间最长的作业在处理的情况,这样势必效率较低。
假定有7个独立作业,所需处理时间分别为{2,14,4,16,6,5,3},由三台机器M1,M2,M3加工。按照贪心算法产生的作业调度如下图所示,所需总加工时间为17.
#include<stdio.h>
#define N 7 //作业数
#define M 3 //机器数
int s[M] = {0,0,0};
int main(void)
{
int time[N] = {16,14,6,5,4,3,2};//处理时间按从大到小排序
int maxtime = 0;
if(M >= N)
{
maxtime = setwork1(time,N);
}
else
{
maxtime = setwork2(time,N);
}
printf("最多耗费时间%d。",maxtime);
}
int setwork1(int t[],int n)
{
int i;
for(i=0;i<n;i++)
{
s[i] = t[i];
}
int ma = max(s,N);
return ma;
}
int setwork2(int t[],int n)
{
int i;
int mi = 0;
for(i=0;i<n;i++)
{
mi = min(M);
printf("第%d号作业,时间和最小的机器号为%d.时间和为%d:\n",i,mi,s[mi]);
s[mi] = s[mi]+t[i];
}
int ma = max(s,M);
return ma;
}
int min(int m)
{
int min = 0;
int i;
for(i=1;i<m;i++)
{
if(s[min] > s[i])
{
min = i;
}
}
return min;
}
int max(int s[],int num)
{
int max = s[0];
int i;
for(i=1;i<num;i++)
{
if(max < s[i])
{
max = s[i];
}
}
return max;
}