• 动态规划(dynamic programming)


      动态规划(dynamic programming)是一种高效的程序设计技术,一般应用与最优化问题,即当我们面临多组选择时,选择一个可行解让问题达到最优。动态规划的一个显著特点是:原问题可以划分成更小的子问题的最优化问题,而这些子问题的解往往有着重叠的部分。

      动态规划算法的解决一个问题,可以分成四个步骤:

      1)描述最优解结构,需找最优子结构

      2)递归定义最优值的解

      3)自底向上的求解问题

      4)依据计算过程,构造一个最优解

      1)与 2)是这个问题可以用动态规划解决的理论基础,3)可以看出是动态规划算法的编程实践,4)是算法输出,依赖于3)。下面分别用三个经典的动态规划案例,阐释动态规划算法的用法。

      案例一:矩阵连乘

      问题描述:给定n个矩阵{A1,A2,…,An},其中AiAi+1是可乘的,i=1,2,…,n-1。考察这n个矩阵的连乘积A1A2…An。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。A是一个p×q矩阵,B是一个q×r矩阵,则计算其乘积C=AB的标准算法中,需要进行pqr次数乘。矩阵连乘积的计算次序不同,计算量也不同

      Example:先考察3个矩阵{A1,A2,A3}连乘,设这三个矩阵的维数分别为10×100100×55×50

    • 若按((A1A2A3)方式需要的数乘次数为10×100×510×5×507500
    • 若按(A1A2A3))方式需要的数乘次数为100×5×5010×100×5075000

      解决方案:动态规划

      1)描述最优解结构,寻找最优子结构

      记AiAi+1…Aj为A[i:j],考察A[1:n]的最优计算次序问题:假设这个计算次序在k(1<=k<=n)处断开,那么A[1:k]和A[k+1:n]两个子序列的中的计算次序也是最优的。

      为证明子结构与原问题也是一个相同的最优问题,一般采用反证法:

      如果A[1:k]或者A[k+1:n]不是最优的,那么可以必然可以找到一个新的计算次序将A[1:k]或是A[k+1:n]替换,新的计算序列需要的计算次数更少,但这与A[1:n]是最有解序列矛盾。

      2)递归定义最优值

      令m[i][j]表示A[i:j]最小的计算次数,那么递归定义的表达式如下:

      3)自底向上的求解

      有了2)的递归表达式,,代码实现将会变得简单,代码如下:

    /************************************************************************/
    /* p: 输入参数,存储矩阵序列的中行列值
     * m: m[i][j], 存放A[i:j]的计算次数
       s: s[i][j], 记录A[i:j]断开的位置
    */
    /************************************************************************/
    int matrix_chain(int* p, int n, int** m, int** s)
    {
        for (int i = 0; i != n; i++)
        {
            m[i][i] = 0;
        }
    
        for (int r = 2; r <= n; r++)
        {
            for (int i = 0; i <= n-r; i++)
            {
                int j = i + r - 1;
                m[i][j] = m[i+1][j] + p[i]*p[i+1]*p[j+1];               //从i处断开
                s[i][j] = i;
                
                for (int k = i+1; k <= j; k++)
                {
                    int t = m[i][k] +m[k+1][j] + p[i]*p[k+1]*p[j+1];   //从k处断开
             if (t< m[i][j]) 
             {
               m[i][j]
    = t;
               s[i][j]
    = k;
             }
           }
         }
      }
    }

      4)构造最优解

      在第三步自底向上的求解过程中,记录了构建最优解的最优的必要形式(A[i:j]该断开的位置),构造最优解的过程如下:

    void trace_back(int** s, int i, int j)
    {
        if (i == j)
        {
            return;
        }
    
        trace_back(s, i, s[i][j]);
        trace_back(s, s[i][j] + 1, j);
    
        cout<<"A("<<i<<","<<s[i][j]<<")"<<"	";
        cout<<"Multiply	"<<"A("<<s[i][j]+1<<","<<j<<")"<<endl;
    }

      程序的主函数如下:

    int main()
    {
        const int n = 6;
        int p[] = {30, 35, 15, 5, 10, 20, 25};
        int **m, **s;
    
        m = new int*[n];
        for( int i = 0; i < n; i++)
            m[i] = new int[n];
    
        s = new int*[n];
        for(int i=0; i<n; i++)
            s[i] = new int[n];  
    
        matrix_chain(p, n, m, s);
        trace_back(s, 0, n-1);
    
        for(int i=0;i<n;i++)  
        {  
            delete []m[i];
            delete []s[i];
        }  
        delete []m;   
        delete []s;  
    
        system("pause");
        return 0;
    }

      案例二:最长公共子序列

      问题描述:子序列是指,在原序列中删除若干元素后所得的序列。公共子序列是指,给定两个序列X和Y,另一个序列Z既是X的子序列又是Y的子序列,那么Z则被称为X与Y的公共子序列。而最长公共子序列,则是求X与Y最长的子序列中长度最大的一个序列。

      Example: X = {A,B, C, B, D, A, B}, Y = {B, D, C, A, B, A},他们的一个最长公共子序列是Z = {B, C, B, A }

      解决方案:动态规划

      步骤一:描述最优解结构,寻找最优子结构

      设序列X = {x1, x2,....xn}, 序列Y = {y1, y2,...ym},它们的最长公共子序列是Z = {z1,z2, ...zk},他们之间有如下的最优结构性质:

      1)若xn=ym,则zk=xn=ym,zk-1将是Xn-1与Yn-1的最长公共子序列

      2)若xn=ym且zk≠xn,那么Z是Y与Xn-1最长公共子序列

      3)若xn=ym且zk≠ym,那么Z是X与Yn-1最长公共子序列

      有关最优子结构性质,依然可以采用反证法证明。

      2)递归定义最优值

      记c[i][j]表示序列Xi与序列Yj最长公共子序列的长度,其中Xi = {x1, x2,....xi}, Yj = {y1, y2,...yj},有如下的递归定义表达式:

      

      3)自底向上的求解

      依照上面的公式,自底向上的求解代码的代码如下:

    #include <iostream>
    
    using namespace std;
    
    int lcs_length(char* x, int m, char* y, int n, int** c)
    {
        for (int i = 0; i <= m ; i++)
            c[i][0] = 0;
        for (int j = 0; j <= n; j++)
            c[0][j] = 0;
    
        for (int i = 1; i <=m; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                if (x[i-1] == y[j-1])             //下标从l开始
                {
                    c[i][j] = c[i-1][j-1] + 1;   
                }
                else
                {
                    c[i][j] =  max(c[i-1][j], c[i][j-1]);
                }
            }
        }
    
        return c[m][n];
    }
    
    void print_lcs(int i, int j, char* x, int** c)
    {
        if (i == 0 || j == 0)
        {
            return;
        }
        
        if (c[i][j] == c[i-1][j-1] + 1)
        {
            print_lcs(i-1, j-1, x, c);
            cout<<x[i-1]<<endl;
        }
        else if (c[i][j] == c[i-1][j])
        {
            print_lcs(i-1, j, x, c);
        }
        else
        {
            print_lcs(i, j-1, x, c);
        }
    
    }
    
    int main()
    {
        char x[] = {"ABCBDAB"};
        char y[] = {"BDCABA"};
    
        int m = strlen(x);
        int n = strlen(y);
    
        int** c = new int*[m+1];
        for (int i = 0; i<=m; i++ )
        {
            c[i] = new int[n+1];
        }
    
        int t = lcs_length(x, m, y, n, c);
        for (int i = 0; i <= m; i++)
        {
            for (int j = 0; j <= n; j++)
            {
                cout<<c[i][j]<< " ";
            }
            cout<<endl;
        }
        print_lcs(m, n, x, c);
    
        system("pause");
        return 0;
    }

    代码的执行图解如下:

      案例三:最长递增子序列

      问题描述:求一个一维数组(N个元素)中的最长递增子序列的长度。

      Example:在序列1-12-34-56-7中,其最长的递增子序列为1246

      解决方案:动态规划

      继续依据动态规划的解决思想,解决过程省略,这里直接给出最优解的递归结构表达式,令m(i, j)表示以i为起点j为终点(包括原始array[i])的子序列,增长序列的最长长度,则递归表达式为:

      

      实现代码如下:

    #include <iostream>
    
    using namespace std;
    
    /************************************************************************/
    /*
      array: 存放序列
      m: m[i][j],表示以i为起点j为终点(包括原始array[i])的子序列,增长序列长度
      n: array长度
    */
    /************************************************************************/
    int lis_length(int* array, int** m, int n)
    {
        for (int i = 0; i < n; i++)
        {
            m[i][i] = 1;
        }
    
        for (int r = 2; r <= n; r++)
        {
            for (int i = 0; i <= n - r; i++)
            {
                int j = i + r -1;
                if (array[i] < array[i+1])
                {
                    m[i][j] = m[i+1][j] + 1;
                }
                else
                {
                    m[i][j] = m[i+1][j];
                }
            }
        }
        
        return m[0][n-1];
    }
    
    void print_lis(int* array, int** m, int n)
    {
        int lic_len = m[0][n-1];
    
        //m[0][i]记录了序列array[0:i],最长递增长度
        //m[0][i] = k,序列第k个增长元素出现
        for (int i = 0, lic_tag = 1; i != n; i++)
        {
            if (m[0][i] == lic_tag)
            {
                cout<<array[i]<<" ";
                lic_tag += 1;
            }
        }
    }
    
    int main()
    {
        const int n = 8;
        int array[n] = {1, 4, 2, -3, 4, 8, 6, -7};
    
        int** m = new int*[n];
        for (int i = 0; i != n; i++)
        {
            m[i] = new int[n];
        }
    
        int t = lis_length(array, m, n);
        print_lis(array, m, n-1);
    
        for (int i = 0; i != n; i++)
        {
            delete[] m[i];
        }
        delete[] m;
    
        system("pause");
        return 0;
    
    }

      总结:1)能够最优子结构性质,是使用动态规划算法的先决条件;2)重复解结构,动态规划算法能够高效使用的原因在于记录了子结构的解,而这些解又会在后续的求解过程中被我们使用(优于递归算法的原因);3)最后在编程实践技巧上,利用了自底向上的求解技术,从子问题开始求解。

      在编程实践的过程,特别需要主要以下问题:1)初始化,零界情况下表格的初始化必须提前完成;2)子问题的求解一定要现优于原问题,如出现某个子问题未求解出,但算法开始处理该原问题,将会引入错误(错误将非常隐晦);3)解的信息保存。

  • 相关阅读:
    SQL server 2005 创建数据库失败提示“Collation <服务器默认值> is not valid”解决方法
    ACM PKU 1011 Sticks 深度优先搜索
    pku1088 滑雪
    javascript 使用金山词霸网络翻译
    JQuery基础 document.ready
    遍历aspx页面中所有的指定控件
    DataTable,DataView和DataGrid中一些容易混淆的概念
    C#中的DBNull、Null、和String.Empty解释
    哈佛大学管训
    美国教育考试中心公布2010年托业考试时间表
  • 原文地址:https://www.cnblogs.com/wangbogong/p/3271067.html
Copyright © 2020-2023  润新知