0x06算法设计与分析复习(二):算法设计策略-贪心法2

参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-贪心法

带时限的作业排序

问题描述

带时限得到作业排序问题:设有一个单机系统、无其他资源限制且每一个作业运行时间相等,假定每一个作业运行1个单位时间。现在有n个作业,每一个作业都有一个截止期限\(d_i>0,d_i\)为整数。如果作业能够在截止期限之内完成,可获得\(P_i>0\)的收益。问题要求得到一种作业调度方案,该方案给出作业的一个子集和该作业自己的一种排列,使得若按照这种排列次序调度作业运行,该子集中的每一个作业都能如期完成,并且能够获得最大收益。

贪心法求解

设n个作业以编号\(0\sim n-1\)标识,每一个作业有唯一的作业编号,\(I={0,1,\cdots,n-1}\)是n个输入作业的集合。带时限的作业排序问题的解是\(I\)的一个子集\(X\),可表示成一个n-元组:\(X=(x_0,x_1,\cdots,x_{r-1}),0<r\leq n\)。每一个\(x_i(0\leq x_i\leq n-1)\)是一个作业的编号。显然解向量X中不应包含相同的分量,即当\(i\neq j\)时,必有\(x_i\neq x_j\)

如果存在某种排列次序,使得按该次序调度执行\(X\)中作业,\(X\)中所有的作业都能在子集的截止期限前完成,\(X\)为问题的一个可行解,可行解中的收益最大的解是最优解。

  • 最优量度标准:选择一个作业加入到部分解向量中,在不违反截止时限的前提下,使得至少就当前而言,已选入部分解向量中的那部分作业的收益最大。为满足这一量度标准,只需先将输入作业集合I中的作业按收益的非增次序排列,即\(p_0\geq p_1\geq \cdots\geq p_{n-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};
  }
}

算法正确性

定理:按上述最优量度标准设计的求解带时限作业排序的贪心算法对于带时限作业排序问题将得到最优解。

可行性判定

定理\(X=(x_0,x_1,\cdots,x_k)\)是k个作业的集合,\(\alpha = (\alpha_0,\alpha_1,\cdots,\alpha_k)\)\(X\)的一种特定排列,它使得\(d_{\alpha_0}\leq d_{\alpha_1}\leq \cdots \leq d_{\alpha_k}\),其中,\(d_{\alpha_j}\)是作业\(\alpha_j\)的时限。\(X\)是一个可行解当且仅当\(X\)中的作业能够按照\(\alpha\)次序调度而不会有作业超期。

作业排序贪心算法

可行解判定方法:对任意一个部分解作业子集\(X=(x_0,x_1,\cdots,x_k)\),使\(X\)中的作业按时限的非递减次序排列,设\(\alpha = (\alpha_0,\alpha_1,\cdots,\alpha_k),d_{\alpha_0}\leq d_{\alpha_1}\leq \cdots \leq d_{\alpha_k}\)\(X\)这样的排列,为了判定\(\alpha\)是否为可行的调度方案,只需对每个作业\(\alpha_j\)判断\(d_{\alpha_j}>j+1\)是否成立。

设作业已按照收益非增次序排列,即\(p_0\geq p_1\geq \cdots \geq p_{n-1}]\)。贪心算法按收益从大到小次序考察作业。初始时,\(x[0]=0,x=(x[0])\)只有一个分量,表示部分解向量x中当前已加入收益最大的作业。在一般情况下,设算法考察作业\(j\)\(x=(x[0],x[1],\cdots,x[k])\)为当前已入选的作业向量,且\(d[x[0]]\leq d[x[1]]\leq \cdots \leq d[x[k]]\)。由于当前解是可行的,所以\(d[x[i]]\geq i+1, 0\leq i\leq k\)

为了判断作业\(j\)是否允许添加到部分解向量中去,具体做法是:将作业\(j\)按时限的非减次序插入向量\((x[0],x[1],\cdots,x[k])\)中的某个位置,使得插入作业\(j\)后,由\(k+1\)个分量组成的部分解向量仍按时限的非减次序排列。假设作业\(j\)被插于下标\(r+1\)处。为了在位置\(r+1\)处添加作业\(j\),作业\(x[r+1],\cdots,x[k]\)在向量中的位置都要向后移一位,形成一个新的部分解向量。为了保证在添加作业\(j\)后的作业子集仍构成可行解,必须满足以下两个要求:

  • \(d[x[j]]>j+1,r+1\leq j\leq k\),否则作业\(x[r+1],\cdots,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(n^2)\)

实验:设有4个作业,每个作业的时限为\((d_0,d_1,d_2,d_3)=(2,1,2,1)\),收益为\((p_0,p_1,p_2,p_3)=(100,10,15,27)\)

#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.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)定义为:

\[\text{WPL}=\sum_{k=1}^mw_kl_k \]

式中,\(m\)是叶子结点的个数,\(w_k\)是第\(k\)个叶子结点的权,\(l_k\)是从根到该叶子结点的路径长度。

贪心法求解

贪心法是一种多步决策的算法策略,一个问题能够使用贪心法求解,除了具有贪心法问题的一般特征外,关键问题是确定最优量度标准。两路合并最佳模式问题的最优量度标准为带权外路径长度最小

两路合并最佳模式的贪心算法如下:

  1. \(W={w_0,w_1,\cdots,w_{n-1}}\)是n个有序文件的长度,以每一个权值作为根节点值,构造n颗只有根的二叉树;
  2. 选择两颗根节点权值最小的树,作为左右子树构造一颗新二叉树。新树根的权值是两颗子树根的权值之和;
  3. 重复步骤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个权值\(W=\{w_0,w_1,\cdots,w_{n-1}\}\)作为外结点的权值,构造两路合并树的贪心算法将生成一棵具有最小带权外路径长度的二叉树。

posted @ 2018-01-08 20:41  main_c  阅读(1060)  评论(0)    收藏  举报