—【动态规划】凸多边形最优三角剖分

来源:互联网 发布:网络山炮是什么意思啊 编辑:程序博客网 时间:2024/05/01 18:38
 

0014算法笔记——【动态规划】凸多边形最优三角剖分

分类: 算法 612人阅读 评论(0) 收藏 举报
三角剖分凸多边形最优解动态规划算法笔记

     1、问题相关定义:

     (1)凸多边形的三角剖分将凸多边形分割成互不相交的三角形的弦的集合T。

    (2)最优剖分给定凸多边形P,以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分,使得该三角剖分中诸三角形上权之和为最小。

     凸多边形三角剖分如下图所示:

          2、最优子结构性质

     若凸(n+1)边形P={V0,V1……Vn}的最优三角剖分T包含三角形V0VkVn,1<=k<=n,则T的权为三个部分权之和:三角形V0VkVn的权,多边形{V0,V1……Vk}的权和多边形{Vk,Vk+1……Vn}的权之和。如下图所示:

          可以断言,由T确定的这两个子多边形的三角剖分也是最优的。因为若有{V0,V1……Vk}和{V0,V1……Vk}更小权的三角剖分,将导致T不是最优三角剖分的矛盾。因此,凸多边形的三角剖分问题具有最优子结构性质。

         3、递推关系:

     设t[i][j],1<=i<j<=n为凸多边形{Vi-1,Vi……Vj}的最优三角剖分所对应的权值函数值,即其最优值。最优剖分包含三角形Vi-1VkVj的权,子多边形{Vi-1,Vi……Vk}的权,子多边形{Vk,Vk+1……Vj}的权之和。

      因此,可得递推关系式:

     凸(n+1)边形P的最优权值为t[1][n]。

     

     程序清单如下:

[cpp] view plaincopy
  1. //3d5 凸多边形最优三角剖分  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 7;//凸多边形边数+1  
  7. int weight[][N] = {{0,2,2,3,1,4},{2,0,1,5,2,3},{2,1,0,2,1,4},{3,5,2,0,6,2},{1,2,1,6,0,1},{4,3,4,2,1,0}};//凸多边形的权  
  8.   
  9. int MinWeightTriangulation(int n,int **t,int **s);  
  10. void Traceback(int i,int j,int **s);//构造最优解  
  11. int Weight(int a,int b,int c);//权函数  
  12.   
  13. int main()  
  14. {  
  15.     int **s = new int *[N];    
  16.     int **t = new int *[N];    
  17.     for(int i=0;i<N;i++)      
  18.     {      
  19.         s[i] = new int[N];    
  20.         t[i] = new int[N];    
  21.     }   
  22.   
  23.     cout<<"此多边形的最优三角剖分值为:"<<MinWeightTriangulation(N-1,t,s)<<endl;    
  24.     cout<<"最优三角剖分结构为:"<<endl;    
  25.     Traceback(1,5,s); //s[i][j]记录了Vi-1和Vj构成三角形的第3个顶点的位置  
  26.   
  27.     return 0;  
  28. }  
  29.   
  30. int MinWeightTriangulation(int n,int **t,int **s)  
  31. {  
  32.     for(int i=1; i<=n; i++)  
  33.     {  
  34.         t[i][i] = 0;  
  35.     }  
  36.     for(int r=2; r<=n; r++) //r为当前计算的链长(子问题规模)    
  37.     {  
  38.         for(int i=1; i<=n-r+1; i++)//n-r+1为最后一个r链的前边界    
  39.         {  
  40.             int j = i+r-1;//计算前边界为r,链长为r的链的后边界    
  41.   
  42.             t[i][j] = t[i+1][j] + Weight(i-1,i,j);//将链ij划分为A(i) * ( A[i+1:j] )这里实际上就是k=i  
  43.   
  44.             s[i][j] = i;  
  45.   
  46.             for(int k=i+1; k<j; k++)  
  47.             {  
  48.                 //将链ij划分为( A[i:k] )* (A[k+1:j])     
  49.                 int u = t[i][k] + t[k+1][j] + Weight(i-1,k,j);  
  50.                 if(u<t[i][j])  
  51.                 {  
  52.                     t[i][j] = u;  
  53.                     s[i][j] = k;  
  54.                 }  
  55.             }  
  56.         }  
  57.     }  
  58.     return t[1][N-2];  
  59. }  
  60.   
  61. void Traceback(int i,int j,int **s)  
  62. {  
  63.     if(i==j) return;  
  64.     Traceback(i,s[i][j],s);  
  65.     Traceback(s[i][j]+1,j,s);  
  66.     cout<<"三角剖分顶点:V"<<i-1<<",V"<<j<<",V"<<s[i][j]<<endl;  
  67. }  
  68.   
  69. int Weight(int a,int b,int c)  
  70. {  
  71.      return weight[a][b] + weight[b][c] + weight[a][c];  
  72. }  

     程序输入如下所示:

     运行结果如图:


 

0015算法笔记——【动态规划】多边形游戏问题

分类: 算法 562人阅读 评论(0) 收藏 举报
动态规划算法笔记多边形游戏最优子结构

         1、问题描述:   

      给定N个顶点的多边形,每个顶点标有一个整数,每条边上标有+(加)或是×(乘)号,并且N条边按照顺时针

依次编号为1~N。下图给出了一个N=4个顶点的多边形。

     游戏规则 :(1) 首先,移走一条边。 (2) 然后进行下面的操作: 选中一条边E,该边有两个相邻的顶点,不妨称为V1和V2。对V1和V2顶点所标的整数按照E上所标运算符号(+或是×)进行运算,得到一个整数;用该整数标注一个新顶点,该顶点代替V1和V2 。 持续进行此操作,直到最后没有边存在,即只剩下一个顶点。该顶点的整数称为此次游戏的得分(Score)。

 

    2、问题分析:

     解决该问题可用动态规划中的最优子结构性质来解。

    设所给的多边形的顶点和边的顺时针序列为op[1],v[1],op[2],v[2],op[3],…,op[n],v[n] 其中,op[i]表示第i条边所对应的运算符,v[i]表示第i个顶点上的数值,i=1~n。

    在所给的多边形中,从顶点i(1<=i<=n)开始,长度为j(链中有j个顶点)的顺时针链p(i,j)可表示为v[i],op[i+1],…,v[i+j-1],如果这条链的最后一次合并运算在op[i+s]处发生(1<=s<=j-1),则可在op[i+s]处将链分割为两个子链p(i,s)和p(i+s,j-s)。

    设m[i,j,0]是链p(i,j)合并的最小值,而m[i,j,1]是最大值。若最优合并在op[i+s]处将p(i,j)分为两个长度小于j的子链的最大值和最小值均已计算出。即:

    a=m[i,s,0]  b=m[i,s,1]  c=m[i,s,0]  d=m[i,s,1]

   (1) 当op[i+s]=’+’时

    m[i,j,0]=a+c ;m[i,j,1]=b+d

   (2) 当op[i+s]=’*’时

    m[i,j,0]=min{ac,ad,bc,bd} ; m[i,j,1]=max{ac,ad,bc,bd}

    由于最优断开位置s有1<=s<=j-1的j-1中情况。 初始边界值为 m[i,1,0]=v[i]   1<=i<=n m[i,1,1]=v[i]   1<=i<=n

    因为多变形式封闭的,在上面的计算中,当i+s>n时,顶点i+s实际编号为(i+s)modn。按上述递推式计算出的m[i,n,1]记为游戏首次删除第i条边后得到的最大得分。

      算法具体代码如下:

[cpp] view plaincopy
  1. //3d6 多边形游戏  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. #define NMAX 100  
  7. int N,m[NMAX+1][NMAX+1][2],v[NMAX+1];   
  8. char op[NMAX+1];  
  9.   
  10. void MinMax(int n,int i,int s,int j,int &minf,int &maxf);  
  11. int PloyMax(int n,int& p);  
  12.   
  13. int main()   
  14. {    
  15.     int p;  
  16.     cout<<"请输入多边形顶点数:"<<endl;  
  17.     cin>>N;  
  18.     for(int i=1; i<=N; i++)  
  19.     {  
  20.         cout<<"请输入多边形顶点"<<i<<"数值:"<<endl;  
  21.         cin>>v[i];    
  22.         m[i][1][0]=v[i];    
  23.         m[i][1][1]=v[i];   
  24.         cout<<"请输入多边形边"<<i<<"运算符:"<<endl;  
  25.         cin>>op[i];     
  26.     }   
  27.     cout<<"多边形游戏首次删除第"<<p<<"条边,结果为:"<<PloyMax(N,p)<<endl;   
  28.     return 0;  
  29. }  
  30.   
  31. void MinMax(int n,int i,int s,int j,int &minf,int &maxf)  
  32. {   
  33.     int e[5];  
  34.     int a=m[i][s][0],b=m[i][s][1];  
  35.     int r=(i+s-1)%n+1;//多边形的实际顶点编号  
  36.     int c=m[r][j-s][0],d=m[r][j-s][1];  
  37.   
  38.     if(op[r-1]=='+')  
  39.     {     
  40.         minf=a+c;  
  41.         maxf=b+d;  
  42.     }   
  43.     else  
  44.     {     
  45.         e[1]=a*c;  
  46.         e[2]=a*d;  
  47.         e[3]=b*c;   
  48.         e[4]=d*b;    
  49.         minf=e[1];    
  50.         maxf=e[1];   
  51.   
  52.         for(int r=2;r<N;r++)   
  53.         {     
  54.             if(minf>e[r])minf=e[r];  
  55.             if(maxf<e[r])maxf=e[r];  
  56.         }  
  57.     }  
  58. }  
  59.   
  60. int PloyMax(int n,int& p)  
  61. {   
  62.     int minf,maxf;  
  63.     for(int j=2;j<=n;j++) //迭代链的长度  
  64.     {  
  65.         for(int i=1;i<=n;i++)//迭代首次删掉第i条边  
  66.         {  
  67.             for(int s=1 ;s<j;s++) //迭代断开位置  
  68.             {      
  69.                 MinMax(n,i,s,j,minf,maxf);  
  70.                 if(m[i][j][0]>minf) m[i][j][0]=minf;   
  71.                 if(m[i][j][1]<maxf) m[i][j][1]=maxf;  
  72.             }    
  73.         }  
  74.     }  
  75.   
  76.     int temp=m[1][n][1];   
  77.     p=1;  
  78.   
  79.     for(int i=2 ;i<=n; i++)    
  80.     {      
  81.         if(temp<m[i][n][1])   
  82.         {  
  83.             temp=m[i][n][1];  
  84.             p=i;  
  85.         }  
  86.     }                    
  87.     return temp;  
  88. }   

     程序运行结果如下:


 

0016算法笔记——【动态规划】图像压缩问题

分类: 算法 501人阅读 评论(0) 收藏 举报
图像压缩动态规划算法笔记最优子结构

    1、问题描述:

     在计算机中,常用像素点的灰度值序列{p1,p1,……pn}表示图像。其中整数pi,1<=i<=n,表示像素点i的灰度值。通常灰度值的范围是0~255。因此最多需要8位表示一个像素。

      压缩的原理就是把序列{p1,p1,……pn}进行设断点,将其分割成一段一段的。分段的过程就是要找出断点,让一段里面的像素的最大灰度值比较小,那么这一段像素(本来需要8位)就可以用较少的位(比如7位)来表示,从而减少存储空间

     b代表bits,l代表length,分段是,b[i]表示每段一个像素点需要的最少存储空间(少于8位才有意义),l[i]表示每段里面有多少个像素点,s[i]表示从0到i压缩为一共占多少存储空间。

     如果限制l[i]<=255,则需要8位来表示l[i]。而b[i]<=8,需要3位表示b[i]。所以每段所需的存储空间为l[i]*b[i]+11位。假设将原图像分成m段,那么需要位的存储空间。

      图像压缩问题就是要确定像素序列{p1,p1,……pn}的最优分段,使得依此分段所需的存储空间最小

     2、最优子结构性质

      设l[i],b[i],1<=i<=m是{p1,p1,……pn}的一个最优分段,则l[1],b[1]是{p1,……,pl[1]}的一个最优分段,且l[i],b[i],2<=i<=m是{pl[1]+1,……,pn}的一个最优分段。即图像压缩问题满足最优子结构性质。

     3、递推关系

      设s[i],1<=i<=n是像素序列{p1,p1,……pi}的最优分段所需的存储位数,则s[i]为前i-k个的存储位数加上后k个的存储空间。由最优子结构性质可得:

,式中

         4、构造最优解

     数组l[i],b[i]记录了最优分段所需的信息最优分段的最后一段的段长度和像素位数分别存储在l[n]和b[n]中,其前一段的段长度和像素位数存储于l[n-l[n]]和b[n-l[n]]中,依此类推,可在O(n)时间内构造最优解。

     算法具体实现代码如下:

[cpp] view plaincopy
  1. //3d7 动态规划 图像压缩问题  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 7;  
  7.   
  8. int length(int i);  
  9. void Compress(int n,int p[],int s[],int l[],int b[]);  
  10. void Tracebace(int n,int& i,int s[],int l[]);  
  11. void Output(int s[],int l[],int b[],int n);  
  12.   
  13. int main()  
  14. {  
  15.     int p[] = {0,10,12,15,255,1,2};//图像灰度数组 下标从1开始计数  
  16.     int s[N],l[N],b[N];  
  17.   
  18.     cout<<"图像的灰度序列为:"<<endl;  
  19.   
  20.     for(int i=1;i<N;i++)  
  21.     {  
  22.         cout<<p[i]<<" ";  
  23.     }  
  24.     cout<<endl;  
  25.   
  26.     Compress(N-1,p,s,l,b);  
  27.     Output(s,l,b,N-1);  
  28.     return 0;  
  29. }  
  30.   
  31. void Compress(int n,int p[],int s[],int l[],int b[])  
  32. {  
  33.     int Lmax = 256,header = 11;  
  34.     s[0] = 0;  
  35.     for(int i=1; i<=n; i++)  
  36.     {  
  37.         b[i] = length(p[i]);//计算像素点p需要的存储位数  
  38.         int bmax = b[i];  
  39.         s[i] = s[i-1] + bmax;  
  40.         l[i] = 1;  
  41.   
  42.         for(int j=2; j<=i && j<=Lmax;j++)  
  43.         {  
  44.             if(bmax<b[i-j+1])  
  45.             {  
  46.                 bmax = b[i-j+1];  
  47.             }  
  48.   
  49.             if(s[i]>s[i-j]+j*bmax)  
  50.             {  
  51.                 s[i] = s[i-j] + j*bmax;  
  52.                 l[i] = j;  
  53.             }  
  54.         }  
  55.         s[i] += header;  
  56.     }  
  57. }  
  58.   
  59. int length(int i)  
  60. {  
  61.     int k=1;  
  62.     i = i/2;  
  63.     while(i>0)  
  64.     {  
  65.         k++;  
  66.         i=i/2;  
  67.     }  
  68.     return k;  
  69. }  
  70.   
  71. void Traceback(int n,int& i,int s[],int l[])  
  72. {  
  73.     if(n==0)  
  74.         return;  
  75.     Traceback(n-l[n],i,s,l);  
  76.     s[i++]=n-l[n];//重新为s[]数组赋值,用来存储分段位置  
  77. }  
  78.   
  79. void Output(int s[],int l[],int b[],int n)  
  80. {  
  81.     //在输出s[n]存储位数后,s[]数组则被重新赋值,用来存储分段的位置  
  82.     cout<<"图像压缩后的最小空间为:"<<s[n]<<endl;  
  83.     int m = 0;  
  84.     Traceback(n,m,s,l);  
  85.     s[m] = n;  
  86.     cout<<"将原灰度序列分成"<<m<<"段序列段"<<endl;  
  87.     for(int j=1; j<=m; j++)  
  88.     {  
  89.         l[j] = l[s[j]];  
  90.         b[j] = b[s[j]];  
  91.     }  
  92.     for(int j=1; j<=m; j++)  
  93.     {  
  94.         cout<<"段长度:"<<l[j]<<",所需存储位数:"<<b[j]<<endl;  
  95.     }  
  96. }  

     算法Compress只需O(n)空间。由于在算法Compress中j的循环次数不超过256,故对每一个确定的i可在O(1)时间内完成。因此整个算法的时间复杂度为O(n)。算法Compress的执行过程可以下图表示:

      方法Output中,在输出s[n]的最小存储空间后,s[]数组被重新赋值,用来存储分段的位置,一边回溯构造最优解。程序运行结果如下:

 

0017算法笔记——【动态规划】电路布线问题

分类: 算法 484人阅读 评论(0) 收藏 举报
电路布线算法笔记动态规划最优子结构

     1、问题描述

      在一块电路板的上、下两端分别有n个接线柱。根据电路设计,要求用导线(i,π(i)) 将上端接线柱i与下端接线柱π(i)相连,如下图。其中,π(i),1≤ i ≤n,是{1,2,…,n}的一个排列。导线(I, π(i))称为该电路板上的第i条连线。对于任何1 ≤ i ≤ j ≤n,第i条连线和第j条连线相交的充要条件是π(i)> π(j).

π(i)={8,7,4,2,5,1,9,3,10,6}

         在制作电路板时,要求将这n条连线分布到若干绝缘层上。在同一层上的连线不相交。电路布线问题要确定将哪些连线安排在第一层上,使得该层上有尽可能多的连线。换句话说,该问题要求确定导线集Nets = {i,π(i),1 ≤ i ≤ n}的最大不相交子集。    

     2、最优子结构性质

     记N(i,j) = {t|(t, π(t)) ∈ Nets,t ≤ i, π(t) ≤ j }. N(i,j)的最大不相交子集为MNS(i,j)Size(i,j)=|MNS(i,j)|。

     (1)当i = 1时

    

    (2)当i >1时

    ① j <π(i)。此时,(i,π(i)) 不属于N(i,j)。故在这种情况下,N(i,j) = N(i-1,j),从而Size(i,j)=Size(i-1,j)。

    ② j ≥π(i)。此时,若(i, π(i))∈MNS(i,j),则对任意(t, π(t))∈MNS(i,j)有t < i且π(t)< π(i);否则,(t, π(t))与(i, π(i))相交。在这种情况下MNS(i,j)-{(i, π(i))}是N(i-1, π(i)-1)的最大不相交子集。否则,子集MNS(i-1, π(i)-1)∪{(i, π(i))}包含于N(i,j)是比MNS(i,j)更大的N(i,j)的不相交子集。这与MNS(i,j)的定义相矛盾。

     若(i, π(i))不属于MNS(i,j),则对任意(t, π(t))∈MNS(i,j),有t<i。从而MNS(i,j)包含于N(i-1,j),因此,Size(i,j)≤Size(i-1,j)。

     另一方面,MNS(i-1,j)包含于N(i,j),故又有Size(i,j) ≥Size(i-1,j),从而Size(i,j)= Size(i-1,j)。

     3、递推关系

     电路布线问题的最优值为Size(n,n)。由该问题的最优子结构性质可知,子问题最优值的递归关系如下:

     自底向上,先算上排接线柱只有1个,2个的最优布线,然后求上排接线柱有多个的最优布线。具体代码如下:

[cpp] view plaincopy
  1. //3d8 动态规划 电路布线问题  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 10;  
  7.   
  8. void MNS(int C[],int n,int **size);  
  9. void Traceback(int C[],int **size,int n,int Net[],int& m);  
  10.   
  11. int main()  
  12. {  
  13.     int c[] = {0,8,7,4,2,5,1,9,3,10,6};//下标从1开始  
  14.     int **size = new int *[N+1];  
  15.   
  16.     for(int i=0; i<=N; i++)  
  17.     {  
  18.         size[i] = new int[N+1];  
  19.     }  
  20.   
  21.     MNS(c,N,size);  
  22.   
  23.     cout<<"电路布线最大不相交连线数目为:"<<size[N][N]<<endl;  
  24.   
  25.     int Net[N],m;  
  26.     Traceback(c,size,N,Net,m);  
  27.   
  28.     cout<<"最大不相交连线分别为:"<<endl;  
  29.     for(int i=m-1; i>=0; i--)  
  30.     {  
  31.         cout<<"("<<Net[i]<<","<<c[Net[i]]<<") ";  
  32.     }  
  33.     cout<<endl;  
  34.     return 0;  
  35. }  
  36.   
  37. void MNS(int C[],int n,int **size)  
  38. {  
  39.     for(int j=0;j<C[1];j++)  
  40.     {  
  41.         size[1][j]=0;  
  42.     }  
  43.   
  44.     for(int j=C[1]; j<=n; j++)  
  45.     {  
  46.         size[1][j]=1;  
  47.     }  
  48.   
  49.     for(int i=2; i<n; i++)  
  50.     {  
  51.         for(int j=0; j<C[i]; j++)  
  52.         {  
  53.             size[i][j]=size[i-1][j];//当i<c[i]的情形  
  54.         }  
  55.         for(int j=C[i]; j<=n; j++)  
  56.         {  
  57.             //当j>=c[i]时,考虑(i,c[i])是否属于MNS(i,j)的两种情况  
  58.             size[i][j]=max(size[i-1][j],size[i-1][C[i]-1]+1);  
  59.         }  
  60.     }  
  61.     size[n][n]=max(size[n-1][n],size[n-1][C[n]-1]+1);  
  62. }  
  63.   
  64. void Traceback(int C[],int **size,int n,int Net[],int& m)  
  65. {  
  66.     int j=n;  
  67.     m=0;  
  68.     for(int i=n;i>1;i--)  
  69.     {  
  70.         if(size[i][j]!=size[i-1][j])//此时,(i,c[i])是最大不相交子集的一条边  
  71.         {  
  72.             Net[m++]=i;  
  73.             j=C[i]-1;//更新扩展连线柱区间  
  74.         }  
  75.     }  
  76.     if(j>=C[1])//处理i=1的情形  
  77.     {  
  78.         Net[m++]=1;  
  79.     }  
  80. }  

     算法MNS时间和空间复杂度为O(n^2)。Traceback时间复杂度为O(n)。程序运行结果如下:


0018算法笔记——【动态规划】流水作业调度问题与Johnson法则

分类: 算法 591人阅读 评论(0) 收藏 举报
流水作业调度问题动态规划Johnson算法笔记

     1、问题描述:    

     n个作业{1,2,…,n}要在由2台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工,然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi。流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少
     2、问题分析

     直观上,一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少。在一般情况下,机器M2上会有机器空闲和作业积压2种情况。设全部作业的集合为N={1,2,…,n}。S是N的作业子集。在一般情况下,机器M1开始加工S中作业时,机器M2还在加工其他作业,要等时间t后才可利用。将这种情况下完成S中作业所需的最短时间记为T(S,t)。流水作业调度问题的最优值为T(N,0)。    

     设π是所给n个流水作业的一个最优调度,它所需的加工时间为 aπ(1)+T’。其中T’是在机器M2的等待时间为bπ(1)时,安排作业π(2),…,π(n)所需的时间。

      记S=N-{π(1)},则有T’=T(S,bπ(1))。

      证明:事实上,由T的定义知T’>=T(S,bπ(1))。若T’>T(S,bπ(1)),设π’是作业集S在机器M2的等待时间为bπ(1)情况下的一个最优调度。则π(1)π'(2),…,π'(n)是N的一个调度,且该调度所需的时间为aπ(1)+T(S,bπ(1))<aπ(1)+T’。这与π是N的最优调度矛盾。故T’<=T(S,bπ(1))。从而T’=T(S,bπ(1))。这就证明了流水作业调度问题具有最优子结构的性质。

     由流水作业调度问题的最优子结构性质可知:

    

     从公式(1)可以看出,该问题类似一个排列问题,求N个作业的最优调度问题,利用其子结构性质,对集合中的每一个作业进行试调度,在所有的试调度中,取其中加工时间最短的作业做为选择方案。将问题规模缩小。公式(2)说明一般情况下,对作业集S进行调度,在M2机器上的等待时间,除了需要等该部件在M1机器上完成时间,还要冲抵一部分原来的等待时间,如果冲抵已成负值,自然仍需等待M1将作业做完,所以公式取max{t-ai,0}。

     3、动态规划法求解思路

     假设有一组作业需要在M1和M2 两台机器上进行流水作业,他们在M1和M2上的作业时间如下表:

     问题是如何安排他们的加工顺序,使得,到最后一个作业在机器M2上加工完成所需要的时间最少。也就是所有作业在两台机器全部加工完成所需的时间最少。
     思路如下:考虑如果只有一个作业的情况,肯定所需时间就是它自身需要在M1和M2 上的加工时间总和;如果有两个作业就要考虑在两种不同的加工顺序下选取最优的一种作为候选,三个作业的时会出现三种组合情况(0,(1,2)); (1,(0,2)); (2,(0,1)),拿第一种为例,它表示先加工作业0,然后再按照作业1和作业2的优化顺序加工;将三种的作业时间计算出来,取最小值,即为三个作业的优化结果,同理可对更多的作业进行排序优化。具体做法是,用类似矩阵连乘的办法,自底向上将所有能的情况计算出来,并产生一个表,供后面的计算查用,减少重复计算的工作量。

     对于j1 作业M2 的等待时间为b0,实际上在M2加工j0作业的同时,M1 并行加工j1,实际它需要等待b1-a1时间。

      2+4+(5-4)+2=9

     J0J1两个作业的加工顺序,可以看出,先加工J0J1,所用时间最短为9,将其填入表中,依此类推,即可得出最优解。

     a4+a0+a2+a1+a3+[(b4+b0+b1+b2)-(a0+a1+a2+a3)]+b3

     =1+2+3+4+6+[(7+5+2+3)-(2+4+3+6)]+1

     =16+[17-15]+1=19

     选其中加工时间短的作为候选方案;在具体计算时非最优子集不必考虑,这样可以减少计算次数。

     4、流水作业调度的Johnson法则

     设是作业集S在机器M2的等待时间为t时的任一最优调度。若在这个调度中,安排在最前面的两个作业分别是i 和j ,即π(1)=I,π(2)=j。则有动态规划递归式可得

     其中


     如果作业i和j满足min{bi,aj} ≥min{bj,ai},则称作业i和j满足Johnson不等式。如果作业i和j 不满足Johnson不等式,则交换作业i和j满足Johnson不等式。

     证明 :在作业集S中,对于机器M2 的等待时间为t的调度π,交换作业i和j 的加工顺序,得到作业集S 的另一个调度π’,它所需的加工时间为


     当作业i和j 满足Johnson 不等式 min{bi,aj} ≥min{bj,ai}时,有

从而


由此可得


因此,对任意t 有

从而,tij≤tji,由此可见,换句话说,当作业i 和j不满足Johnson 不等式时,交换它们的加工顺序后,作业i和j满足Johnson 不等式,且不增加加工时间。由此可知,对于流水作业调度问题,必存在最优调度π,使得作业π(i)和π(i+1)满足Johnson 不等式:


这样的调度π称为满足Johnson 法则的调度。进一步还可以证明,调度满足Johnson 法则当且仅当对任意i<j 有:


     由此可知,任意两个满足Johnson 法则的调度具有相同的加工时间,从而所有满足Johnson 法则的调度均为最优调度。

    5、流水作业调度问题Johnson算法

    从上面的分析可知,流水作业调度问题一定存在满足Johnson法则的最优调度,且容易由下面的算法确定:

    流水作业调度问题的Johnson算法:

    (1)令N1={i|ai<bi},N2={i|ai>=bi};

    (2)将N1中作业按ai的非减序排序;将N2中作业按bi的非增序排序;

    (3)N1中作业接N2中作业构成满足Johnson法则的最优调度。   

    Johnson算法中分类及排序的作用(验证不等式)设数组c[]为排序后的作业排列,排序结果如下:

 

     红线左侧满足 a[c[i]]<=b[c[i]] 和 a[c[i]]<=a[c[i+1]] 符合johnson 不等式,min(b[c[i]],a[c[i+1]])>=min(b[c[i+1]],a[c[i]])其调度顺序最优;
     红线右侧满足 b[c[i]]<=a[c[i]] 和 b[c[i]]>=b[c[i+1]] 符合johnson 不等式,min(b[c[i]],a[c[i+1]])>=min(b[c[i+1]],a[c[i]])其调度顺序最优;

     中间过渡部分横向比较,左侧a[c[i]]< b[c[i]] 右侧b[c[i+1]]<=a[c[i+1] ]满足min(b[c[i]],a[c[i+1]])>=min(b[c[i+1]],a[c[i]])其调度顺序也最优;

     程序具体代码如下:

[cpp] view plaincopy
  1. //3d9 动态规划 流水作业调度问题  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 5;  
  7.   
  8. class Jobtype  
  9. {  
  10.     public:  
  11.         int operator <=(Jobtype a) const  
  12.         {  
  13.             return(key<=a.key);  
  14.         }  
  15.         int key,index;  
  16.         bool job;  
  17. };  
  18.   
  19. int FlowShop(int n,int a[],int b[],int c[]);  
  20. void BubbleSort(Jobtype *d,int n);//本例采用冒泡排序  
  21.   
  22. int main()  
  23. {  
  24.     int a[] = {2,4,3,6,1};  
  25.     int b[] = {5,2,3,1,7};  
  26.     int c[N];  
  27.   
  28.     int minTime =  FlowShop(N,a,b,c);  
  29.   
  30.     cout<<"作业在机器1上的运行时间为:"<<endl;  
  31.     for(int i=0; i<N; i++)  
  32.     {  
  33.         cout<<a[i]<<" ";  
  34.     }  
  35.     cout<<endl;  
  36.     cout<<"作业在机器2上的运行时间为:"<<endl;  
  37.     for(int i=0; i<N; i++)  
  38.     {  
  39.         cout<<b[i]<<" ";  
  40.     }  
  41.     cout<<endl;  
  42.   
  43.     cout<<"完成作业的最短时间为:"<<minTime<<endl;  
  44.     cout<<"编号从0开始,作业调度的顺序为:"<<endl;  
  45.     for(int i=0; i<N; i++)  
  46.     {  
  47.         cout<<c[i]<<" ";  
  48.     }  
  49.     cout<<endl;  
  50.     return 0;  
  51. }  
  52.   
  53. int FlowShop(int n,int a[],int b[],int c[])  
  54. {  
  55.     Jobtype *d = new Jobtype[n];  
  56.     for(int i=0; i<n; i++)  
  57.     {  
  58.         d[i].key = a[i]>b[i]?b[i]:a[i];//按Johnson法则分别取对应的b[i]或a[i]值作为关键字  
  59.         d[i].job = a[i]<=b[i];//给符合条件a[i]<b[i]的放入到N1子集标记为true  
  60.         d[i].index = i;  
  61.     }  
  62.   
  63.     BubbleSort(d,n);//对数组d按关键字升序进行排序  
  64.   
  65.     int j = 0,k = n-1;  
  66.   
  67.     for(int i=0; i<n; i++)  
  68.     {  
  69.         if(d[i].job)  
  70.         {  
  71.             c[j++] = d[i].index;//将排过序的数组d,取其中作业序号属于N1的从前面进入  
  72.         }  
  73.         else  
  74.         {  
  75.             c[k--] = d[i].index;//属于N2的从后面进入,从而实现N1的非减序排序,N2的非增序排序  
  76.         }  
  77.     }  
  78.   
  79.     j = a[c[0]];  
  80.     k = j+b[c[0]];  
  81.     for(int i=1; i<n; i++)  
  82.     {  
  83.         j += a[c[i]];//M1在执行c[i]作业的同时,M2在执行c[i-1]号作业,最短执行时间取决于M1与M2谁后执行完  
  84.         k = j<k?k+b[c[i]]:j+b[c[i]];//计算最优加工时间  
  85.     }  
  86.   
  87.     delete d;  
  88.     return k;  
  89. }  
  90.   
  91. //冒泡排序  
  92. void BubbleSort(Jobtype *d,int n)  
  93. {  
  94.     int i,j,flag;   
  95.     Jobtype temp;  
  96.   
  97.     for(i=0;i<n;i++){    
  98.         flag = 0;    
  99.         for(j=n-1;j>i;j--){    
  100.             //如果前一个数大于后一个数,则交换    
  101.             if(d[j]<=d[j-1]){    
  102.                 temp = d[j];    
  103.                 d[j] = d[j-1];    
  104.                 d[j-1] = temp;    
  105.                 flag = 1;    
  106.             }    
  107.         }    
  108.         //如果本次排序没有进行一次交换,则break,减少了执行之间。    
  109.         if(flag == 0){    
  110.             break;    
  111.         }    
  112.     }  
  113. }  

运行结果如下:

 

0019算法笔记——【动态规划】0-1背包问题

分类: 算法 781人阅读 评论(0) 收藏 举报
背包问题动态规划算法笔记最优子结构跳跃点

     1、问题描述

     给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问:应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

     形式化描述:给定c >0, wi >0, vi >0 , 1≤i≤n.要求找一n元向量(x1,x2,…,xn,), xi∈{0,1}, ∋ ∑ wi xi≤c,且∑ vi xi达最大.即一个特殊的整数规划问题。

       2、最优性原理

     设(y1,y2,…,yn)是 (3.4.1)的一个最优解.则(y2,…,yn)是下面相应子问题的一个最优解:

     证明:使用反证法。若不然,设(z2,z3,…,zn)是上述子问题的一个最优解,而(y2,y3,…,yn)不是它的最优解。显然有
                                    ∑vizi > ∑viyi   (i=2,…,n)
     且                           w1y1+ ∑wizi<= c
     因此                       v1y1+ ∑vizi (i=2,…,n) > ∑ viyi, (i=1,…,n) 
     说明(y1,z2, z3,…,zn)是(3.4.1)0-1背包问题的一个更优解,导出(y1,y2,…,yn)不是背包问题的最优解,矛盾。

       3、递推关系

    设所给0-1背包问题的子问题

     

     的最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值。由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式:

     注:(3.4.3)式此时背包容量为j,可选择物品为i。此时在对xi作出决策之后,问题处于两种状态之一:
    (1)背包剩余容量是j,没产生任何效益;
    (2)剩余容量j-wi,效益值增长了vi ;
     算法具体代码如下:

[cpp] view plaincopy
  1. //3d10-1 动态规划 背包问题  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 4;  
  7.   
  8. void Knapsack(int v[],int w[],int c,int n,int m[][10]);  
  9. void Traceback(int m[][10],int w[],int c,int n,int x[]);  
  10.   
  11. int main()  
  12. {  
  13.     int c=8;  
  14.     int v[]={0,2,1,4,3},w[]={0,1,4,2,3};//下标从1开始  
  15.     int x[N+1];  
  16.     int m[10][10];  
  17.   
  18.     cout<<"待装物品重量分别为:"<<endl;  
  19.     for(int i=1; i<=N; i++)  
  20.     {  
  21.         cout<<w[i]<<" ";  
  22.     }  
  23.     cout<<endl;  
  24.   
  25.     cout<<"待装物品价值分别为:"<<endl;  
  26.     for(int i=1; i<=N; i++)  
  27.     {  
  28.         cout<<v[i]<<" ";  
  29.     }  
  30.     cout<<endl;  
  31.   
  32.     Knapsack(v,w,c,N,m);  
  33.   
  34.     cout<<"背包能装的最大价值为:"<<m[1][c]<<endl;  
  35.   
  36.     Traceback(m,w,c,N,x);  
  37.     cout<<"背包装下的物品编号为:"<<endl;  
  38.     for(int i=1; i<=N; i++)  
  39.     {  
  40.         if(x[i]==1)  
  41.         {  
  42.             cout<<i<<" ";  
  43.         }  
  44.     }  
  45.     cout<<endl;  
  46.   
  47.     return 0;  
  48. }  
  49.   
  50. void Knapsack(int v[],int w[],int c,int n,int m[][10])  
  51. {  
  52.     int jMax = min(w[n]-1,c);//背包剩余容量上限 范围[0~w[n]-1]  
  53.     for(int j=0; j<=jMax;j++)  
  54.     {  
  55.         m[n][j]=0;  
  56.     }  
  57.   
  58.     for(int j=w[n]; j<=c; j++)//限制范围[w[n]~c]  
  59.     {  
  60.         m[n][j] = v[n];  
  61.     }  
  62.   
  63.     for(int i=n-1; i>1; i--)  
  64.     {  
  65.         jMax = min(w[i]-1,c);  
  66.         for(int j=0; j<=jMax; j++)//背包不同剩余容量j<=jMax<c  
  67.         {  
  68.             m[i][j] = m[i+1][j];//没产生任何效益  
  69.         }  
  70.   
  71.         for(int j=w[i]; j<=c; j++) //背包不同剩余容量j-wi >c  
  72.         {  
  73.             m[i][j] = max(m[i+1][j],m[i+1][j-w[i]]+v[i]);//效益值增长vi   
  74.         }  
  75.     }  
  76.     m[1][c] = m[2][c];  
  77.     if(c>=w[1])  
  78.     {  
  79.         m[1][c] = max(m[1][c],m[2][c-w[1]]+v[1]);  
  80.     }  
  81. }  
  82.   
  83. //x[]数组存储对应物品0-1向量,0不装入背包,1表示装入背包  
  84. void Traceback(int m[][10],int w[],int c,int n,int x[])  
  85. {  
  86.     for(int i=1; i<n; i++)  
  87.     {  
  88.         if(m[i][c] == m[i+1][c])  
  89.         {  
  90.             x[i]=0;  
  91.         }  
  92.         else  
  93.         {  
  94.             x[i]=1;  
  95.             c-=w[i];  
  96.         }  
  97.     }  
  98.     x[n]=(m[n][c])?1:0;  
  99. }  

     算法执行过程对m[][]填表及Traceback回溯过程如图所示:

      从m(i,j)的递归式容易看出,算法Knapsack需要O(nc)计算时间; Traceback需O(n)计算时间;算法总体需要O(nc)计算时间。当背包容量c很大时,算法需要的计算时间较多。例如,当c>2^n时,算法需要Ω(n2^n)计算时间。

         4、算法的改进
     由m(i,j)的递归式容易证明,在一般情况下,对每一个确定的i(1≤i≤n),函数m(i,j)是关于变量j的阶梯状单调不减函数。跳跃点是这一类函数的描述特征。在一般情况下,函数m(i,j)由其全部跳跃点唯一确定。如图所示。

     对每一个确定的i(1≤i≤n),用一个表p[i]存储函数m(i,j)的全部跳跃点。表p[i]可依计算m(i,j)的递归式递归地由表p[i+1]计算,初始时p[n+1]={(0,0)}。 
     一个例子:n=3,c=6,w={4,3,2},v={5,2,1}。

     函数m(i,j)是由函数m(i+1,j)与函数m(i+1,j-wi)+vi作max运算得到的。因此,函数m(i,j)的全部跳跃点包含于函数m(i+1,j)的跳跃点集p[i+1]与函数m(i+1,j-wi)+vi的跳跃点集q[i+1]的并集中。易知,(s,t)∈q[i+1]当且仅当wi<=s<=c且(s-wi,t-vi)∈p[i+1]。因此,容易由p[i+1]确定跳跃点集q[i+1]如下:

q[i+1]=p[i+1]⊕(wi,vi)={(j+wi,m(i,j)+vi)|(j,m(i,j))∈p[i+1]}

    另一方面,设(a,b)和(c,d)是p[i+1]q[i+1]中的2个跳跃点,则当c>=a且d<b时,(c,d)受控于(a,b),从而(c,d)不是p[i]中的跳跃点。除受控跳跃点外,p[i+1]q[i+1]中的其他跳跃点均为p[i]中的跳跃点。

    由此可见,在递归地由表p[i+1]计算表p[i]时,可先由p[i+1]计算出q[i+1],然后合并表p[i+1]和表q[i+1],并清除其中的受控跳跃点得到表p[i]。

      例:n=5,c=10,w={2,2,6,5,4},v={6,3,5,4,6}。跳跃点的计算过程如下:

    初始时p[6]={(0,0)},(w5,v5)=(4,6)。因此,q[6]=p[6]⊕(w5,v5)={(4,6)}。 p[5]={(0,0),(4,6)}。q[5]=p[5]⊕(w4,v4)={(5,4),(9,10)}。从跳跃点集p[5]与q[5]的并集p[5]q[5]={(0,0),(4,6),(5,4),(9,10)}中看到跳跃点(5,4)受控于跳跃点(4,6)。将受控跳跃点(5,4)清除后,得到

     p[4]={(0,0),(4,6),(9,10)}
     q[4]=p[4]⊕(6,5)={(6,5),(10,11)}
     p[3]={(0,0),(4,6),(9,10),(10,11)}
     q[3]=p[3]⊕(2,3)={(2,3),(6,9)}
     p[2]={(0,0),(2,3),(4,6),(6,9),(9,10),(10,11)}
     q[2]=p[2]⊕(2,6)={(2,6),(4,9),(6,12),(8,15)}
     p[1]={(0,0),(2,6),(4,9),(6,12),(8,15)}
     p[1]的最后的那个跳跃点(8,15)给出所求的最优值为m(1,c)=15。

    具体代码实现如下:

[cpp] view plaincopy
  1. //3d10-2 动态规划 背包问题 跳跃点优化  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 4;  
  7.   
  8. template<class Type>  
  9. int Knapsack(int n,Type c,Type v[],Type w[],int **p,int x[]);  
  10. template<class Type>  
  11. void Traceback(int n,Type w[],Type v[],Type **p,int *head,int x[]);  
  12.   
  13. int main()  
  14. {  
  15.     int c=8;  
  16.     int v[]={0,2,1,4,3},w[]={0,1,4,2,3};//下标从1开始  
  17.     int x[N+1];  
  18.   
  19.     int **p = new int *[50];  
  20.     for(int i=0;i<50;i++)    
  21.     {    
  22.         p[i] = new int[2];  
  23.     }   
  24.   
  25.     cout<<"待装物品重量分别为:"<<endl;  
  26.     for(int i=1; i<=N; i++)  
  27.     {  
  28.         cout<<w[i]<<" ";  
  29.     }  
  30.     cout<<endl;  
  31.   
  32.     cout<<"待装物品价值分别为:"<<endl;  
  33.     for(int i=1; i<=N; i++)  
  34.     {  
  35.         cout<<v[i]<<" ";  
  36.     }  
  37.     cout<<endl;  
  38.   
  39.     cout<<"背包能装的最大价值为:"<<Knapsack(N,c,v,w,p,x)<<endl;  
  40.   
  41.     cout<<"背包装下的物品编号为:"<<endl;  
  42.     for(int i=1; i<=N; i++)  
  43.     {  
  44.         if(x[i]==1)  
  45.         {  
  46.             cout<<i<<" ";  
  47.         }  
  48.     }  
  49.     cout<<endl;  
  50.   
  51.     for(int i=0;i<50;i++)    
  52.     {    
  53.         delete p[i];  
  54.     }   
  55.   
  56.     delete[] p;  
  57.   
  58.     return 0;  
  59. }  
  60.   
  61. template<class Type>  
  62. int Knapsack(int n,Type c,Type v[],Type w[],int **p,int x[])  
  63. {  
  64.     int *head = new int[n+2];  
  65.     head[n+1]=0;  
  66.   
  67.     p[0][0]=0;//p[][0]存储物品重量  
  68.     p[0][1]=0;//p[][1]存储物品价值,物品n的跳跃点(0,0)  
  69.   
  70.     // left 指向p[i+1]的第一个跳跃点,right指向最后一个  
  71.     //拿书上的例子来说,若计算p[3]=0;则left指向p[4]的第一跳跃点(0 0)right指向(9,10)  
  72.     int left = 0,right = 0,next = 1;//next即下一个跳跃点要存放的位置  
  73.     head[n]=1;//head[n]用来指向第n个物品第一个跳跃点的位置  
  74.   
  75.     for(int i=n; i>=1; i--)  
  76.     {  
  77.         int k = left;//k指向p[ ]中跳跃点,移动k来判断p[]与p[]+(w v)中的受控点  
  78.         for(int j=left; j<=right; j++)  
  79.         {  
  80.             if(p[j][0]+w[i]>c) break;//剩余的空间不能再装入i,退出for循环;  
  81.             Type y = p[j][0] + w[i],m = p[j][1] + v[i];//计算p[ ]+(w v)  
  82.   
  83.             //若p[k][0]较小则(p[k][0]  p[k][1])一定不是受控点,将其作为p[i]的跳跃点存储  
  84.             while(k<=right && p[k][0]<y)  
  85.             {  
  86.                 p[next][0]=p[k][0];  
  87.                 p[next++][1]=p[k++][1];  
  88.             }  
  89.   
  90.             //如果 p[k][0]==y而m<p[k][1],则(y m)为受控点不存  
  91.             if(k<=right && p[k][0]==y)  
  92.             {  
  93.                 if(m<p[k][1])//对(p[k][0]   p[k][1])进行判断  
  94.                 {  
  95.                     m=p[k][1];  
  96.                 }  
  97.                 k++;  
  98.             }  
  99.   
  100.             // 若p[k][0]>=y且m> =p[k][1],判断是不是当前i的最后一个跳跃点的受控点  
  101.             //若不是则为i的跳跃点存储  
  102.             if(m>p[next-1][1])  
  103.             {  
  104.                 p[next][0]=y;  
  105.                 p[next++][1]=m;  
  106.             }  
  107.   
  108.             //若是,则对下一个元素进行判断。  
  109.             while(k<=right && p[k][1]<=p[next-1][1])  
  110.             {  
  111.                 k++;  
  112.             }  
  113.         }  
  114.   
  115.         while(k<=right)  
  116.         {  
  117.             p[next][0]=p[k][0];  
  118.             p[next++][1]=p[k++][1];//将i+1剩下的跳跃点作为做为i的跳跃点存储  
  119.         }  
  120.   
  121.         left = right + 1;  
  122.         right = next - 1;  
  123.   
  124.         // 第i-1个物品第一个跳跃点的位置  head[0]指第0个物品第一个跳跃点的位置  
  125.         head[i-1] = next;  
  126.     }  
  127.   
  128.     Traceback(n,w,v,p,head,x);  
  129.     return p[next-1][1];  
  130. }  
  131.   
  132. //x[]数组存储对应物品0-1向量,0不装入背包,1表示装入背包  
  133. template<class Type>  
  134. void Traceback(int n,Type w[],Type v[],Type **p,int *head,int x[])  
  135. {  
  136.     //初始化j,m为最后一个跳跃点对应的第0列及第1列  
  137.     //如上例求出的 最后一个跳跃点为(8 15)j=8,m=15  
  138.     Type j = p[head[0]-1][0],m=p[head[0]-1][1];  
  139.     for(int i=1; i<=n; i++)  
  140.     {  
  141.         x[i]=0;// 初始化数组;  
  142.         for(int k=head[i+1]; k<=head[i]-1;k++)// 初始k指向p[2]的第一个跳跃点(0 0)  
  143.         {  
  144.             //判断物品i是否装入,如上例与跳跃点(6 9)相加等于(8 15)所以1装入  
  145.             if(p[k][0]+w[i]==j && p[k][1]+v[i]==m)  
  146.             {  
  147.                 x[i]=1;//物品i被装入,则x[i]置1  
  148.                 j=p[k][0];// j和m值置为满足if条件的跳跃点对应的值  
  149.                 m=p[k][1];// 如上例j=6,m=9  
  150.                 break;//再接着判断下一个物品  
  151.             }  
  152.         }  
  153.     }  
  154. }  

     上述算法的主要计算量在于计算跳跃点集p[i](1≤i≤n)。由于q[i+1]=p[i+1]⊕(wi,vi),故计算q[i+1]需要O(|p[i+1]|)计算时间。合并p[i+1]和q[i+1]并清除受控跳跃点也需要O(|p[i+1]|)计算时间。从跳跃点集p[i]的定义可以看出,p[i]中的跳跃点相应于xi,…,xn的0/1赋值。因此,p[i]中跳跃点个数不超过2^(n-i+1)。由此可见,算法计算跳跃点集p[i]所花费的计算时间为从而,改进后算法的计算时间复杂性为O(2^n)。当所给物品的重量wi(1≤i≤n)是整数时,|p[i]|≤c+1,(1≤i≤n)。在这种情况下,改进后算法的计算时间复杂性为O(min{nc,2^n})。

    运行结果如图:

 

0020算法笔记——【动态规划】最优二叉搜索树问题

分类: 算法 356人阅读 评论(0) 收藏 举报
最优二叉搜索树算法笔记最小平均路长四边形不等式动态规划

      1、问题描速:  

     设 S={x1, x2, ···, xn} 是一个有序集合,且x1, x2, ···, xn表示有序集合的二叉搜索树利用二叉树的顶点存储有序集中的元素,而且具有性质:存储于每个顶点中的元素x 大于其左子树中任一个顶点中存储的元素,小于其右子树中任意顶点中存储的元素。二叉树中的叶顶点是形如(xi, xi+1) 的开区间。在表示S的二叉搜索树中搜索一个元素x,返回的结果有两种情形:

    (1) 在二叉树的内部顶点处找到: x = xi
    (2) 在二叉树的叶顶点中确定: x∈ (xi , xi+1)

    设在情形(1)中找到元素x = xi的概率为bi;在情形(2)中确定x∈ (xi , xi+1)的概率为ai。其中约定x0= -∞ , xn+1= + ∞ ,有

    

    集合{a0,b1,a1,……bn,an}称为集合S的存取概率分布。    

   最优二叉搜索树在一个表示S的二叉树T中,设存储元素xi的结点深度为ci;叶结点(xj,xj+1)的结点深度为dj

     

    注:在检索过程中,每进行一次比较,就进入下面一层,对于成功的检索,比较的次数就是所在的层数加1。对于不成功的检索,被检索的关键码属于那个外部结点代表的可能关键码集合,比较次数就等于此外部结点的层数。对于图的内结点而言,第0层需要比较操作次数为1,第1层需要比较2次,第2层需要3次。

     p表示在二叉搜索树T中作一次搜索所需的平均比较次数。P又称为二叉搜索树T的平均路长,在一般情况下,不同的二叉搜索树的平均路长是不同的。对于有序集S及其存取概率分布(a0,b1,a1,……bn,an),在所有表示有序集S的二叉搜索树中找出一棵具有最小平均路长的二叉搜索树。    

     设Pi是对ai检索的概率。设qi是对满足ai<X<ai+1,0<=i<=n的标识符X检索的概率, (假定a0=--∞且an+1=+ ∞)。


      对于有n个关键码的集合,其关键码有n!种不同的排列,可构成的不同二叉搜索树有棵。(n个结点的不同二叉树,卡塔兰数)。如何评价这些二叉搜索树,可以用树的搜索效率来衡量。例如:标识符集{1, 2, 3}={do, if, stop}可能的二分检索树为:


     若P1=0.5, P2=0.1, P3=0.05,q0=0.15, q1=0.1, q2=0.05, q3=0.05,求每棵树的平均比较次数(成本)。     

     Pa(n)=1 × p1 + 2 × p2+3 × p3 + 1×q0 +2×q1+ 3×( q2 + q3 ) =1 × 0.5+ 2 × 0.1+3 ×0.05 + 1×0.05 +2×0.1+ 3×( 0.05 + 0.05 ) =1.5

     Pb(n)=1 × p1 + 2 × p3+3 × p2 + 1×q0 + 3×( q2 + q3 ) =1 × 0.5+ 2 × 0.05 + 3 ×0.1 + 1×0.15 +2×0.05+ 3×( 0.05 + 0.05 ) =1.6

     Pc(n)=1 × p2 + 2 × (p1 +  p3) + 2×(q0 +q1 +q2 + q3 ) =1 × 0.1+ 2 × (0.5 + 0.05) + 2×(0.15 + 0.1 + 0.05 + 0.05) =1.9

     Pd(n)=1 × p3 + 2 × p1+3 × p2 + 1 × q3+2 × q0 +3 × (q1+ q2) =1 × 0.05 + 2 × 0.5 + 3 × 0.1 + 1×0.05 + 2 × 0.15 + 3 × (0.1 + 0.05) =2.15

     Pe(n)=1 × p3 + 2 × p1+3 × p2 + 1 × q3+2 × q0 +3 × (q1 + q2) =1 × 0.05 + 2 × 0.5 + 3 × 0.1 + 1×0.05 + 2 × 0.15 + 3 × (0.1 + 0.05) =2.15

     因此,上例中的最小平均路长为Pa(n)=1.5。

     可以得出结论:结点在二叉搜索树中的层次越深,需要比较的次数就越多,因此要构造一棵最小二叉树,一般尽量把搜索概率较高的结点放在较高的层次

     2、最优子结构性质

     假设选择 k为树根,则 1, 2, …, k-1 和a0, a1, …, ak-1 都将位于左子树 L 上,其余结点 (k+1, …, n 和 ak, ak+1, …, an)位于右子树 R 上。设COST(L) 和COST(R) 分别是二分检索树T的左子树和右子树的成本。则检索树T的成本是:P(k)+ COST(L) + COST(R) + …… 。若 T 是最优的,则上式及 COST(L) 和COST(R) 必定都取最小值。

    证明:二叉搜索树T 的一棵含有顶点xi , ··· , xj和叶顶点(xi-1 , xi ) , ··· , ( xj , xj+1)的子树可以看作是有序集{ x, ··· , xj}关于全集为 { xi-1 , xj+1 }的一棵二叉搜索树(T自身可以看作是有序集) 。根据S 的存取分布概率,在子树的顶点处被搜索到的概率是:。{xi , ··· , xj}的存储概率分布为{ai-1, bi, …, bj, aj },其中,ah,bk分别是下面的条件概率:

     设Tij是有序集{xi , ··· , xj}关于存储概率分布为{ai-1, bi, …, bj, aj}的一棵最优二叉搜索树,其平均路长为pij,Tij的根顶点存储的元素xm,其左子树Tl和右子树Tr的平均路长分别为pl和pr。由于Tl和Tr中顶点深度是它们在Tij中的深度减1,所以得到:


     由于Ti是关于集合{xi , ··· , xm-1}的一棵二叉搜索树,故Pl>=Pi,m-1。若Pl>Pi,m-1,则用Ti,m-1替换Tl可得到平均路长比Tij更小的二叉搜索树。这与Tij是最优二叉搜索树矛盾。故Tl是一棵最优二叉搜索树同理可证Tr也是一棵最优二叉搜索树。因此最优二叉搜索树问题具有最优子结构性质。

     3、递推关系:

     根据最优二叉搜索树问题的最优子结构性质可建立计算pij的递归式如下:

     初始时:

     记 wi,j pi,j为m(i,j) ,则m(1,n)=w1,n p1,n=p1,n为所求的最优值。计算m(i,j)的递归式为:

    

     4、求解过程:

    1)没有内部节点时,构造T[1][0],T[2][1],T[3][2]……,T[n+1][n]

    2)构造只有1个内部结点的最优二叉搜索树T[1][1],T[2][2]…, T[n][n],可以求得m[i][i] 同时可以用一个数组存做根结点元素为:s[1][1]=1, s[2][2]=2…s[n][n]=n

    3)构造具有2个、3个、……、n个内部结点的最优二叉搜索树。

    ……

    r ( 起止下标的差)
    0   T[1][1], T[2][2]       , …,     T[n][n],
    1   T[1][2], T[2][3], …,T[n-1][n],
    2   T[1][3], T[2][4], …,T[n-2][n],
    ……
    r   T[1][r+1], T[2][r+2], …,T[i][i+r],…,T[n-r][n]
    ……
    n-1   T[1][n] 

    具体代码如下:     

[cpp] view plaincopy
  1. //3d11-1 最优二叉搜索树 动态规划  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;  
  5.   
  6. const int N = 3;  
  7.   
  8. void OptimalBinarySearchTree(double a[],double b[],int n,double **m,int **s,double **w);  
  9. void Traceback(int n,int i,int j,int **s,int f,char ch);  
  10.   
  11. int main()  
  12. {  
  13.     double a[] = {0.15,0.1,0.05,0.05};  
  14.     double b[] = {0.00,0.5,0.1,0.05};  
  15.   
  16.     cout<<"有序集的概率分布为:"<<endl;  
  17.     for(int i=0; i<N+1; i++)  
  18.     {  
  19.         cout<<"a"<<i<<"="<<a[i]<<",b"<<i<<"="<<b[i]<<endl;  
  20.     }  
  21.   
  22.     double **m = new double *[N+2];  
  23.     int **s = new int *[N+2];  
  24.     double **w =new double *[N+2];  
  25.   
  26.     for(int i=0;i<N+2;i++)    
  27.     {    
  28.         m[i] = new double[N+2];    
  29.         s[i] = new int[N+2];    
  30.         w[i] = new double[N+2];    
  31.     }   
  32.   
  33.     OptimalBinarySearchTree(a,b,N,m,s,w);  
  34.     cout<<"二叉搜索树最小平均路长为:"<<m[1][N]<<endl;  
  35.     cout<<"构造的最优二叉树为:"<<endl;  
  36.     Traceback(N,1,N,s,0,'0');  
  37.   
  38.     for(int i=0;i<N+2;i++)    
  39.     {    
  40.         delete m[i];  
  41.         delete s[i];  
  42.         delete w[i];  
  43.     }   
  44.     delete[] m;  
  45.     delete[] s;  
  46.     delete[] w;  
  47.     return 0;  
  48. }  
  49.   
  50. void OptimalBinarySearchTree(double a[],double b[],int n,double **m,int **s,double **w)  
  51. {  
  52.     //初始化构造无内部节点的情况  
  53.     for(int i=0; i<=n; i++)  
  54.     {  
  55.         w[i+1][i] = a[i];  
  56.         m[i+1][i] = 0;  
  57.     }  
  58.   
  59.     for(int r=0; r<n; r++)//r代表起止下标的差  
  60.     {  
  61.         for(int i=1; i<=n-r; i++)//i为起始元素下标  
  62.         {  
  63.             int j = i+r;//j为终止元素下标  
  64.   
  65.             //构造T[i][j] 填写w[i][j],m[i][j],s[i][j]  
  66.             //首选i作为根,其左子树为空,右子树为节点  
  67.             w[i][j]=w[i][j-1]+a[j]+b[j];  
  68.             m[i][j]=m[i+1][j];  
  69.             s[i][j]=i;  
  70.   
  71.             //不选i作为根,设k为其根,则k=i+1,……j  
  72.             //左子树为节点:i,i+1……k-1,右子树为节点:k+1,k+2,……j  
  73.             for(int k=i+1; k<=j; k++)  
  74.             {  
  75.                 double t = m[i][k-1]+m[k+1][j];  
  76.   
  77.                 if(t<m[i][j])  
  78.                 {  
  79.                     m[i][j]=t;  
  80.                     s[i][j]=k;//根节点元素  
  81.                 }  
  82.             }  
  83.             m[i][j]+=w[i][j];  
  84.         }  
  85.     }  
  86. }  
  87.   
  88. void Traceback(int n,int i,int j,int **s,int f,char ch)  
  89. {  
  90.     int k=s[i][j];  
  91.     if(k>0)  
  92.     {  
  93.         if(f==0)  
  94.         {  
  95.             //根  
  96.             cout<<"Root:"<<k<<" (i:j):("<<i<<","<<j<<")"<<endl;  
  97.         }  
  98.         else  
  99.         {  
  100.             //子树  
  101.             cout<<ch<<" of "<<f<<":"<<k<<" (i:j):("<<i<<","<<j<<")"<<endl;  
  102.         }  
  103.   
  104.         int t = k-1;  
  105.         if(t>=i && t<=n)  
  106.         {  
  107.             //回溯左子树  
  108.             Traceback(n,i,t,s,k,'L');  
  109.         }  
  110.         t=k+1;  
  111.         if(t<=j)  
  112.         {  
  113.             //回溯右子树  
  114.             Traceback(n,t,j,s,k,'R');  
  115.         }  
  116.     }  
  117. }  
        4、构造最优解:

   算法OptimalBinarySearchTree中用s[i][j]保存最优子树T(i,j)的根节点中的元素。当s[i][n]=k时,xk为所求二叉搜索树根节点元素。其左子树为T(1,k-1)。因此,i=s[1][k-1]表示T(1,k-1)的根节点元素为xi。依次类推,容易由s记录的信息在O(n)时间内构造出所求的最优二叉搜索树。

     5、复杂度分析与优化:

   算法中用到3个数组m,s和w,故所需空间复杂度为O(n^2)。算法的主要计算量在于计算。对于固定的r,它需要的计算时间O(j-i+1)=O(r+1)。因此算法所耗费的总时间为:。事实上,由《动态规划加速原理之四边形不等式》可以得到:而此状态转移方程的时间复杂度为O(n^2)。由此,对算法改进后的代码如下:

[cpp] view plaincopy
  1. //3d11-1 最优二叉搜索树 动态规划加速原理 四边形不等式  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;  
  5.   
  6. const int N = 3;  
  7.   
  8. void OptimalBinarySearchTree(double a[],double b[],int n,double **m,int **s,double **w);  
  9. void Traceback(int n,int i,int j,int **s,int f,char ch);  
  10.   
  11. int main()  
  12. {  
  13.     double a[] = {0.15,0.1,0.05,0.05};  
  14.     double b[] = {0.00,0.5,0.1,0.05};  
  15.   
  16.     cout<<"有序集的概率分布为:"<<endl;  
  17.     for(int i=0; i<N+1; i++)  
  18.     {  
  19.         cout<<"a"<<i<<"="<<a[i]<<",b"<<i<<"="<<b[i]<<endl;  
  20.     }  
  21.   
  22.     double **m = new double *[N+2];  
  23.     int **s = new int *[N+2];  
  24.     double **w =new double *[N+2];  
  25.   
  26.     for(int i=0;i<N+2;i++)    
  27.     {    
  28.         m[i] = new double[N+2];    
  29.         s[i] = new int[N+2];    
  30.         w[i] = new double[N+2];    
  31.     }   
  32.   
  33.     OptimalBinarySearchTree(a,b,N,m,s,w);  
  34.     cout<<"二叉搜索树最小平均路长为:"<<m[1][N]<<endl;  
  35.     cout<<"构造的最优二叉树为:"<<endl;  
  36.     Traceback(N,1,N,s,0,'0');  
  37.   
  38.     for(int i=0;i<N+2;i++)    
  39.     {    
  40.         delete m[i];  
  41.         delete s[i];  
  42.         delete w[i];  
  43.     }   
  44.     delete[] m;  
  45.     delete[] s;  
  46.     delete[] w;  
  47.     return 0;  
  48. }  
  49.   
  50. void OptimalBinarySearchTree(double a[],double b[],int n,double **m,int **s,double **w)  
  51. {  
  52.     //初始化构造无内部节点的情况  
  53.     for(int i=0; i<=n; i++)  
  54.     {  
  55.         w[i+1][i] = a[i];  
  56.         m[i+1][i] = 0;  
  57.         s[i+1][i] = 0;  
  58.     }  
  59.   
  60.     for(int r=0; r<n; r++)//r代表起止下标的差  
  61.     {  
  62.         for(int i=1; i<=n-r; i++)//i为起始元素下标  
  63.         {  
  64.             int j = i+r;//j为终止元素下标  
  65.             int i1 = s[i][j-1]>i?s[i][j-1]:i;  
  66.             int j1 = s[i+1][j]>i?s[i+1][j]:j;  
  67.   
  68.             //构造T[i][j] 填写w[i][j],m[i][j],s[i][j]  
  69.             //首选i作为根,其左子树为空,右子树为节点  
  70.             w[i][j]=w[i][j-1]+a[j]+b[j];  
  71.             m[i][j]=m[i][i1-1]+m[i1+1][j];  
  72.             s[i][j]=i1;  
  73.   
  74.             //不选i作为根,设k为其根,则k=i+1,……j  
  75.             //左子树为节点:i,i+1……k-1,右子树为节点:k+1,k+2,……j  
  76.             for(int k=i1+1; k<=j1; k++)  
  77.             {  
  78.                 double t = m[i][k-1]+m[k+1][j];  
  79.   
  80.                 if(t<m[i][j])  
  81.                 {  
  82.                     m[i][j]=t;  
  83.                     s[i][j]=k;//根节点元素  
  84.                 }  
  85.             }  
  86.             m[i][j]+=w[i][j];  
  87.         }  
  88.     }  
  89. }  
  90.   
  91. void Traceback(int n,int i,int j,int **s,int f,char ch)  
  92. {  
  93.     int k=s[i][j];  
  94.     if(k>0)  
  95.     {  
  96.         if(f==0)  
  97.         {  
  98.             //根  
  99.             cout<<"Root:"<<k<<" (i:j):("<<i<<","<<j<<")"<<endl;  
  100.         }  
  101.         else  
  102.         {  
  103.             //子树  
  104.             cout<<ch<<" of "<<f<<":"<<k<<" (i:j):("<<i<<","<<j<<")"<<endl;  
  105.         }  
  106.   
  107.         int t = k-1;  
  108.         if(t>=i && t<=n)  
  109.         {  
  110.             //回溯左子树  
  111.             Traceback(n,i,t,s,k,'L');  
  112.         }  
  113.         t=k+1;  
  114.         if(t<=j)  
  115.         {  
  116.             //回溯右子树  
  117.             Traceback(n,t,j,s,k,'R');  
  118.         }  
  119.     }  
  120. }  
运行结果如图:



原创粉丝点击