算法设计与分析复习(二):算法设计策略-贪心法
来源:互联网 发布:下载壁纸软件 编辑:程序博客网 时间:2024/06/06 09:43
参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-贪心法
贪心法
- 贪心法是一种求解最优化问题的算法设计策略。贪心法是通过分步决策(stepwise decision)的方法来求解问题的。贪心法在求解问题的每一步上做出某种决策,产生n-元组解的一个分量。贪心法要求根据题意,选定一种最优量度标准(optimization criterion)或贪心准则(greedy criterion),也称贪心选择性质(greedy choice property)。这种量度标准通常只考虑局部最优行。
- 在初始状态下,解向量
solution=∅ ,其中不包含任何分量。使用最优量度标准,一次选择一个分量,逐步形成解向量(x0,x1,⋯,xn−1) 。算法执行过程中生成的向量(x0,x1,⋯,xk),k<n ,称为部分解向量,也称部分向量(partial vector)。 - 在根据最有量度标准选择分量的过程中,还需要使用一个可行解判定函数。设
(x0,x1,⋯,xk−1) 是贪心算法已经生成的部分解,根据最优量度标准,算法当前选取解向量的第k(k<n) 个分量为xk ,此时需要使用可行解判定函数来判断,在添加新的分量xk 后所形成部分解(x0,x1,⋯,xk) 是否违反可行解约束条件。 - 贪心法之所以被称为是贪心的,是因为它希望每一步决策都是正确的,即要求在算法的每一步上,仅根据最有量度标准选择分量,并只需要保证形成的部分解不违反约束条件,最终得到n-元组不仅是可行解,而且必定是最优解。
- 事实上,最优量度标准一般并不从整体考虑,它只是在某种意义上的局部最优选择,每一步决策只是在当前看来是最优的。因此,贪心法不能保证对所有问题都得到整体最优解。
//贪心法框架SolutionType Greedy(SType a[], int n){ //初始时,解向量不包含任何分量 SolutionType solution=EmptySet; //多步决策,每次选择解向量的一个分量 for(int i = 0; i<n;i++){ //遵循最优量度标准选择一个分量 SType x= Select(a); //判定加入分量x后的部分解是否可行 if(Feasible(solution, x)) //形成新的部分解 solution=Union(solution, x); } return solution;//返回生成的最优解}
对于一个贪心算法,必须进一步证明该算法的每一步上所做出的选择,都必然最终导致问题的一个整体最优解。
背包问题
问题描述
背包问题:已知一个载重为M的背包和n件物品,第i件物品的重量为
- 0/1背包问题:每一件物品不能分割,只能作为整体装入背包,或者不能装入。
- 一般背包问题:物品时可以分割的,允许将其中的一部分装入背包。
用贪心法求解 0/1背包问题只能求得近似解。
一般背包问题的贪心法求解
一般背包问题:
最优量度标准:选择使单位重量收益最大的物品装入背包,即按
//背包问题的贪心算法template<class T> class Knapsack { public: //创建一个一维数组w和p,并赋初值 Knapsack(int mSize, float cap, float *wei, T *prof); //数组x为背包问题的最优解 void GreedyKnapsack(float* x); ... private: float m, *w;//m为背包载重量,w指示存储n个物品重量的数组 T* p;//p指示存储n个物品收益的数组; int n;//n为物品数量 };template<class T> vois Knapsack<T>::GreedyKnapsack(float* x) { //前置条件:w[i]已按p[i]/w[i]的非增次序排列; float u = m;//u为背包剩余载重量,初始时为m for(int i = 0;i<n;i++) x[i]=0;//对解向量初始化 for(i = 0;i<n;i++){ //按最优量度标准选择解的分量 if(w[i]>u) break; x[i]=1.0; u=u-w[i]; } if(i<n) x[i]=u/w[i]; }
定理:如果
C语言实验:设有载重M=20的背包,3件物品的重量为
#include <stdio.h>#include <stdlib.h>void GreedyKnapsack(float p[], float w[], float x[], int n, float m);void SortW(float* p, float* w, int n);int main(){ float p[3] = { 25,24,15 }, w[3] = { 18,15,10 }; float x[3] = { 0,0,0 }; float M = 20; //对w[i]进行排序,按p[i]/w[i]的非增次序排列 SortW(p, w, 3); printf("after sorting:\n"); for (int i = 0; i < 3; i++) { printf("p[%d] = %f ", i, p[i]); } printf("\n"); for (int i = 0; i < 3; i++) { printf("w[%d] = %f ", i, w[i]); } printf("\n"); GreedyKnapsack(p, w, x, 3, M); printf("The solution is:\n"); for (int i = 0; i < 3; i++) { printf("x[%d] = %f ", i, x[i]); } printf("\n"); system("pause"); return 0;}void SortW(float* p, float* w, int n){ int i = 0, j = n; float tmp = 0; for (i = 0; i < n; i++) { for (j = 0; j < n - i-1; j++) { if (p[j] / w[j] < p[j + 1] / w[j + 1]) { tmp = p[j]; p[j] = p[j + 1]; p[j + 1] = tmp; tmp = w[j]; w[j] = w[j + 1]; w[j + 1] = tmp; } } }}void GreedyKnapsack(float p[], float w[], float x[], int n, float m){ //前置条件:w[i]已按p[i]/w[i]的非增次序排列; float u = m;//u为背包剩余载重量,初始时为m int i = 0; for (i = 0; i<n; i++) x[i] = 0;//对解向量初始化 for (i = 0; i<n; i++) { //按最优量度标准选择解的分量 if (w[i]>u) break; x[i] = 1.0; u = u - w[i]; } if (i<n) x[i] = u / w[i];}
实验结果:
after sorting:p[0] = 24.000000 p[1] = 15.000000 p[2] = 25.000000w[0] = 15.000000 w[1] = 10.000000 w[2] = 18.000000The solution is:x[0] = 1.000000 x[1] = 0.500000 x[2] = 0.000000请按任意键继续. . .
带时限的作业排序
问题描述
带时限得到作业排序问题:设有一个单机系统、无其他资源限制且每一个作业运行时间相等,假定每一个作业运行1个单位时间。现在有n个作业,每一个作业都有一个截止期限
贪心法求解
设n个作业以编号
- 最优量度标准:选择一个作业加入到部分解向量中,在不违反截止时限的前提下,使得至少就当前而言,已选入部分解向量中的那部分作业的收益最大。
//带时限作业排序的贪心算法void GreedyJob(int d[], Set X, int n){ //前置条件:p0>=p1>=p2>=...>=p(n-1) X={0}; for(int i =1;i<n;i++){ //可行解判定 if(集合XU{i}中的作业都能在给定的时限内完成) X=XU{i}; }}
定理:
- 可行解判定方法:对任意一个部分解作业子集
X=(x0,x1,⋯,xk) ,使X 中的作业按时限的非递减次序排列,设α=(α0,α1,⋯,αk),dα0≤dα1≤⋯≤dαk 是X 这样的排列,为了判定α 是否为可行的调度方案,只需对每个作业αj 判断dαj>j+1 是否成立。
设作业已按照收益非增次序排列,即
为了判断作业
d[x[j]]>j+1,r+1≤j≤k ,否则作业x[r+1],⋯,x[k] 的后移将导致其中某些作业超期。d[j]>r+1 ,否则作业j 自己无法在时刻r+2 前完成。
//带时限的作业排序程序int JS(int *d, int *x, int n){ //前置条件:p0>=p1>=...>=p(n-1) int k = 0,x[0]=0; for(int j = 1; j<n;j++){ int r=k; while(r>=0 && d[x[r]]>d[j] && d[x[r]]>r+1) r--;//搜索作业j的插入位置 //若条件不满足,选下一个作业 if((r<0||d[x[r]]<=d[j]) && d[j]>r+1){ //将x[r]以后的作业后移 for(int i = k;i>=r+1;i--){ x[i+1]=x[i]; } //将作业j插入r+1处 x[r+1]=j; k++; } } return k;}
该程序最坏时间复杂度为
O(n2) 。
实验:设有4个作业,每个作业的时限为
#include <stdio.h>//带时限的作业排序算法int JS(float *p, int *d, int *x, int n);void SortP(float *p, int *d, int n);int main(){ int n = 4,k=0, d[4]={2,1,2,1}; float p[4] = {100,10,15,27}; int x[4]={0}; k = JS(p,d,x,n);//k+1表示解的个数 for(int i = 0; i<n;i++){ printf("p[%d]=%f ", i, p[i]); } printf("\n"); for(int i = 0; i<n;i++){ printf("d[%d]=%d ", i, d[i]); } printf("\n"); for(int i = 0; i<=k;i++){ printf("x[%d]=%d ", i, x[i]); } return 0;}int JS(float *p, int *d, int *x, int n){ SortP(p,d,n); //前置条件:p0>=p1>=...>=p(n-1) int k = 0; x[0]=0; for(int j = 1; j<n;j++){ int r=k; while(r>=0 && d[x[r]]>d[j] && d[x[r]]>r+1) r--;//搜索作业j的插入位置 //若条件不满足,选下一个作业 if((r<0||d[x[r]]<=d[j]) && d[j]>r+1){ //将x[r]以后的作业后移 for(int i = k;i>=r+1;i--){ x[i+1]=x[i]; } //将作业j插入r+1处 x[r+1]=j; k++; } } return k;}void SortP(float *p, int *d, int n){ float tmp = 0; int i = 0, j = 0; for(i = 0; i<n; i++){ for(j = 0; j<n-i-1; j++){ if(p[j]<p[j+1]){ tmp = p[j]; p[j] = p[j+1]; p[j+1] = tmp; tmp = d[j]; d[j] = d[j+1]; d[j+1] = tmp; } } }}
实验结果:
p[0]=100.000000 p[1]=27.000000 p[2]=15.000000 p[3]=10.000000d[0]=2 d[1]=1 d[2]=2 d[3]=1x[0]=1 x[1]=0
结果表明:运行收益为100和27的任务,先运行收益为27时限为1的任务
最佳合并模式
问题描述
合并n个有序子文件成为一个有序文件的合并过程可以有多种方式,称为合并模式。每执行一次合并需要将两个有序文件的全部记录依次从外村读入内存,还需要将合并后的新文件写入外村。在整个合并过程中,需从外存读/写的记录数最少的合并方案称为最佳合并模式(optimal merge pattern)。
可以用合并树来描述一种合并模式。两路合并排序过程中所需读/写的记录总数正是对应两路合并树的带权外路径长度,带权外路径长度是针对扩充二叉树而言的。扩充二叉树(extended binary tree)中除叶子结点外,其余结点都必须要有两个孩子。扩充二叉树的带权外路径长度(weighted external path length)定义为:
式中,
贪心法求解
贪心法是一种多步决策的算法策略,一个问题能够使用贪心法求解,除了具有贪心法问题的一般特征外,关键问题是确定最优量度标准。两路合并最佳模式问题的最优量度标准为带权外路径长度最小。
两路合并最佳模式的贪心算法如下:
- 设
W=w0,w1,⋯,wn−1 是n个有序文件的长度,以每一个权值作为根节点值,构造n颗只有根的二叉树; - 选择两颗根节点权值最小的树,作为左右子树构造一颗新二叉树。新树根的权值是两颗子树根的权值之和;
- 重复步骤2,直到合并成一棵二叉树为止。
//两路合并最佳模式的贪心算法template<class T> struct HNode { //优先权队列中的元素类型 operator T()const{ return weight;} BTNode<T> *ptr; T weight; };template<class T> BTNode<T>* CreateHfmTree(T* w, int n){ //w为一维数组保存n个权值; PrioQueen<HNode<T>> pq(2*n-1);//创建长度为2n-1的优先权队列pq BTNode<T> *p; HNode<T> a,b; //权值作为根,构造n颗只有根的二叉树; for(int i=0;i<n;i++){ //创建合并树的新结点 p=new BTNode<T>(w[i]); //对象a包含指向根的指针和根的权值 a.ptr=p; a.weight=w[i]; //将指向根的指针和根的权值进队列pq pq.Append(a); } //两两合并n-1次,将n颗树合并成一颗 for(int i=1;i<n;i++){ //从pq依次取出根权值最小的两棵树; pq.Serve(a); pq.Serve(b); a.weight += b.weight; //将取出的两颗树合并,构造一颗新的二叉树; p=new BTNode<T>(a.weight,a.ptr,b.ptr); a.ptr=p; //将指向新根的指针和根的权值进队列pq pq.Append(a); } //取出生成的最佳合并树 pq.Serve(a); return a.ptr;//a.ptr指向最佳合并树的根}
最小代价生成树
问题描述
一个无向连通图的生成树是一个极小连通子图,它包括图中全部的结点,并且尽可能少的边。遍历一个连通图得到图的一颗生成树。
一颗生成树的代价是树中各条边上的代价之和。一个网络的各个生成树中,具有最小代价的生成树称为该网络的最小代价生成树(minimum-cost spanning tree)。
贪心法求解
一个无向图的所有生成树都可看成是问题的可行解,其中代价最小的生成树就是所求的最优解,生成树的代价是问题的目标函数。
//最小代价生成树的贪心算法ESetType SpanningTree(ESetType E, int n){ //G=(V,E)为无向图,E是图G的边集,n是图中结点个数 ESetType S = EmptySet;//S为生成树上边的集合 int u,v,k=0; EType e;//e=(u,v)为一条边 //选择生成树的n-1条边 while(k<n-1 && E中尚有未检查的边){ //按照最优量度标准选择一条边 e=select(E); //判定可行性 if(S U e不包含回路){ //在生成树边集S中添加一条边 S=S U e; k++; } } return S;}
最简单的最优量度标准是:选择使得迄今为止已入选S中边的代价这和增重最小的边。对于最优量度标准的不同解释产生不同的构造最小代价生成树算法,对于上述最优量度标准有两种可能的理解,它们是普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法。
库鲁斯卡尔算法的贪心准则是:按边代价的非减次序考察E中的边,从中选择一条代价最小的边
普里姆(prim)算法
设
设无向图中结点数为n,很明显,普里姆算法的时间复杂度是
库鲁斯卡尔(Kruskal)算法
设
设无向图有n个结点和e条边,一般有
定理:设图
定理:普里姆算法和克鲁斯卡尔算法都将产生一个带权无向连通图的最小代价生成树。
单源最短路径
问题描述
单源最短路径问题是:给定带权的有向图
贪心法求解
从源点到另一个结点的任意一条路径均可看成是问题的可行解,其中长度最小的路径就是所求的最优解,路径的长度是问题的目标函数。
迪杰斯特拉(Dijkstra)提出了按路径长度的非递减次序逐一产生最短路径的算法:首先求得长度最短的一条最短路径,再求得长度次短的一条最短路径,其余类推,直到从源点到其他所有结点之间的最短路径都已被求出为止。
设
迪杰斯特拉(Dijkstra)算法
设集合S存放已经求得最短路径的终点,则
在算法执行中,一个结点
迪杰斯特拉算法用到以下数据结构:
- 一维数组d[i]中存放从源点s到结点i的当前最短路径的长度。
- 一位整型数组path[i]存放从源点s到结点i的当前最短路径上,结点i的前一个结点。因此,从源点到结点i的路径可以根据path的反向溯源来创建。
- 一维布尔数组inS,若inS[i]为true,表示结点i在S中;否则,表示i不在
V−S 中。
迪杰斯特拉算法计算过程
+ 求第一条最短路径
在初始状态下,集合S中只有一个源点s,
式中,
第一条最短路径是所有最短路径中的最短者,它必须只包含一条边
更新d和path
将结点k加入到S中,并对所有的
i∈V−S 按式d[k]=min{d[i]|i∈V−{s}} 修正,即d[j]=min{d[j],d[k]+w(k,j)}
式中,w(k,j) 是边<k,j> 的权值。求下一条最短路径
在
V−S 中,选择具有最短的当前最短路径值的结点k,满足d[k]=min{d[i]|i∈V−S} 。
算法的主要步骤如下(有向图用邻接矩阵表示):
- 初始化:创建长度为n的一维数组inS、d和path,并将每个inS[i]初始化为false,d[i]为a[v][i],如果
i≠v 且d[i]<∞ ,则path[i]=v ,否则path[i]=−1 。 - 将源点v加入集合S中:
inS[v]=true;d[v]=0 。 - 使用for循环,按长度的非递减次序,依次产生n-1条最短路径:选出最小的d[k];将结点k加入集合S中,
inS[k]=true ;使用内层for语句更新数组d和path的值,使得其始终代表当前最短路径。
上述迪杰斯特拉算法的时间复杂度为
磁带最优存储
单带最优存储
问题描述
设有n个程序编号分别为
贪心法求解
最优量度标准:计算迄今为止已选的那部分程序的D值,选择下一个程序的标准应使得该值的增加最小。
定理:设有n个程序
多带最优存储
问题描述
设有
贪心法求解
在多带情况下计算最优平均检索时间,可以先将程序按照长度的非减次序排列,即
//多带最优存储#include <iostream.h>void Store(int n, int m) { int j=0; for(int i=0;i<n;i++){ cout<<"将程序"<<i<<"加到磁带"<<j<<endl; j=(j+1)%m; } cout<<endl; }
定理:设有n个程序
贪心法的基本要素
一般来说,适用于贪心法求解的问题大都具有下面两个特征:最优量度标准和最优子结构。
最优量度标准
所谓贪心法的最优量度标准,是指可以根据该量度标准,实行多步决策进行求解,虽然在该量度意义下所做的这些选择都是局部最优的,但最终得到的解却是全局最优的。选择最优量度标准是使用贪心法求解问题的核心问题。值得注意的是,贪心算法每步做出的选择可以依赖以前做出的选择,但决不依赖将来的选择,也不依赖于子问题的解。虽然贪心算法的每一步选择也将问题简化为一个规模更小的子问题,但由于贪心算法每一步选择不依赖子问题的解,每步选择只按最优量度标准进行,因此,对于一个贪心算法,必须证明所采用的量度标准能够导致一个整体最优解。
贪心法的当前选择可能会依赖于已经做出的选择,但不依赖于尚未做出的选择和子问题,因此它的特征是自顶向下,一步一步地做出贪心决策。
最优子结构
所谓最优子结构特征是 关于问题最优解的特征。当一个问题的最优解中包含了子问题的最优解时,则称该问题具有最优子结构特性(optimal substructure)。一个可用贪心法求解的问题往往呈现最优子结构特性。
一般而言,如果一个最优化问题的解结构具有元组形式,并具有最优子结构特性,我们可以尝试选择量度标准。如果经过证明(一般是归纳法),确认该量度标准能够导致最优解,便可容易地按贪心法的算法框架设计出求解该问题的具体的贪心算法。
并非所有的具有最优子结构特性的最优化问题,都能够找到最优量度标准,此时,可以考虑采用动态规划法来求解。
小结
一个问题能够用贪心策略的条件是该问题的解是向量结构的,具有最优子结构特性,还要求能够通过分析问题获取最优量度标准。但是,按照该量度标准一次生成解的分量所形成的解是否确实是最优解仍需证明。
- 算法设计与分析复习(二):算法设计策略-贪心法
- 算法设计与分析复习(二):算法设计策略-分治法
- 算法分析与设计复习-贪心算法描述
- 贪心算法(算法分析与设计)
- 算法设计与分析复习
- 算法设计与分析之贪心法
- 算法设计与分析复习-分治法算法描述
- 算法分析与设计复习小结
- 大学算法分析与设计复习总结
- 算法设计与分析复习-归纳法代码
- 算法与设计分析作业3(贪心)
- 算法设计与分析 普通背包 贪心
- 计算机算法设计与分析——递归与分治策略(二)
- 算法设计与分析(二)分治
- 递归与分治策略(一)---算法设计与分析
- 算法分析与设计复习概要(上)
- 算法设计与分析复习(一):习题解答
- 算法设计与分析复习(一):算法和算法分析
- sql命令语句基本知识学习记录(1)
- 注释转换
- Mac 下查看二进制文件
- mysql 字段为NULL的一些操作
- 树状数组
- 算法设计与分析复习(二):算法设计策略-贪心法
- subptwo
- 2017湖湘杯pwn300的wp
- fragment 简单的例子
- 【Django】路由分配系统
- 数据结构之线性表的顺序存储结构
- Python连接mysql数据库
- struts2动态调用+Servlet过滤器+struts2拦截器
- spring+active