动态规划

1、 什么样的问题适合用动态规划来求解

1.1. 问题具有最优子结构;就是说问题的最优解所包含的子问题的解也是最优的;比如数字三角形问题中对于第i行j列的元素来说,其到i+1行的最优路径是确定的即:max(dp[i+1][j], dp[i+1][j+1])
1.2. 状态具有无后效性;即当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种路径演变到当前的这若干个状态,没有关系。

2、 动态规划问题解题的一般思路

1、将原问题分解为子问题

  • 把原问题分解为若干个子问题,子问题和原问题形式类似,只是规模变小,子问题都解决了的话,原问题也就解决了;
  • 子问题的解一旦求出就会被保存,所以每个子问题只用求解一次;
  • 比如数字三角形问题中某个元素对应的最优路径的最大和就是一个子问题;

2、确定状态

  • 将和子问题相关的各变量的一组取值,称为一个“状态”;一个状态对应于一个或多个子问题,所谓某个状态下的值,就是这个状态所对应的子问题的解;所有状态的集合就构成了问题的状态空间,状态空间的大小,与动规解决问题的时间复杂度相关;
  • 比如数字三角形问题中每个元素对应的最优路径的最大和就是一个状态,总共有N*(N+1)/2个状态,乘以每个状态计算所需的时间就是整个问题的求解时间;

3、确定边界状态值

  • 比如数字三角形问题中边界值就是三角形底边的值;

4、确定状态转移方程

  • 如何从一个或多个值已知的状态,求出另一个状态的值,(“人人为我”递推型)。状态的转移可以用递推公式表示,此公式就叫状态转移方程;
  • 比如数字三角形问题中的状态转移方程:
if(start_raw == n-1)//边界条件 
   max_sum[start_raw][start_col] = nums[start_raw][start_col];
else
    int max_1 = maxSum(nums, start_raw + 1, start_col, n);
    int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n);
    int max = max_1 >= max_2 ? max_1 : max_2;
    max_sum[start_raw][start_col] = max + nums[start_raw][start_col];

3 实例讲解

例题一、 数字三角形

题目描述:

如下图所示的数字三角形图,寻找从顶至底的某处的一条路径,使该路径所经过的数字之和最大。(1)每一步只能沿左斜线向下或右斜线向下,只需要给出最大和就可以了,不必给出具体路径(2)1 < 三角形行数 < 101(3)三角形数字为0,1,…99


image.png

解法方法分析:

  • 首先构建输入:该数据是三角形,属于二维图形,适合使用二维向量或是数组来表示;
    可以使用如下代码来实现

#include<iostream>
#include<vector>
using namespace std;

void getInput(vector<vector<int> >& dp_test)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
        }
        dp_test.push_back(temp_vector);
        
    }
    cout << "The raw num is: " << raw_n << endl;
    cout << "The input nums are: " << endl;
    for(size_t i = 0; i < dp_test.size(); I++)
    {
        for(size_t j = 0; j < dp_test[i].size(); j++)
        {
            cout << dp_test[i][j] << " ";
        }
        cout << endl;
    }
}

得到的数组是这样的

The raw num is: 5
The input nums are: 
7 
3 8 
8 1 0 
2 7 4 4 
4 5 2 6 5 

这里元素dp[i][j], ij从0开始,每次向下移动的路径就是dp[i+1][j]或者dp[i+1][j+1];maxSum(i,j)表示从dp[i][j]到底边的各条路径中,最佳路径的数字和;

  • 解法一、 该问题是典型的递归问题
    dp[i][j]出发,下一步只能走dp[i+1][j]或者dp[i+1][j+1],即只要知道maxSum(i+1,j)maxSum(i+1,j+1)就能得到maxSum(i,j) = max(maxSum(i+1,j),maxSum(i+1,j+1)) + dp[i][j];边界就是maxSum(n-1,*) = nums[n-1][*]
int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n)
{
    if(start_raw == n-1)//边界条件 
        return nums[start_raw][start_col];
    int max_1 = maxSum(nums, start_raw + 1, start_col, n);
    int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n);
    int max = max_1 >= max_2 ? max_1 : max_2;
    return max + nums[start_raw][start_col];
}

int main()
{
    vector<vector<int>> input_vec;
    getInput(input_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size()) << endl;
    return 0;
}

分析递归算法的时间复杂度,该递归方法是从上到下一层一层的递归求解,那么存在大量的重复计算,每个元素上面重复计算的次数如下入所示:


image.png

根据等比数列求和可该算法的时间复杂度是O(2^n),对于n=100行,时间肯定是超时的;

  • 解法二、 记忆递归型动规程序,采用递归的方式,时间复杂度太大主要是因为优太多的重复计算,改进的方法是将计算出的每一个maxSum值都存起来,下次用到的时候直接取用,避免重复计算;那么这时的时间复杂度根据等差数列求和可以知道是O(n^2);
#include<iostream>
#include<vector>
using namespace std;

void getInput(vector<vector<int>>& dp_test, vector<vector<int>>& dp_max)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        vector<int> max_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
            max_vector.push_back(-1);
            
        }
        dp_test.push_back(temp_vector);
        dp_max.push_back(max_vector);
    }
}

int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n, vector<vector<int>> max_sums)
{
    if(max_sums[start_raw][start_col] != -1)
        return max_sums[start_raw][start_raw];
    if(start_raw == n-1)//边界条件 
    {
        max_sums[start_raw][start_col] = nums[start_raw][start_raw];
    }
    else
    {
        int max_1 = maxSum(nums, start_raw + 1, start_col, n, max_sums);
        int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n, max_sums);
        int max = max_1 >= max_2 ? max_1 : max_2;
        max_sums[start_raw][start_col] = max + nums[start_raw][start_col];
    }
}

int main()
{
    vector<vector<int>> input_vec;
    vector<vector<int>> max_vec;
    getInput(input_vec,max_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size(), max_vec) << endl;
    return 0;
}
  • 解法三、递推(“人人为我”递推型动归程序)
    递归会有调用栈溢出的风险;可以使用递推的方式,即从最后一行开始,逐层向上求出每个元素位置的最大值,这样既可以每个元素只计算一次,时间复杂度为O(2^n),又不使用递归,不会有调用栈溢出的风险;采用递推时,最后一行的最大值就是自己,倒数第二行的最大值时元素本身加上最后一行两个数的最大值,上面的行以此类推,结果如下:
    输入数据:
    7
    3 8
    8 1 0
    2 7 4 4
    4 5 2 6 5

递推后的结果:

30
23 21
20 13 10
7 12 10 10
4 5 2 6 5

上面的表格过程可以看出来,整个过程会从底向上求出每一个节点在以该节点开始到最下面的最优路径的解;这个值只是该节点的最优解,并不是该层的最优解,但是当我们以此方式求到最上面的节点即顶点时,顶点的最优解就是我门需要的最优解;这里不需要记录路径;

#include<iostream>
#include<vector>
using namespace std;

/*
@dp_max:的元素对于每一行都是复用的
*/
void getInput(vector<vector<int>>& dp_test, vector<int>& dp_max)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
            
        }
        dp_test.push_back(temp_vector);
        for(auto iter : dp_test[raw_n - 1])
            dp_max.push_back(iter);
    }
}

int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n, vector<int> max_sums)
{
    for(int raw = n - 1; raw > start_raw - 1; raw--)
    {
        for(int col = 0; col < n; col++)
        {
        int max = nums[raw + 1][col] >= nums[raw + 1][col+1] ? nums[raw + 1][col] : nums[raw + 1][col + 1];
            max_sums[col] = max + nums[raw][col];
        }
    }
    return max_sums[start_col];
}

int main()
{
    vector<vector<int>> input_vec;
    vector<int> max_vec;
    getInput(input_vec,max_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size(), max_vec) << endl;
    return 0;
}
  • 递归到动规程序的一般转换方法
    • 递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值时递归函数的返回值,这样就可以从边界开始,逐步填充数组,这就相当于计算递归函数值的逆过程。

例题二、 最长公共子序列

题目描述:

给出两个字符串,求出这样的一 个最长的公共子序列的长度:子序列 中的每个字符都能在两个原串中找到, 而且每个字符的先后顺序和原串中的 先后顺序一致。

解法方法分析:

首先拿到题目,是求最优解的题目,一般用dp来解,但是dp需要有无后效性的最优子问题,这个需要好好设计dp的状态,设计不出来就不能用dp;
首先想到的是对于长度为m和n的两个字符串S1和S2,其

  • 状态:最长公共子序列用dp(m,n)表示;
  • 那么显然边界条件:
dp(i,0) = 0; (i = 0...m)
dp(0,j) = 0; (j = 0...n)
  • 状态转移方程
当S1[i-1] == S2[j-1]时这时显然的:dp(i, j) = dp(i-1, j-1) + 1; 
当S1[i-1] != S2[j-1]时有:dp(i, j) = max(dp(i-1,j), dp(i, j-1))
屏幕快照 2019-11-19 上午12.34.52的副本.png
  • 显然dp(i, j)不会比dp(i-1,j)和dp(i, j-1)小;
  • 那么会不会比两个都大呢?假设dp(i, j)比dp(i-1,j)大,因为二者唯一不同的就是S1[i-1],那么S1[i-1]一定是S1和S2的最长公共子序列的最后一个元素;同理,假设dp(i, j)比dp(i,j-1)大,那么S2[j-1]一定是S1和S2的最长公共子序列的最后一个元素;那么必然S1[i-1] == S2[j-1],而这个于假设冲突,所以可以得到上面的状态转移方程;
  • 采用递推的方式实现


    屏幕快照 2019-11-19 上午12.40.12.png

    image.png
int longestCommonSubsequence(string text1, string text2) {
        if(text1.size() == 0 || text2.size() == 0){
            return 0;
        }
        int len1 = text1.size();
        int len2 = text2.size();
        vector<vector<int>> lcs;
        lcs.resize(len1 + 1);

        for(size_t i = 0; i <= len1; i++){
            lcs[i].push_back(0);
            lcs[i].resize(len2 + 1);
        }
        for(size_t j = 1; j <= len2; j++){
            lcs[0][j] = 0;
        }
        for(size_t i = 1; i <= len1; i++){
            for(size_t j = 1; j <= len2; j++){
                if(text1[i-1] == text2[j-1]){
                    lcs[i][j] = lcs[i-1][j-1] + 1;
                } else {
                    lcs[i][j] = lcs[i-1][j] > lcs[i][j-1] ? lcs[i-1][j] : lcs[i][j-1];
                }
            }
        }
        return lcs[len1][len2];
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容