动态规划之线性动归

线性规划是一类问题。目标函数为特定变量的线性函数,约束是这些变量的线性不等式(standard form)或等式(slack form),目的是求目标函数的最大值或最小值。这类动态规划是平时比较常见的一类动态规划问题。


一、钢条切割问题:


假设公司出售一段长度为i英寸的钢条的价格为Pi(i = 1, 2, …单位:美元),下面给出了价格表样例:

长度i  5 6 7 8 9 10
价格Pi  1 5 8 9 10 17 17 20 24 30

切割钢条的问题是这样的:给定一段长度为n英寸的钢条和一个价格表Pi,求切割方案,使得销售收益Rn最大。
当然,如果长度为n英寸的钢条价格Pn足够大,最优解可能就是完全不需要切割。


对于上述价格表样例,我们可以观察所有最优收益值Ri及对应的最优解方案:(钢条长度依次从1到10对应的切割方案)
钢条长度1,R1 = 1,  切割方案1 = 1(无切割)
钢条长度2,R2 = 5,  切割方案2 = 2(无切割)
钢条长度3,R3 = 8,  切割方案3 = 3(无切割)
钢条长度4,R4 = 10, 切割方案4 = 2 + 2
钢条长度5,R5 = 13, 切割方案5 = 2 + 3
钢条长度6,R6 = 17, 切割方案6 = 6(无切割)
钢条长度7,R7 = 18, 切割方案7 = 1 + 6或7 = 2 + 2 + 3
钢条长度8,R8 = 22, 切割方案8 = 2 + 6
钢条长度9,R9 = 25, 切割方案9 = 3 + 6
钢条长度10,R10 = 30,切割方案10 = 10(无切割)
更一般地,对于Rn(n >= 1),我们可以用更短的钢条的最优切割收益来描述它:Rn = max(Pn, R1 + Rn-1, R2 + Rn-2,…,Rn-1 + R1)
首先将钢条切割为长度为i和n – i两段,接着求解这两段的最优切割收益Ri和Rn – i(每种方案的最优收益为两段的最优收益之和),由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。

对于上述方案我们可以进行简化,我们将钢条从左边切割下来长度为i的一段,只对右边剩下的长度为n-i的一段进行切割(递归求解),对左边的一段则不用进行切割。即问题分解的方式为:将长度为n的钢条分解为左边开始的一段,以及剩余部分继续分解的结果。这样,不做任何切割的方案就可以描述为:第一段长度为n,收益为Pn,剩余部分长度为0,对应的收益为R0=0。于是我们可以得到公式 的简化版本:Rn=max(Pi+Rn-i)[其中a<=i<=n]

我们可以不用动态规划,直接用自顶向下递归实现:

CUT—ROD(p,n)
if n==01
   return 0
q=-oo
for i= 1 to n
   q=max(q,p[i]+CUT—ROD(p,n-i))
return q

上述程序的运行时间是指数级的。

我们发现,朴素递归算法之所以效率低下,是因为它反复求解相同的子问题,因此,动态规划的方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此问题的解,只需查找保存的结果,而不必重新计算。这是典型的空间换时间。动态规划有两种等价的实现方法:

1:带备忘的自顶向下法:
按照自然的递归形式编写过程,但是过程会保存每个子问题的解,当需要子问题的解时,就先检查是否已经保存过此解。保存过就查表返回,否
则计算子问题解。

MEMOIZED-CUT—ROD(p,n)  //长度为n的钢条    
 let r[0..n]be a new array   //辅助数组r[n]
 for i=0 to n
 r[i]=-oo
 return MEMOIZED-CUT—AUX(p,n,r)

MEMOIZED-CUT—AUX(p,n,r)
 if r[n]>=0
    return r[n]
 if n==0    //钢条长度为0,收益为0
    q=0
 else q=-oo
for i=1 to n
    q=max(q,p[i]+MEMOIZED-CUT—AUX(p,n-i,r))   //第一段钢条长度为i,价格为p[i](p[i]是价格表),剩下的长度为n-i,最大收益为r[n-i]。长度为i的钢条的最大收益为r[i]
 r[n]=q
 return q

 
2:自底向上法:
这种方法一般需要恰当的定义子问题的“规模”的概念,使得任何子问题都只依赖于更小的子问题的求解。因此我们可以将子问题按规模进行排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题都只求解一次,当求解这个子问题时,它所依赖的所有前提子问题都已经求解完成。

BOTTOM-UP-CUT—ROD(p,n)
 let r[0..n]be a new array
 r[0]=0
 for j=1 to n	//钢条总长度为j
q=-oo
for i=1 to j   //第一段长度为i
q=max(q,p[i]+r[j-i])
r[j]=q	//长度为j的钢条的最大收益为r[j]
 return r[n] 

以上两种方法均只返回了最优解的收益值,但并没有返回解本身(一个长度列表,给出切割后每段钢条的长度)。

EXTENDED-BOTTOM-UP-CUT—ROD(p,n)
 let r[0..n] and s[0..n] be new array //s保存第一段钢条的切割长度
 r[0]=0
 for j=1 to n  //钢条总长度
    q=-oo
 for i=1 to j //第一段钢条的切割长度
    if q < p[i]+r[j-i]
       q=p[i]+r[j-i]
 s[j]=i //钢条长度为j时,第一段钢条的切割长度
 r[j]=q
 return r and s

PRINT-CUT—ROD-SOLUTION(p,n)
 (r,s)=EXTENDED-BOTTOM-UP-CUT—ROD(p,n)
 while n>0
    print s[n]
 n=n-s[n]

代码实现:请移步:GitHub

结:

       由以上钢条切割问题我们发现,动态规划有时并不是必须的,但是使用常规方法效率极其低下。而动态规划之所以能提高程序的效率,是因为我们避免了重复子问题的求解(两种方法:一种使用备忘;一种合适的执行顺序)。我们使用动态规划总是在子子问题的基础上求解子问题,再由子问题出求解最终的答案,而这一切的一切关键在于动归表达式的建立。


二、最长上升子序列长上升子序列LIS,或者 最长不下降子序列

1)问题描述

一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)。
我们的任务就是对于给定的序列,求出最长上升子序列的长度。

2)解题思路:

设 A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F [t] = 0(t = 1, 2, …, len(A))。

假设 i=1 ,此时F[1]=1;因为A[1]的前面没有元素。那么如果A[2]>A[1],显然可以得到F[2]=F[1]+1; 因为A[1],A[2]是一个上升子序列。如果A[3]<=A[2]且A[3]>=A[1],那么F[3]为2;因为它可以和A[1]组成上升子序列。同理,若A[3]>A[3-1],则F[3]=max{1,F[2]+1}=3。由此我们可以得到递推关系式. 

动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, …, t – 1, 且A[j] < A[t])。 【重要:主要就是这个方程】


3)代码实现:

//A[i]为输入序列
//B[i]为序列前i个数中的最长递增序列的长度
#include <iostream>  
using namespace std;  
  
//用于保存子问题最优解的备忘录  
typedef struct    
{  
    int maxlen; //当前子问题最优解  
    int prev;   //构造该子问题所用到的下一级子问题序号(用于跟踪输出最优队列)  
}Memo;  
  
//用于递归输出Memo B中的解  
void Display(int* A, Memo* M, int i)  
{  
    if (M[i].prev == -1)  
    {  
        cout<<A[i]<<" ";  
        return;  
    }  
    Display(A, M, M[i].prev);  //递归调用,按从低到高顺序输出
    cout<<A[i]<<" ";  
}  
  
 
void GetBestQuence(int* A, int n)  
{  
    //定义备忘录 并作必要的初始化  
    Memo *B = new Memo[n];              //B[i]代表从A[0]到A[i]满足升序剔除部分元素后能得到的最多元素个数  
   
    B[0].maxlen = 1;        //由于B[i]由前向后构造 初始化最前面的子问题  (元素本身就是一个满足升序降序的序列)  
   
    for (int i=0; i<n; i++) //为前一个最优子问题序号给定一个特殊标识-1   
                            //用于在跟踪路径时终止递归或迭代(因为我们并不知道最终队列从哪里开始)  
    {  
        B[i].prev = -1;  
      
    }  
////////////////////////////////////////关键程序/////////////////////////////////////////////////////	
    for (int i=1; i<n; i++)      //构造B[n]  i为1时就是求B[1]即从A[0]到A[1]满足升序排列的最多元素个数
    {  
		B[i].maxlen = 1;
        for (int j=i-1; j>=0; j--)               //查看前面的子问题 找出满足条件的最优解 并且记录  
        {  
            if (A[j]<=A[i] && B[j].maxlen+1>B[i].maxlen)  //A[j]<=A[i]这里符号小于等于表示求出的是最长不下降子序列,若是小于则求得的是最长递增子序列
            {  
                B[i].maxlen = B[j].maxlen+1;            //跟踪当前最优解  
                B[i].prev = j;                  //跟踪构造路径  
            }  
        }  
    }  
/////////////////////////////////////////////////////////////////////////////////////////////////      
   
  
    //遍历i 得到最大的B[i]+C[i]-1(-1是因为我们在B[i]和C[i]中 均加上了A[i]这个数 因此需要减去重复的)  
    int maxQuence = 0;  //记录当前最优解  
    int MostTall;       //记录i 用于跟踪构造路径   
    for (int i=0; i<n; i++)  
    {  
        if (B[i].maxlen> maxQuence)  
        {  
            maxQuence = B[i].maxlen;  
            MostTall = i;  
        }  
    }  
      
    cout<<"最长不下降子序列: "<<maxQuence<<endl;  
      
    //B由前向后构造 因此prev指向前面的元素 需要递归输出  
    Display( A, B, MostTall);  //输出递增序列
   
    cout<<endl;  
      
    delete []B;  
    
}  
int main()  
{  
    //测试  
    int *A;  
    int n;  
    cout<<"请输入序列个数: "<<endl;  
    cin>>n;  
  
    A = new int[n];  
    cout<<"依次输入每个序列值 :"<<endl;  
    for (int i=0; i<n; i++)  
    {  
        cin>>A[i];  
    }  
	
    GetBestQuence(A, n);  
    delete []A;  
    system("pause");
    return 0;  
}

三、合唱队形问题

1)问题描述

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别

为T1,T2,…,TK,  则他们的身高满足T1<…<Ti>Ti+1>…>TK(1<=i<=K)。你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。 
 
输入格式 Input Format      

输入的第一行是一个整数N(2<=N<=100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。 

输出格式 Output Format     

输出包括得到的最优队列的同学个数以及最终同学的身高排列

2)解题思路
这个问题可以分解为对于0<=i<=n-1(n为同学数) 我们要求得满足子序列[0, i)的升序排列最优个数p加上子序列[i, n)满足降序最优个数q即p+q最大的i值。

对于升序和降序的最优解可以用动态规划来构造,由上面的最长递增子序列问题我们可以方便的构造出动归方程b[t] = max{1, b[j] + 1} (j = 1, 2, …, t – 1, 且a[j] < a[t])。
数组a[i]是第i个人的身高,b[i]是从左边第一个到a[i]的最长上升子序列,c[i]是从右边第一个到a[i]的最长上升子序列。

最后,可以得到符合合唱队的队列是b[i]+c[i]-1(a[i]被重复计算了一次),而题目要求的合唱队列是:max{b[i]+c[i]-1} 1<=i<=总人数,那么要挑出去多少人也就明白了,即人数 –  max{b[i]+c[i]-1

3)代码实现(完整代码有些长就不在这里贴了)

void GetBestQuence(int* A, int n)  
{  
    //定义备忘录 并作必要的初始化  
    Memo *B = new Memo[n];              //B[i]代表从A[0]到A[i]满足升序剔除部分元素后能得到的最多元素个数  
    Memo *C = new Memo[n];              //C[i]代表从A[i]到A[n-1]满足降序剔除部分元素后能得到的最多元素个数  
    B[0].maxlen = 1;        //由于B[i]由前向后构造 初始化最前面的子问题  (元素本身就是一个满足升序降序的序列)  
    C[n-1].maxlen = 1;      //同样C[i]由后向前构造  
    for (int i=0; i<n; i++) //为前一个最优子问题序号给定一个特殊标识-1   
                            //用于在跟踪路径时终止递归或迭代(因为我们并不知道最终队列从哪里开始)  
    {  
        B[i].prev = -1;  
        C[i].prev = -1;  
    }  
    //这里以A[i]为队形的中心!!!
    for (int i=1; i<n; i++)      //构造B[n]  i为1时就是求B[1]即从A[0]到A[1]满足升序排列的最多元素个数
    {  
        int max=1;  
        for (int j=i-1; j>=0; j--)               //查看前面的子问题 找出满足条件的最优解 并且记录  
        {  
            if (A[j]<A[i] && B[j].maxlen+1>max)  //B[j].maxlen+1>max这里一定是大于
            {  
                max = B[j].maxlen+1;            //跟踪当前最优解  
                B[i].prev = j;                  //跟踪构造路径  
            }  
        }  
        B[i].maxlen = max;                      //构造最优解  
    }  
      
    for (int i=n-1; i>=0; i--)  //C[i]是从后往前算的
    {  
        int max=1;  
        for (int j=i; j<n; j++)          //从后往前构造 这是为了后面在统筹最终解时可以直接用B[i]+C[i]-1  
                            //否则我们得到的最优解始终为B[n-1]+C[n-1]  
        {  
            if (A[j]<A[i] && C[j].maxlen+1>max)   //比当前长度更长 记录并构造  
            {  
                max = C[j].maxlen+1;  
                C[i].prev = j;  
            }  
        }  
        C[i].maxlen = max;  
    }  
  
    //遍历i 得到最大的B[i]+C[i]-1(-1是因为我们在B[i]和C[i]中 均加上了A[i]这个数 因此需要减去重复的)  
    int maxQuence = 0;  //记录当前最优解  
    int MostTall;       //记录i 用于跟踪构造路径   
    for (int i=0; i<n; i++)  
    {  
        if (B[i].maxlen+C[i].maxlen-1 > maxQuence)  
        {  
            maxQuence = B[i].maxlen+C[i].maxlen-1;  
            MostTall = i;  
        }  
    }  
      
    cout<<"最大合唱队形长度: "<<maxQuence<<endl;  
      
    //B由前向后构造 因此prev指向前面的元素 需要递归输出  
    Display( A, B, MostTall);  //输出递增序列
    //C的prev指向后面元素 直接迭代输出  
    while (C[MostTall].prev != -1)   //输出递减序列
    {  
        MostTall = C[MostTall].prev;  
        cout<<A[MostTall]<<" ";  
    }  
    cout<<endl;  
      
    delete []B;  
    delete []C;  
}  

完整代码请移步:GitHub

4)程序分析

例子8个人以及各自的身高

0 1 2 3 4 5 6 7
186 186 150 200 160 130 197 200

求B[i]当i等于3时,j=2,1,0.
j=2时更新 max = B[j].maxlen+1;           
则当j=1,0判断B[j].maxlen+1>max时不成立不用更新B[1].maxlen和B[0].maxlen

B[3]意思是从A[0]到A[3]最大的递增序列数
C[3]意思是从A[3]到A[7]最大是递减序列数

maxlen
B[0]=1, prev=-1;  186
B[1]=1, prev=-1;  186 
B[2]=1, prev=-1;  150 
B[3]=2, B[2]<B[3];  B[2].maxlen+1=2 > max=1;       prev=2;        150,200 
B[4]=2, B[2]<B[4];  B[2].maxlen+1=2 > max=1;       prev=2;        150,160
B[5]=1, prev=-1;  130
B[6]=3, B[5]<B[6];  B[5].maxlen+1=2 > max=1;     B[4]<B[6];  B[4].maxlen+1=3 > max=2;      prev=4;  150,160,197 
B[7]=4, B[6]<B[7];  B[6].maxlen+1=4 > max=1;     prev=6;    150,160,197,200

更多内容请关注:麻子来了 

    原文作者:动态规划
    原文地址: https://blog.csdn.net/sinat_22991367/article/details/51859387
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞