当前最优解导致全局的最优解,且一旦做出选择则不能更改。每次考虑一个输入,针对每个输入,选择当前的最优解。
参考去年从图书馆里借的一本书,忘了叫什么了,这里好多代码都是从那本书上抄的,过几天去看看再补上。
算法设计
- 将优化问题转化为:先做出选择,再解决剩下的子问题
- 证明原问题总是有一个最优解是贪心选择得到的,从而说明贪心选择的安全。即利用数学归纳法或反证法证明按照贪心操作,在可行解中不可能存在比贪心解更优的解。
- 说明在做出贪心选择后,剩余的子问题具有这样一个性质:将子问题的最优解和所做出的贪心选择联合后,得出原问题的一个最优解。
- EG:算法导论 16.2-4
描述:
M教授从Newark开一辆车沿着公路到Reno。他车子的油箱在满时候,可以存放够跑 n英里的汽油,并且他的地图标识了在他的路途中加油站之间的距离。教授希望在路 途中尽量少的加油。
分析:
由题意可知,我们可以将原问题转换为图论。以起点、终点和各个加油站为节点,将 再加油后能到达的节点以边相连。对于起点,视为加油量为 0 的加油站;终点为加油 量为 -INF (负无穷)的加油站。求从起点到终点的最短路径距离。所以,其可行解集为 所有从起点到终点的路径。不难得出,贪心选择为在所有可连接的节点中,选择加油 量最大的节点。
此外,有的贪心策略结果是从原集中找出一个最优子集,这种贪心策略称为子集范例。而对于那些不需要找出最优子集的问题,如霍夫曼树,通常已某种特定的顺序来考量这些输入,每一步的选择都是基于一个已有选择的最优标准,这样的贪心策略叫排列范例。
附:
子集范例的抽象控制
函数Select 选择出数组a[ ]中的一个值作为输入,同时将它从a[ ]中删除,这个被选出的值设为x,Feasible函数是一个布尔函数,用来表示x是否可以被加入到当前的部分分解序列中,Union函数的功能是将x合并到当前的部分分解序列中,同时更新目标函数的值。
SolType Greedy(Type a[] ,int n){
//a[] input length: n
SolType solution = EMPTY; //初始化解
for( int i=1;i<=n;i++){
Type x=Select(a);
if( feasible(solution , x) )
solution =Union(solution , x);
}
return solution;
}
- 贪心算法 与 DP 设计的不同
在动态规划中,每一步都要做出选择,但是这些选择依赖于子问题的解,因此,解动态规划问题一般是自底向上,从小问题处理至大问题。在贪心算法中,每一步做出当前看似最优的选择,再解决之后出现的子问题。当前算则可能依赖已做出的所有选择,但不依赖于有待做出的子问题的解,一年次,贪心算法通常是自顶向下的选择,不断将问题划分为更小的问题。
典型例题
- 字典序最小POJ 3617 代表类型:所有的解决方案可列,注意最优解可能不唯一
本题中的两个操作为在执行的每一步中的全部解决方案,因此不难看出贪心算法的体现以及如何设计。但是需要注意的是,当局部最优解不唯一,即首尾元素相同,也是需要讨论的地方。秉着局部最优解导出全局最优解的规则,先假设选取首或尾的元素,再由其各自产生的结果,判断当前应选择的元素。由于可能由连读的首位相同的情况,即最优解不唯一的下一步仍然无法得到唯一的最优解。因此,将原数组反转并与原数组比较字典序的大小。
- 硬币问题
若使使用的嘤币最少,应当按降序排列输入,优先从最大面额选取,这样就保证了可以
从局部最优到全局最优
PS;
1、这里贪心算法可以成立存在隐藏条件,对于任意大于500的数 N=C100(C ϵ n,& C>5) ,一定存在两个正整数k1 , k2,由 N =k1500 + k2*100。这样才使得第一次去500后一定有解。而对于{500,300,50,30,5,3}这样的币值,当A=600时,则不能用贪心算法(设计穷竭算法求解?)
2、对于一个给定的数列{aⁱ},求其中是否存在 M 个数的和为N ,不能使用贪心的原因:若将所给的数列按照降序排列,当前最优解可能不导致全局最优
{14,12,5,1 } M=2 N=24
- 会场安排问题
变形:区间图着色问题
描述:假设要用多个教室对一组活动进行调度。我们希望使用尽可能少的的教室来调度所有的活动。
我们可以作出一个区间图,其定点为已知活动,其边连接着不兼容的活动。为了使人两个相邻节点的颜色均不相同,所需最少颜色数对应于找出调度给定的所有活动所需的人最少教室。
分析:
问题的可行解是原集的 m 个子集。子集中元素在时间上互不冲突,求min{m}因而易知最优解为最大的同时发生事件数量。
算法设计:
以链表实现区间图着色问题。链表中元素表示已使用过的教室,并记录该教室中活动结束的时间。将原有的输入按照开始时间升序排列,开始时间相同,则结束时间早的在前。对于每一活动,如果当前教室中的活动结束时间 time End与 当前活动开始时间 time Star 满足 time End <= time Star 。则表示该教室中活动与当前活动兼容,可安排在该教室中。如果所有教室与当前活动都不兼容,则新使
用一间教室。
对于区间图着色问题,先在一个集合中放入一个点,然后把不与这个相邻的所有元素放入这个集合,对于剩下的点,重复前面的动作即可,依此循环,直至没有点可选。最后,有多少个集合就是多少种颜色,集合中的元素用相同的色渲染。
实现:
#include <stdio.h>
#include <stdlib.h>
//每个时间点;是起始时间,还是终止时间;及其对应的结束时间
typedef struct point_t
{
int time;
int is_start;
int end_time;//若is_start为1,end_time写对应的时间;若is_start为0,end_time为-1
} point;
//升序排列,若时间相同,则为终止时间的时间点排在前面
int compare(const void* a, const void* b)
{
if ((*(point*)a).time != (*(point*)b).time)
return (*(point*)a).time > (*(point*)b).time;
else
return (*(point*)a).is_start < (*(point*)b).is_start;//这里得用小于
}
void process(point* points, const int n)
{
//排序
qsort(points, n, sizeof(point), compare);
//最多n/2个教室
int classrooms[n/2];
int count = 0;
classrooms[count++] = points[0].end_time;
printf("[%d, %d)在教室%d\n", points[0].time, points[0].end_time, count);
int i;
int j;
for (i = 1; i < n; i++)
{
if (points[i].is_start == 1)
{
for (j = 0; j < count; j++)
{
if (classrooms[j] <= points[i].time)
{
classrooms[j] = points[i].end_time;
printf("[%d, %d)在教室%d\n", points[i].time, points[i].end_time, j+1);
break;
}
}
if (j == count)
{
classrooms[count++] = points[i].end_time;
printf("[%d, %d)在教室%d\n", points[i].time, points[i].end_time, count);
}
}
}
printf("总共需要%d个教室.\n", count);
}
int main()
{
int rows;
scanf("%d", &rows);
//2*rows个点
point* points = (point*)malloc(2*rows*sizeof(point));
//point p;
int n = rows;
//point p;
int start_time;
int end_time;
while (rows--)
{
int id = n - rows - 1;
scanf("%d%d", &start_time, &end_time);
point p1;
p1.is_start = 1;
p1.time = start_time;
p1.end_time = end_time;
points[2*id] = p1;
point p2;
p2.is_start = 0;
p2.time = end_time;
p2.end_time = -1;
points[2*id + 1] = p2;
}
process(points, 2*n);
free(points);
return 0;
}
PS:变形后不能再使用原题目中得方法,即在第一个教室中选取一个最大兼容子集,,从原集中移除子集中元素,再在第二个教室中选取最大兼容子集,直到没有活动可以选择。
有如下一组数据:[1,4] , [2,5] , [6,7] , [4,8]
按照原题中思路,则将会使用 3 间教室,但实际上只需要 2 间( {[1,4] , [4,8]} {[2,5] , [6,7]} )
- Fence Repair POJ 3253 代表类型:霍夫曼树
在定义霍夫曼树之前,首先阐明几个相关概念:
1、路径: 树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
2、路径长度:路径上的分枝数目称作路径长度。
3、树的路径长度:从树根到每一个结点的路径长度之和。
4、结点的带权路径长度:在一棵树中,如果其结点上附带有一个权值,通常把该 结点的路径长度与该结点上的权值之积称为该结点的带权路径长度(weighted path length)
霍夫曼树:带权路径长度最小的树
首先,根据题意,若将记录分割木板长度的节点建立二叉树,那么其花费当为各个叶节点木板的长度 X 节点的深度,那么霍夫曼树的定义可知,建立已分割木板的长度为节点的霍夫曼树
构造霍夫曼树的方法
- 设给定的一组权值为{W1,W2,W3,……Wn},据此生成森林F={T1,T2,T3,……Tn},F中的没棵二叉树只有一个带权为W1的根节点(i=1,2,……n)。
- 在F中选取两棵根节点的权值最小和次小的二叉树作为左右构造一棵新的叉树,新二叉树根节点的权值为其左、右子树根节点的权值之和。
- 在F中删除这两棵最小和次小的二叉树,同时将新生成的二叉树并入森林中。
- 重复(2)(3)过程直到F中只有一棵二叉树为止。
演示程序
#include <iostream>
#include <climits>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <string>
#include <vector>
#include <queue>
#include <utility>
#include <stack>
#define re return
#define ll long long
#define Push(date) push_back(date)
#define rep(index,star,finish) for(register int index=star;index<finish;index++)
#define drep(index,finish,star) for(register int index=finish;index>=star;index--)
using namespace std;
class TreeNode{
public:
int val;
TreeNode* left;
TreeNode* right;
TreeNode():left(NULL),right(NULL){}
TreeNode(int v):val(v),left(NULL),right(NULL){}
TreeNode(int v,TreeNode* l,TreeNode* r):val(v),left(l),right(r){}
TreeNode(const TreeNode& treenode){
val=treenode.val;
left=treenode.left;
right=treenode.right;
}
};
class binTree{
private:
TreeNode* root;
int dep;
public:
binTree():root(NULL),dep(0){}
binTree(TreeNode* r):root(r){}
binTree(const binTree& tree){
root=tree.root;
dep=tree.dep;
}
TreeNode* getRoot(){
return root;
}
void make(TreeNode* p){
int store;
cin>>store;
if(!store)
return;
p=new TreeNode(store);
make(p->left);
make(p->right);
}
void show(TreeNode* p){
if(!p)
return;
cout<<"In this node store inf:"<<p->val<<endl;
show(p->left);
show(p->right);
}
void show(){
queue<TreeNode*> Q;
vector<int> store;
TreeNode* now;
int nowNum=1,nextNum=0;
Q.push(root);
while(!Q.empty()){
now=Q.front();
Q.pop();
store.push_back(now->val);
nowNum--;
if(now->left){
Q.push(now->left);
nextNum++;
}
if(now->right){
Q.push(now->right);
nextNum++;
}
if(nowNum==0){
for(int i=0;i<store.size();i++)
cout<<store[i]<<" ";
cout<<endl;
nowNum=nextNum;
nextNum=0;
store.clear();
}
}
}
};
class cmp{
public:
bool operator()(TreeNode* a,TreeNode* b){
return a->val > b->val;
}
};
class huffman{
private:
TreeNode* root;
priority_queue<TreeNode*,vector<TreeNode*>,cmp> Q;
public:
huffman():root(NULL){}
~huffman(){}
TreeNode* getRoot(){return root;}
void setRoot(TreeNode* r){root=r;}
void make();
void show();
void show(TreeNode* p);
};
void huffman::make(){
int store;
cout<<"Please enter the data(end with Ctrl+z):";
while(cin>>store){
Q.push(new TreeNode(store));
cout<<"Please enter the data:";
}
TreeNode* left;
TreeNode* right;
while(Q.size()>1){
left=Q.top();
Q.pop();
right=Q.top();
Q.pop();
Q.push(new TreeNode(left->val+right->val,left,right));
}
//root=Q.top();
setRoot(Q.top());
}
void huffman::show(){
queue<TreeNode*> Q;
vector<int> store;
TreeNode* now;
int nowNum=1,nextNum=0;
Q.push(root);
while(!Q.empty()){
now=Q.front();
Q.pop();
store.push_back(now->val);
nowNum--;
if(now->left){
Q.push(now->left);
nextNum++;
}
if(now->right){
Q.push(now->right);
nextNum++;
}
if(nowNum==0){
for(int i=0;i<store.size();i++)
cout<<store[i]<<" ";
cout<<endl;
nowNum=nextNum;
nextNum=0;
store.clear();
}
}
}
void huffman::show(TreeNode* p){
if(!p)
return;
cout<<"In this node store:"<<p->val<<endl;
show(p->left);
show(p->right);
}
void visual(TreeNode* root);
inline void show(string s,int num);
int main(){
huffman tree;
tree.make();
cout<<endl;
visual(tree.getRoot());
return 0;
}
void visual(TreeNode* root){
queue<TreeNode*> Q;
vector<int> storeNode,storePos;
vector<vector<int> > node;
vector<vector<int> >pos;
pos.push_back(vector<int>(1,0));
TreeNode* now;
int add=INT_MAX;
int dep=0,n=0;
int nowNum=1,nextNum=0;
Q.push(root);
while(!Q.empty()){
now=Q.front();
Q.pop();
storeNode.push_back(now->val);
int nowPos=pos[dep][n];
nowNum--;
if(now->left){
Q.push(now->left);
storePos.push_back(nowPos-1);
add=min(add,nowPos-1);
nextNum++;
}
if(now->right){
Q.push(now->right);
storePos.push_back(nowPos+1);
nextNum++;
}
if(nowNum==0){
node.push_back(storeNode);
pos.push_back(storePos);
nowNum=nextNum;
nextNum=0;
dep++;
storeNode.clear();
storePos.clear();
}
}
add=(-1)*add;
rep(i,0,pos.size()-1){
rep(c,0,pos[i][0]+add)
cout<<" ";
cout<<node[i][0];
rep(j,1,pos[i].size()){
int diff=pos[i][j]-pos[i][j-1];
if(diff<=0){
cout<<" "<<node[i][j];
}else{
rep(c,0,diff)
cout<<" ";
cout<<node[i][j];
}
}
cout<<endl;
}
}
inline void show(string s,int num){
cout<<s<<":"<<num<<endl;
}
- 树的节点分裂问题(Tree Vertex Splitting Problem)
PS;这个问题在中文搜索引擎中基本搜不到,只在bing 里找到过几篇贪心算法的说明论文里有提到,应用性似乎不强。所以对这部分不做详细证明,只了解贪心在图论中的使用方式。
描述:
设T=(V,E,w) 表示一棵带权有向树,V是指的节点集,E是边集,w是指这些边的权。对于任意不在E中的边,它的边权没有定义。对于树上的任意一条路径P,其延迟(delay),d§ 定义为路径上的权的和。整棵树的延迟记为 d(T),定义为最大的路径延迟。T/X是指将x中的每个节点 u 分裂为两个节点 uⁱ 和 u⁰ 。将所有从 u 出发的边改为从u⁰出发,所有从 u 进入的边改为从 uⁱ 进入。
TVSP问题就是要寻找V的一个最小的子集X,使得 d(T/X) <= δ,其中δ为一个容错极限。
分析:
对于该问题,一个显而易见的方法是计算出V所有子集的d(T/X),共有2ˡ ͮ ˡ个子集。可以采用贪心思想来优化这个算法,进行如下的算法设计:
计算任意节点u ϵ V 到它的子树上任意节点的最大延迟,d(u)。如果存在一个邻接点v,使得 d(u)+w(v,u) > δ,则将 u 分裂,并且设 d(u) 为0。
所以,我们按照从叶节点到根节点的顺序计算。初始,叶节点的延迟为0。只有当一个节点的所有子节点的延迟都计算完毕,才计算当前节点的延迟。对于任意节点u,设C(u)为其所有子节点的集合,则可用如下关系式计算d(u):
d(u)=max( d(v) + w(u,v) ) v ϵ C(u)
完全照抄的伪代码:
void TVS(tree T,Type delta){
if(T!=NULL){
d[T]=0;
for(T 子节点 v){
TVS(v,delta);
d[T]=max(d[T],d[v]+w(T,v));
}
if(T 不是根节点 && d[T]+w(parent(T),T)> delta){
cout<<T<<endl;
d[T]=0;
}
}
}
C++对二叉树的实现:
数组tree[ ]存储树的信息,weight[i]存储节点i的入边,节点i的延迟保存在d[i]中。
d[ ]初始为0,数组tree[ ] 和 weight[ ]中对应不存在的节点的值都为0
int tree[SIZE];
Type weight[SIZE],d[SIZE];
void TVS(int i,Type delta){
if(tree[i])
if(2i > N)
d[i]=0;
else{
TVS(2i,delta);
d[i]=max(d[i],d[2i]+weight[2i]);
if(2i+1 <= N){
TVS(2i+1,delta);
d[i]=max(d[i],d[2i+1]+weight[2i+1]);
}
}
if(tree[i]!=1 && d[i]+weight[i]>delta){
cout<<tree[i]<<endl;
d[i]=0;
}
}
- 背包问题
描述:
假设有n个物品和1个背包,物品i的重量为wᵢ,背包的容量为m。如果物品i的一部分xᵢ,0<=xᵢ<=1放入了背包,就可以获得pᵢ xᵢ的价值。求背包中能容纳的最大价值。
分析:
显而易见的,我们可以首先得出两个贪心选择:
1、每次选择当前价值最大的物品加入背包
2、每次选择占用空间最小的物品加入背包
这两种方法分别对应着获得 最大价值增幅 和 最多操作次数。但在选取第一种贪心策略时,价值增加虽然最快,但是操作次数减小;第二种方法时,操作次数虽然增加,但每一步的价值不一定最高。显而易见的,这两种贪心策略是不对的。
因此,我们要在 体积的增长 和 价值的增长 之间寻找一个平衡:在每一步中,选择单位价值最大的物品放入背包。
实现:
void GreedyKnapsack(float m,int n){
/*
p[1:n] 和 w[1:n] 分存放n个物品的价值和重量
且满足 p[i]/w[i] >=p[i+1]/w[i+1]
m是背包的大小,x[1;n]存放解
*/
int i;
for(i=1;i<=n;i++)
x[i]=0.0;
float U=m;
for(i=1;i<=n;i++){
if(w[i]>U)
break;
x[i]=1.0;
U-=w[i];
}
if(i<=n)
s[i]=U/w[i];
}
- 有期限的工作序列化
描述:
在一个集合中,有n个工作需要完成。工作i有一个截至期限dᵢ >= 0和一个完成价值pᵢ > 0,只要在截至期限之前完成,就可以从工作i 获得 pᵢ 的价值。每一个工作需要在机器上使用一个单位时间才能完成,只有一台机器可以用来执行这些工作。求一个子集,,使得能完成这个集合中所有的工作 且 获得最大价值。
分析:
根据题意,执行贪心算法后的目标是最大价值 且 工作间相兼容。在这个目标下
我们优先选择价值最大工作,并在其并入解集时判断是否兼容。
伪代码:
GreedyJob(int d[],set J,int n){
J={1};
for(int i=2;i<=n;i++)
if(所有J U {i}中的工作都能完成)
J= J U {i};
}
C++实现:
int JS(int d[],int J[],int n){
//初始化
d[0]=J[0]=0;
//包含第一个工作
J[1]=1;
int k=1;
for(int i=2;i<=n;i++){
int r=k;
//找到要插入i的位置
while(d[J[r]]>d[i] && d[J[r]]!=r )
r--;
if(d[J[r]]<=d[i] && d[i]>r ){
//插入i
for(int q=k;q>=r+1;q--)
J[q+1]=J[q];
J[r+1]=i;
k++;
}
}
}