一、贪心法
1、定义
? 贪心法的基本思路是在对问题求解时总是做出在当前看来是最好的选择,也就是说贪心法不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。
? 人们通常希望找到整体最优解,所以采用贪心法需要证明设计的算法确实是整体最优解或求解了它要解决的问题。
2、贪心法具有的性质
1、贪心选择性质
? 所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
也就是说,贪心法仅在当前状态下做出最好选择,即局部最优选择,然后再去求解做出这个选择后产生的相应子问题的解。
2、最优子结构性质
? 如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优子结构性质。
? 问题的最优子结构性质是该问题可用动态规划算法或贪心法求解的关键特征。
3、贪心法的算法框架
SolutionType Greedy(SType a[],int n)
{ SolutionType x={};
for (int i=0;i<n;i++)
{ SType xi=Select(a);
if (Feasiable(xi))
solution=Union(x,xi);
}
return x;
}
5、求解活动安排问题
? 【问题描述】假设有一个需要使用某一资源的n个活动所组成的集合S,S={1,…,n}。该资源任何时刻只能被一个活动所占用,活动i有一个开始时间bi和结束时间ei(bi<ei),其执行时间为ei-bi,假设最早活动执行时间为0。
? 一旦某个活动开始执行,中间不能被打断,直到其执行完毕。若活动i和活动j有bi≥ej或bj≥ei,则称这两个活动兼容。
? 设计算法求一种最优活动安排方案,使得所有安排的活动个数最多。
? 【问题求解】假设活动时间的参考原点为0。一个活动i(1≤i≤n)用一个区间[bi,ei)表示,当活动按结束时间(右端点)递增排序后,两个活动[bi,ei)和[bj,ej)兼容(满足bi≥ej或bj≥ei)实际上就是指它们不相交。
? 用数组A存放所有的活动,A[i].b(1≤i≤n),存放活动起始时间,A[i].e存放活动结束时间。
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|
开始时间 | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 | 结束时间 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 |
产生最大兼容活动集合的过程:
活动1 √
活动2 ′
活动3 ′
活动4 √
活动5 ′
活动6 ′
活动7 ′
活动8 √
活动9 ′
活动10 ′
活动11 √
代码如下:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAX 51
struct Action
{ int b;
int e;
bool operator<(const Action &s) const
{
return e<=s.e;
}
};
int n=11;
Action A[]={{0},{1,4},{3,5},{0,6},{5,7},{3,8},{5,9},{6,10},{8,11},{8,12},{2,13},{12,15}};
bool flag[MAX];
int Count=0;
void solve()
{
memset(flag,0,sizeof(flag));
sort(A+1,A+n+1);
int preend=0;
for (int i=1;i<=n;i++)
{ if (A[i].b>=preend)
{ flag[i]=true;
preend=A[i].e;
}
}
}
int main()
{
solve();
printf("求解结果\n");
printf(" 选取的活动:");
for (int i=1;i<=n;i++)
if (flag[i])
{
printf("[%d,%d] ",A[i].b,A[i].e);
Count++;
}
printf("\n 共%d个活动\n",Count);
}
【算法分析】算法的主要时间花费在排序上,排序时间为O(nlog2n),所以整个算法的时间复杂度为O(nlog2n)。
6、求解最优装载问题
【问题描述】有n个集装箱要装上一艘载重量为W的轮船,其中集装箱i(1≤i≤n)的重量为wi。
? 不考虑集装箱的体积限制,现要选出尽可能多的集装箱装上轮船,使它们的重量之和不超过W。
【问题求解】第5章讨论了简单装载问题,采用回溯法选出尽可能少的集装箱个数。这里的最优解是选出尽可能多的集装箱个数,并采用贪心法求解。
? 当重量限制为W时,wi越小可装载的集装箱个数越多,所以采用优先选取重量轻的集装箱装船的贪心思路。
? 对wi从小到大排序得到{w1,w2,…,wn},设最优解向量为x={x1,x2,…,xn},显然,x1=1,则x’={x2,…,xn}是装载问题w’={w2,…,wn},W’=W-w1的最优解,满足贪心最优子结构性质。
代码如下:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAXN 20
int w[]={0,5,2,6,4,3};
int n=5,W=10;
int maxw;
int x[MAXN];
void solve()
{
memset(x,0,sizeof(x));
sort(w+1,w+n+1);
maxw=0;
int restw=W;
for (int i=1;i<=n && w[i]<=restw;i++)
{
x[i]=1;
restw-=w[i];
maxw+=w[i];
}
}
int main()
{
solve();
printf("最优方案\n");
for (int i=1;i<=n;i++)
if (x[i]==1)
printf(" 选取重量为%d的集装箱\n",w[i]);
printf("总重量=%d\n",maxw);
}
【算法分析】算法的主要时间花费在排序上,时间复杂度为O(nlog2n)。
二、 贪心法实验
1、实验一 求解田忌赛马问题
? 【问题描述】二千多年前的战国时期,齐威王与大将田忌赛马。双方约定每人各出300匹马,并且在上、中、下三个等级中各选一匹进行比赛,由于齐威王每个等级的马都比田忌的马略强,比赛的结果可想而知。现在双方各n匹马,依次派出一匹马进行比赛,每一轮获胜的一方将从输的一方得到200银币,平局则不用出钱,田忌已知所有马的速度值并可以安排出场顺序,问他如何安排比赛获得的银币最多。
? 输入:输入包含多个测试用例,每个测试用例的第一行正整数n(n≤1000)马的数量,后两行分别是n个整数,表示田忌和齐威王的马的速度值。输入n=0结束。
? 输出:每个测试用例输出一行,表示田忌获得的最多银币数。
输入样例:
3
92 83 71
95 87 74
2
20 20
20 20
2
20 19
22 18
0
样例输出:
200
0
0
【问题求解】田忌的马速度用数组a表示,齐威王的马速度用数组b表示,将a、b数组递增排序。采用常识性的贪心思路,分以下几种情况:
(1)田忌最快的马比齐威王最快的马快即a[righta]>b[rightb],则两者比赛(两个最快的马比赛),田忌赢。因为此时田忌最快的马一定赢,而选择与齐威王最快的马比赛对于田忌来说是最优的,下图中“■”代表已经比赛的马,“□”代表尚未比赛的马,箭头指向的马速度更快。
(2)田忌最快的马比齐威王最快的马慢即a[righta]<b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输。因为齐威王最快的马一定赢,而选择与田忌最慢的马比赛对于田忌来说是最优的。
(3)田忌最快的马与齐威王最快的马的速度相同即a[righta]=b[rightb],又分为以下3种情况:
? ① 田忌最慢的马比齐威王最慢的马快即a[lefta]>b[leftb],则两者比赛(两个最慢的马比赛),田忌赢。因为此时齐威王最慢的马一定输,而选择与田忌最慢的马比赛对于田忌来说是最优的。
? ② 田忌最慢的马比齐威王最慢的马慢,并且田忌最慢的马比齐威王最快的马慢,即a[lefta]≤b[leftb]且a[lefta]<b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输。因为此时田忌最慢的马一定输,而选择与齐威王最快的马比赛对于田忌来说是最优的。
? ③ 其他情况,即a[righta]=b[rightb]且a[lefta]≤b[leftb]且a[lefta]≥b[rightb],则a[lefta]≥b[rightb]=a[righta],即a[lefta]=a[righta],b[leftb]≥a[lefta]=b[rightb],即b[leftb]=b[rightb],说明比赛区间的所以马的速度全部相同,任何两匹马比赛都没有输赢。
上述过程看出每种情况对于田忌来说都是最优的,因此最终获得的比赛方案也一定是最优的。
代码如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
#define MAX 1001
int n;
int a[MAX];
int b[MAX];
int ans;
void solve()
{
sort(a,a+n);
sort(b,b+n);
ans=0;
int lefta=0,leftb=0;
int righta=n-1,rightb=n-1;
while (lefta<=righta)
{
if (a[righta]>b[rightb])
{
ans+=200;
righta--;
rightb--;
}
else if (a[righta]<b[rightb])
{
ans-=200;
lefta++;
rightb--;
}
else
{
if (a[lefta]>b[leftb])
{
ans+=200;
lefta++;
leftb++;
}
else
{
if (a[lefta]<b[rightb])
ans-=200;
lefta++;
rightb--;
}
}
}
}
int main()
{
while (true)
{
scanf("%d",&n);
if (n==0) break;
for (int i=0;i<n;i++)
scanf("%d",&a[i]);
for (int j=0;j<n;j++)
scanf("%d",&b[j]);
solve();
printf("%d\n",ans);
}
return 0;
}
【算法分析】算法的主要时间花费在排序上,时间复杂度为O(nlog2n)。
2、实验二 求解多机调度问题
【问题描述】设有n个独立的作业{1,2,…,n},由m台相同的机器{1,2, …,m}进行加工处理,作业i所需的处理时间为ti(1≤i≤n),每个作业均可在任何一台机器上加工处理,但未完工前不允许中断,任何作业也不能拆分成更小的子作业。
多机调度问题要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。
【问题求解】贪心法求解多机调度问题的贪心策略是最长处理时间作业优先,即把处理时间最长的作业分配给最先空闲的机器,这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。
按照最长处理时间作业优先的贪心策略,当m≥n时,只要将机器i的[0,ti)时间区间分配给作业i即可;
当m<n时,首先将n个作业依其所需的处理时间从大到小排序,然后依此顺序将作业分配给空闲的处理机。
例如,有7个独立的作业{1,2,3,4,5,6,7},由3台机器{1,2,3}加工处理,各作业所需的处理时间如下:
作业编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|
作业的处理时间 | 2 | 14 | 4 | 16 | 6 | 5 | 3 |
采用贪心法求解的过程如下:
(1)7个作业按处理时间递减排序,其结果如下表所示。
作业编号 | 4 | 2 | 5 | 6 | 3 | 7 | 1 |
---|
作业的处理时间 | 16 | 14 | 6 | 5 | 4 | 3 | 2 |
(2)先将排序后的前3个作业分配给3台机器。此时机器的分配情况为{{4},{2},{5}},对应的总处理时间为{16,14,6}。
作业编号 | 4 | 2 | 5 | 6 | 3 | 7 | 1 |
---|
作业的处理时间 | 16 | 14 | 6 | 5 | 4 | 3 | 2 |
(3)分配余下的作业
代码如下:
#include <stdio.h>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
#define N 100
int n=7;
int m=3;
struct NodeType
{
int no;
int t;
int mno;
bool operator<(const NodeType &s) const
{ return t>s.t; }
};
struct NodeType A[]={{1,2},{2,14},{3,4},{4,16},{5,6},{6,5},{7,3}};
void solve()
{
NodeType e;
if (n<=m)
{ printf("为每一个作业分配一台机器\n");
return;
}
printf(" 排序前");
for (int s=0;s<n;s++)
printf(" [%d:%d]",A[s].no,A[s].t);
printf("\n");
sort(A,A+n);
printf(" 排序后");
for (int s=0;s<n;s++)
printf(" [%d:%d]",A[s].no,A[s].t);
printf("\n");
priority_queue<NodeType> qu;
for (int i=0;i<m;i++)
{
A[i].mno=i+1;
printf(" 给机器%d分配作业%d,执行时间为%2d,占用时间段:[%d,%d]\n",
A[i].mno,A[i].no,A[i].t,0,A[i].t);
qu.push(A[i]);
}
for (int j=m;j<n;j++)
{
e=qu.top(); qu.pop();
printf(" 出队:e.no=%d,e.t=%d,e.mno=%d\n",e.no,e.t,e.mno);
printf(" 给机器%d分配作业%d,执行时间为%2d,占用时间段:[%d,%d]\n",
e.mno,A[j].no,A[j].t,e.t,e.t+A[j].t);
e.t+=A[j].t;
qu.push(e);
printf(" 进队:e.no=%d,e.t=%d,e.mno=%d\n",e.no,e.t,e.mno);
}
}
int main()
{
printf("多机调度方案:\n");
solve();
}
【算法分析】排序的时间复杂度为O(nlog2n),两次for循环的时间合起来为O(n),所以本算法的时间复杂度为O(nlog2n)。
3、实验三 哈夫曼编码
【问题描述】设要编码的字符集为{d1, d2, …, dn},它们出现的频率为{w1, w2, …, wn},应用哈夫曼树构造最优的不等长的由0、1构成的编码方案。
【问题求解】先构建以这个n个结点为叶子结点的哈夫曼树,然后由哈夫曼树产生各叶子结点对应字符的哈夫曼编码。
? 哈夫曼树(Huffman Tree)的定义:设二叉树具有n个带权值的叶子结点,从根结点到每个叶子结点都有一个路径长度。从根结点到各个叶子结点的路径长度与相应结点权值的乘积的和称为该二叉树的带权路径长度,记作:
? 由n个叶子结点可以构造出多种二叉树,其中具有最小带权路径长度的二叉树称为哈夫曼树(也称最优树)。
构造一棵哈夫曼树的方法如下:
(1)由给定的n个权值{w1,w2,…,wn}构造n棵只有一个叶子结点的二叉树,从而得到一个二叉树的集合F={T1,T2,…,Tn}。
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。即合并两棵二叉树为一棵二叉树。
(3)重复步骤(2),当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
代码如下:
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <map>
using namespace std;
#define MAX 101
int n;
struct HTreeNode
{
char data;
int weight;
int parent;
int lchild;
int rchild;
};
HTreeNode ht[MAX];
map<char,string> htcode;
struct NodeType
{
int no;
char data;
int weight;
bool operator<(const NodeType &s) const
{
return s.weight<weight;
}
};
void CreateHTree()
{
NodeType e,e1,e2;
priority_queue<NodeType> qu;
for (int k=0;k<2*n-1;k++)
ht[k].lchild=ht[k].rchild=ht[k].parent=-1;
for (int i=0;i<n;i++)
{
e.no=i;
e.data=ht[i].data;
e.weight=ht[i].weight;
qu.push(e);
}
for (int j=n;j<2*n-1;j++)
{
e1=qu.top(); qu.pop();
e2=qu.top(); qu.pop();
ht[j].weight=e1.weight+e2.weight;
ht[j].lchild=e1.no;
ht[j].rchild=e2.no;
ht[e1.no].parent=j;
ht[e2.no].parent=j;
e.no=j;
e.weight=e1.weight+e2.weight;
qu.push(e);
}
}
void CreateHCode()
{
string code;
code.reserve(MAX);
for (int i=0;i<n;i++)
{
code="";
int curno=i;
int f=ht[curno].parent;
while (f!=-1)
{
if (ht[f].lchild==curno)
code='0'+code;
else
code='1'+code;
curno=f; f=ht[curno].parent;
}
htcode[ht[i].data]=code;
}
}
void DispHCode()
{
map<char,string>::iterator it;
for (it=htcode.begin();it!=htcode.end();++it)
cout << " " << it->first << ": " << it->second << endl;
}
void DispHTree()
{
for (int i=0;i<2*n-1;i++)
{
printf(" data=%c, weight=%d, lchild=%d, rchild=%d, parent=%d\n",
ht[i].data,ht[i].weight,ht[i].lchild,ht[i].rchild,ht[i].parent);
}
}
int WPL()
{
int wps=0;
for (int i=0;i<n;i++)
wps+=ht[i].weight*htcode[ht[i].data].size();
return wps;
}
int main()
{
n=5;
ht[0].data='a'; ht[0].weight=4;
ht[1].data='b'; ht[1].weight=2;
ht[2].data='c'; ht[2].weight=1;
ht[3].data='d'; ht[3].weight=7;
ht[4].data='e'; ht[4].weight=3;
CreateHTree();
printf("构造的哈夫曼树:\n");
DispHTree();
CreateHCode();
printf("产生的哈夫曼编码如下:\n");
DispHCode();
printf("WPL=%d\n",WPL());
}
|