参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-贪心法
带时限的作业排序
问题描述
带时限得到作业排序问题:设有一个单机系统、无其他资源限制且每一个作业运行时间相等,假定每一个作业运行1个单位时间。现在有n个作业,每一个作业都有一个截止期限
贪心法求解
设n个作业以编号
如果存在某种排列次序,使得按该次序调度执行
- 最优量度标准:选择一个作业加入到部分解向量中,在不违反截止时限的前提下,使得至少就当前而言,已选入部分解向量中的那部分作业的收益最大。为满足这一量度标准,只需先将输入作业集合I中的作业按收益的非增次序排列,即
p0≥p1≥⋯≥pn−1 。
//按上述最优量度标准设计的求解带时限作业排序的贪心算法
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};
}
}
算法正确性
定理:按上述最优量度标准设计的求解带时限作业排序的贪心算法对于带时限作业排序问题将得到最优解。
可行性判定
定理:
作业排序贪心算法
可行解判定方法:对任意一个部分解作业子集
设作业已按照收益非增次序排列,即
为了判断作业
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("
");
for(int i = 0; i<n;i++){
printf("d[%d]=%d ", i, d[i]);
}
printf("
");
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.000000
d[0]=2 d[1]=1 d[2]=2 d[3]=1
x[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
{
//HNode类,包含两个数据成员:指针ptr(指向二叉树的根)和weight(存放该根的权值);
//优先权队列中的元素类型
operator T()const{ return weight;}
BTNode<T> *ptr;
T weight;
};
template<class T>
BTNode<T>* CreateHfmTree(T* w, int n)
{
//类BTNode<T>为合并树结点
//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);//Append向优先权队列中添加新元素
}
//两两合并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);//Serve从优先权队列中取出具有最高优先权(即权值最小)的元素
return a.ptr;//a.ptr指向最佳合并树的根
}
算法正确性
定理:设有n个权值