动态规划的简要总结和四个经典问题的c++实现

本文给出了动态规划的简要定义、适用场景、算法实现。并给出了四种经典动态规划:钢条切割求最大收益问题、矩阵链相乘求最小乘法次数问题、最长公共子序列问题、求最小的搜索代价的最优二叉搜索树的c++代码实现。

  • 定义
  • 性质 适用条件
  • 算法实现过程
      • 首先观察问题是否满足最优子结构性质
      • 写出递归等式递归的定义子问题的最优解
      • 求解子问题的最优解
      • 构造最优解
  • 四个经典问题的cpp实现
    • 1 钢条切割
    • 2 矩阵链相乘
    • 3 最长公共子序列
    • 4 最优二叉搜索树
  • 代码下载

1. 定义

动态规划(dynamic programming)与分治法类似,都是通过组合子问题求解原问题。(在这里,programming 指表格法。而非“编程”)。分治法是将原问题分为互不相交

的子问题,递归的求解子问题,再将它们组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题,动态规划算法对每个子子问题只求解一次,将其解保存在表格中,从而不用每次求解该子子问题时都要反复计算。

总结来讲:分治法的子子问题是全新的,动态规划的子子问题是有重叠的。

2. 性质 | 适用条件

使用动态规划必须满足的2条性质

- 最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

- 重叠子问题:递归算法会反复求解相同的子问题,而不是一直生成新的子问题,(对比分治法:递归的每一步都生成全新的子问题),动态规划算法能够最每个子问题都只求解一次,将解存表中,需要时直接查表。但是应该注意,同一问题的其中一个子问题的解不能影响另一个子问题的解,即无关。换句话说,当前问题的每一次划分生成的子问题之间不存在重复的资源。

3. 算法实现过程

1. 首先观察问题是否满足最优子结构性质

2. 写出递归等式,递归的定义子问题的最优解

3. 求解子问题的最优解

动态规划的实现有2种等价的方法:

1. 带备忘的自顶向下(递归)

仍按原始的递归形式,只是在递归求解过程中记录每个子问题的解,存储到数组(通常在使用前会初始化值,常用正负无穷)中。当需要一个子问题的解时,首先检查是否保存过此解,如果保存过(非初始值),则直接返回该解,节省计算时间;否则,按常规方式计算该子问题的解并记录。

2. 自底向上

4. 构造最优解

通常根据第2步中的递归等式,可倒推出重构方式。设计递归式,分解子问题时,一般都通过定义一个分割点,那么同样的根据这个分割点分割隐含最优解的矩阵。

4. 四个经典问题的cpp实现

以下给出了四个经典动态规划问题的c++实现:

钢条切割求最大收益问题、矩阵链相乘求最小乘法次数问题、最长公共子序列问题、求最小的搜索代价的最优二叉搜索树。

只给出代码实现,具体算法过程参见《算法导论》P205-231

4.1 钢条切割

#include <iostream>
using namespace std;
class Rod{
private:
    int rod_len;
    int *memorized_q;
    int *s;
    int times1,times2,times3; // 统计计算次数
public:
    Rod(int n) {
        times1 = 0;
        times2 = 0;
        times3 = 0;
        rod_len = n;
        s = new int [rod_len+1];

        memorized_q = new int [rod_len+1];
        for (int i = 0; i <= rod_len; i++)
            memorized_q[i] = INT_MIN;
    }
    ~Rod() {
        delete memorized_q;
        memorized_q = NULL;
    }
    // 普通递归
    int CutRod(int rod_len, const int prices[]) {
        if (rod_len == 0)
            return 0;
        int q = -1;
        for (int i = 1; i <= rod_len; i++) {
            times1++;
            q = max(q, prices[i] + CutRod(rod_len-i, prices));
        }
        return q;
    };
    // 自顶向下带备忘
    int CutRodMemoized(int rod_len, const int prices[]) {
        if (memorized_q[rod_len] >= 0)
            return memorized_q[rod_len];
        int q =INT_MIN;
        if (rod_len == 0)
            q = 0;
        for (int i = 1; i <= rod_len; i++) {
            times2++;
            //q = max(q, prices[i] + CutRodMemoized(rod_len-i, prices));
            if (q < prices[i] + CutRodMemoized(rod_len-i, prices)) {
                q = prices[i] + CutRodMemoized(rod_len-i, prices);
                s[rod_len] = i;
            }
        }
        memorized_q[rod_len] = q;
        return q;
    };
    // 自底向上
    int BottomUpCutRod(int rod_len, const int prices[]) {
        int *r = new int[rod_len+1];
        int *s2 = new int [rod_len+1];
        r[0] = 0; // 初始化最小的子问题
        for (int i = 1; i <= rod_len; i++) {
            int q = 0;
            for (int j = 1; j <= i; j++) {
                times3++;
                if (q < prices[j] + r[i-j]) {
                    q = prices[j] + r[i-j];
                    s2[i] = j; //最终留下的是 i 子问题 下的q最大时的 j
                }
            }
            r[i] = q;
        }
        cout << "BottomUpCutRod 解空间:";
        int n = rod_len;
        while(n > 0) {
            cout << s2[n] << " "; // 打印 n 问题 下的q最大时的 j
            n -= s2[n];
        }
        cout << endl;
        return r[rod_len];
    }
    void printTimes() {
        cout << "普通递归 CutRod 计算次数:" << times1 << endl;
        cout << "CutRodMemoized 计算次数:" << times2 << endl;
        cout << "BottomUpCutRod 计算次数:" << times3 << endl;
        int n = rod_len;
        cout << "CutRodMemoized 解空间:";
        while(n > 0) {
            cout << s[n] << " "; // 打印 n 问题 下的q最大时的 j
            n -= s[n];
        }
        cout << endl;
    }
};

int main()
{
    const int prices[]= {0,1,5,8,9,10,17,17,20,24,30};
    int rod_len = 8;
    Rod rod(rod_len); //sizeof(prices)/sizeof(prices[0])
    cout << rod.CutRod(rod_len, prices) << endl;
    cout << rod.CutRodMemoized(rod_len, prices) << endl;
    cout << rod.BottomUpCutRod(rod_len, prices) << endl;
    rod.printTimes();
    getchar();
    return 0;
}

4.2 矩阵链相乘

#include <iostream>
using namespace std;
class Matrix {
private:
    int n;
    int **m;
    int **s;
public:
    Matrix(int len) {
        n = len;
        m = new int *[n+1];
        s = new int *[n+1];
        for (int i = 0; i < n+1; i++) {
            m[i] = new int[n+1];
            s[i] = new int [n+1];
        }
    }

    //===================Recursive Memoized====================
    // 算法时间复杂度O(n^3), 空间复杂度O(n^2)
    int RecursiveMatrixMulti(const int * &p, int i, int j) {
        if (m[i][j] != INT_MAX)
            return m[i][j];
        if (i == j)
            m[i][j] = 0;
        for(int k = i; k < j; k++) {
            int q = RecursiveMatrixMulti(p, i, k) + RecursiveMatrixMulti(p, k+1, j) + p[i-1]*p[k]*p[j];
            if (q < m[i][j]) {
                m[i][j] = q;
                s[i][j] = k;
            }
        }
        return m[i][j];
    }
    int MatrixMultiMemoized(const int* p) {
        for (int i = 0; i < n+1; i++)
            for (int j = 0; j < n+1; j++) {
                m[i][j] = INT_MAX;
                s[i][j] = 0;
            }
        return RecursiveMatrixMulti(p, 1, n);
    }
    //===============Bottom Up========================
    // 算法时间复杂度O(n^3), 空间复杂度O(n^2)
    void MatrixMulti(const int* p) {
        for(int i = 0; i < n+1; i++) {
            for (int j = 0; j < n+1; j++)
            {
                m[i][j] = 0;
                s[i][j] = 0;
            }
        }
        for (int l = 2; l <= n; l++) {  // l 是矩阵链的长度,l=2代表有2个矩阵的子问题
            //得到 i
            for (int i = 1; i <= n-l+1; i++) {
                // 得到 j (-1 是减去 A_i)
                int j = i+l-1;
                m[i][j] = INT_MAX;
                for (int k = i; k < j; k++) { //  i <= k < j
                    int q = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
                    if (q < m[i][j]) {
                        m[i][j] = q;
                        s[i][j] = k;
                    }
                }
            }
        }
    }
    //=================打印最后括号化的结果====================
    void PrintOptimalParens(int i, int j) {
        if (i == j)
            cout << "A" << i;
        else
        {
            cout << "(";
            PrintOptimalParens(i, s[i][j]); // s[i][j] 即 (i, j)最优的k值
            PrintOptimalParens(s[i][j]+1, j);
            cout << ")";
        }
    }
    //===============打印最优解的结构矩阵==================
    void PrintS() {
        cout <<endl << "打印 s 矩阵如下:" << endl;
        for(int i = 0; i < n+1; i++) {
            for (int j = 0; j < n+1; j++)
            {
                cout << s[i][j] << " ";
            }
            cout << endl;
        }
    }

    ~Matrix() {
        for (int i = 0; i < n+1; i++) {
            delete m[i];
            m[i] = NULL;
            delete s[i];
            s[i] = NULL;
        }
        delete m;
        delete s;
        m = NULL;
        s = NULL;
    }
};

int main() {
    const int p[] = {30,35,15,5,10,20,25};
    int n = sizeof(p)/sizeof(p[0]) - 1;
    cout << "原始矩阵链:";
    for (int i = 1; i <= n; i++)
        cout << "A" << i;
    cout << endl;
    cout << "===============================" << endl;
    Matrix matrix(n);
    cout << "Bottom Up:" << endl;
    matrix.MatrixMulti(p);
    matrix.PrintOptimalParens(1, n);
    matrix.PrintS();
    cout << "===============================" << endl;
    cout << "Recursive Memoized: " << endl;
    matrix.MatrixMultiMemoized(p);
    matrix.PrintOptimalParens(1, n);
    matrix.PrintS();
    getchar();
    return 0;
}

4.3 最长公共子序列

#include <iostream>
#include <vector>
#include <string>
using namespace std;
template<class T>
class Sequence {
private:
    int m;
    int n;
    int **c;
    void MatrixInit(int m, int n) {
        c = new int *[m];
        for (int i = 0; i <m+1; i++) {
            c[i] = new int [n+1];
        }
        for (int i = 0; i <= m; i++) {
            for (int j =0; j <= n; j++) {
                c[i][j] = 0;
            }
        }
    }
public:
    // 时间复杂度 O(mn), 空间复杂度 O(mn)
    void LCS(const T&X, const T &Y) {
        m = X.size();
        n = Y.size();
        MatrixInit(m, n);
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (X[i-1] == Y[j-1]) {  // vector 从 0 开始
                    c[i][j] = c[i-1][j-1] + 1;
                } else if(c[i-1][j] > c[i][j-1]){
                    c[i][j] =c[i-1][j];
                } else {
                    c[i][j] =c[i][j-1];
                }
            }
        }
    }

    void PrintLCS(const T &X, int i, int j) {
        if (i == 0 || j == 0)
            return;
        if (c[i][j] == c[i-1][j-1]+1) {
            PrintLCS(X, i-1, j-1);
            cout << X[i-1];
        } else if (c[i][j] == c[i-1][j]) {
            PrintLCS(X, i-1, j);
        } else
            PrintLCS(X, i, j-1);
    }
};

int main()
{
    const string X_str = "ABCBDAB";
    const string Y_str = "BDCABA";
    vector<char> X(X_str.begin(), X_str.begin()+X_str.size());
    vector<char> Y(Y_str.begin(), Y_str.begin()+Y_str.size());
    Sequence<vector<char>> seq;
    seq.LCS(X, Y);
    cout << endl << "Thle LCS of  <" << X_str << "> && <" << Y_str << "> is :" << endl;
    seq.PrintLCS(X, X.size(), Y.size());

    int A[] = {1,0,0,1,0,1,0,1};
    int B[] = {0,1,0,1,1,0,1,1,0};
    vector<int> A_(A, A+sizeof(A)/sizeof(A[0]));
    vector<int> B_(B, B+sizeof(B)/sizeof(B[0]));
    Sequence<vector<int>> seq_int;
    seq_int.LCS(A_, B_);
    cout << endl << "Thle LCS of  <A> && <B> is :" << endl;
    seq_int.PrintLCS(A_, A_.size(), B_.size());
    getchar();
}

4.4 最优二叉搜索树

#include <iostream>
using namespace std;
class BSTree {
private:
    float** e;
    float** w;
    int** root;
    void MatInit(int n) {
        e = new float *[n+2];  // + 的是两头
        w = new float *[n+2];
        root = new int *[n+2];
        for (int i = 0; i < n+2; i++) {
            e[i] = new float[n+2];
            w[i] = new float[n+2];
            root[i] = new int[n+2];
        }
        for (int i = 0; i < n+2; i++)
            for (int j = 0; j < n+2; j++)
                root[i][j] = 0;
    }
public:
    // 时间复杂度 O(n^3) 空间复杂度 O(n^2)
    void OptimalBST(const float* p, const float* q, int n) {
        MatInit(n);
        for (int i = 1; i <= n+1; i++) {
            e[i][i-1] = q[i-1];
            w[i][i-1] = q[i-1];
        }
        for (int l = 1; l <= n; l++) {
            for (int i = 1; i <= n-l+1; i++) {
                int j = i+l-1;
                w[i][j] = w[i][j-1] + p[j] + q[j];
                e[i][j] =INT_MAX;
                for (int r = i; r <= j; r++) {
                    float t = e[i][r-1] + e[r+1][j] + w[i][j];
                    if (t < e[i][j]) {
                        e[i][j] = t;
                        root[i][j] = r;
                    }
                }
            }
        }

    }
    void PrintRoot(int n) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (root[i][j] == 0)
                    cout << "  ";
                else
                    cout << root[i][j] << " ";
            }
            cout << endl;
        }
    }
    void PrintOptimalBST(int n) {
        int r = root[1][n];
        cout << "k" << r << " is root." << endl;
        PrintOptimalSubTree(1, r-1, r,"left child");
        PrintOptimalSubTree(r+1, n, r, "right child");
    }
private:
    void PrintOptimalSubTree(int i, int j, int r, const char* dir) {
        if (i <= j) {
            int t = root[i][j];
            cout << "k" << t << " is k" << r << "‘s " << dir << endl;
            PrintOptimalSubTree(i, t-1, t, "left child");
            PrintOptimalSubTree(t+1, j, t, "right child");
        }
        return;
    }

};
int main() {
    const float p[] = {0, 0.15, 0.10, 0.05, 0.10, 0.20};
    const float q[] = {0.05, 0.10, 0.05, 0.05, 0.05, 0.10};
    BSTree bst;
    int n = sizeof(p)/sizeof(p[0]) - 1;
    bst.OptimalBST(p, q, n);
    bst.PrintRoot(n);
    bst.PrintOptimalBST(n);
    getchar();
}

5. 代码下载

http://download.csdn.net/detail/quzhongxin/8827577

参考资料: 《算法导论》

时间: 2024-10-31 00:32:38

动态规划的简要总结和四个经典问题的c++实现的相关文章

连续子序列最大和问题的四种经典解答

问题描述 给定(可能是负的)整数序列A1, A2,...,AN, 寻找(并标识)使Sum(Ak)(k >=i, k <= j)的值最大的序列.如果所有的整数都是负的,那么连续子序列的最大和是那个最大的负数项.最好给出给出最大和连续子序列!! 1 暴力破解法 这个问题有一个最简单直接的穷举解决法.我们看问题,既然要求里面最大的连续子序列.那么所有的连续子序列将由哪些组成呢?以数组的第一个元素为例,连续子序列必须是至少包含元素A1,也可能包含从A1到A2...以及从A1到AN.这样就有N种可能.后

安卓企业开发(三) activity的四种经典传值方法

开发中遇到多个activity的传值问题 相邻两个之间的传值 或者多个三个以上之间的传值问题 但是很多同学这方面经验还是不足,说下常用的开发场景 1 一般的注册或者添加某项信息界面就会遇activity传值问题 2  比如我在一个界面提交新息  需要打开一个新的界面选择里面的信息回到当前activty的时候 现在说下比较经典的四种比较经典的传值方法 一 如果是两个相邻activity之间的传值: 可以用Intent传值 对象和单个属性都可以都可以 Intent intent =new Inten

【后缀自动机】【拓扑排序】【动态规划】hihocoder1457 后缀自动机四&#183;重复旋律7

解题方法提示 小Hi:我们已经学习了后缀自动机,今天我们再来看这道有意思的题. 小Ho:好!这道题目让我们求的是若干的数字串所有不同子串的和. 小Hi:你能不能结合后缀自动机的性质来思考如何解决本题? 小Ho:这道题目既然是关于子串,那么我知道从后缀自动机的所有状态中包含的子串的集合恰好对应原串的所有不重复子串. 小Hi:很好.那你可以先简化问题,想想只有一个串怎么做? 小Ho:好的.这个难不倒我.我上次已经知道如何计算一个串所有不同子串的数量,现在这题也类似,只不过计算更加复杂一点. 小Hi:

简要总结selenium四个工具组

selenium 是基于WEB的自动化测试工具. 由以下几个工具组组成 1.selenium IDE: 一个火狐插件 点击这个插件就进入录制界面,能够记录用户的操作,并且将其导出为可重复使用的测试脚本,并且支持多种语言 优点: 无需编程技能即可快速上手 缺点: 1.分散的脚本不可重用且难以维护,一旦UI发生变化测试就很受影响. 2.系统在测试之前必须可用.不适用于ATDD 3.仅支持firefox,不支持其他浏览器,无法做浏览器兼容性测试 2.selenium RC (selenium 1):

JavaScript权威设计--JavaScript类型,值,变量(简要学习笔记四)

1.宿主对象与宿主环境 宿主对象:由ECMAScript实现的宿主环境提供的对象,可以理解为:浏览器提供的对象.所有的BOM和DOM都是宿主对象. 宿主环境:一般宿主环境由外壳程序创建与维护,只要能提供js引擎执行的环境都可称之为外壳程序.如:web浏览器等. 2.日期的转换 var now =new Date(); typeof(now+1) //string typeof(now-1) //number now==now.toString(); //true now>(now-1) //tr

什么是 “动态规划” , 用两个经典问题举例

1.什么是动态规划? 看了很多题解,一般解决者开始就说用DP来解,然后写了嵌套的for循环,不是很容易看懂,但是确实解出来了,我们这次来看下到底什么是动态规划?它有什么特点呢?容我抄一段话: 动态规划(Dynamic programming,DP),通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法.通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表. 这种做法在重复

JavaScript权威设计--JavaScript函数(简要学习笔记十)

1.函数命名规范 函数命名通常以动词为前缀的词组.通常第一个字符小写.当包含多个单词时,一种约定是将单词以下划线分割,就像"like_Zqz()". 还有一种就是"likeZqz()".有些些函数是用作内部用的或者为私有函数通常以一条下划线为前缀,就像"_zqzName()". 2.以表达式方式定义的函数 如: var zqz=function (){ return "zhaoqize"; } 在使用的时候必须把它赋值给一个变

算法学习——动态规划1

众所周知,在面试中最难,也是大公司最容易考的就是动态规划,所以今天打算开撕动态规划,之前只是对于单个的题目知道解法,不过时间一久就忘记了,今天开始要彻底的理解方法论再到实践,希望老天保佑能够一周搞定! 一.通过一个小例子了解方法论: 一个例子,如上图: 求从起点到终点的最短路径: 这是一道最基本的动态规划问题,如果不使用动态规划可以使用穷举进行求解,路径大概存在2的k次方.利用动态规划求解主要步骤: 1. 找出规划的初始状态:对此,找出第一个子问题的解从而可以在后面的内容中利用该解来作为后面状态

VMware vSphere四种迁移类型的区别与适应场景

最近一直刚开始接触VMware vSphere这款虚拟化软件,每天的过程都是上午学新知识不理解,痛苦:下午实验各种出错,折磨:晚上回顾一天所学,五味陈杂,不过相比上午,下午已经好很多了.然后第二天依旧如此.虽然每天很受挫不过过得还挺充实. 昨天把四种迁移搞明白就很开心,这里给大家分享一下,愿你们看后有点收获,少走点弯路. 正文开始 下面这张图是这篇文字的核心内容.下文就是按这张图展开讲解的. 先简要解释一下四种迁移类型的基本概念 迁移–将虚拟机从一台主机或数据存储移到另一台主机或数据存储. 迁移