动态规划及其常见问题

概念

  1. 无后效性:

    一旦f(n)确定,“我们如何凑出f(n)”就再也用不着了。要求出f(15),只需要知道f(14),f(10),f(4)的值,而f(14),f(10),f(4)是如何算出来的,对之后的问题没有影响。“未来与过去无关”,这就是无后效性。(严格定义:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。)

  1. 最优子结构:

    回顾我们对f(n)的定义:我们记“凑出n所需的最少钞票数量”为f(n).f(n)的定义就已经蕴含了“最优”。利用w=14,10,4的最优解,我们即可算出w=15的最优解。  大问题的最优解可以由小问题的最优解推出,这个性质叫做“最优子结构性质”。

  2. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

引入概念之后,我们如何判断一个问题能否使用DP解决呢?能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。

首先根据最优子结构将原问题拆分为无后效性的子问题,通过解决子问题来解决原问题

DP为什么会快?

无论是DP还是暴力,我们的算法都是在可能解空间内,寻找最优解。

来看钞票问题。暴力做法是枚举所有的可能解,这是最大的可能解空间。
DP是枚举有希望成为答案的解。这个空间比暴力的小得多。
也就是说:DP自带剪枝。
DP舍弃了一大堆不可能成为最优解的答案。譬如:15 = 5+5+5 被考虑了。15 = 5+5+1+1+1+1+1 从来没有考虑过,因为这不可能成为最优解。
从而我们可以得到DP的核心思想:尽量缩小可能解空间。在暴力算法中,可能解空间往往是指数级的大小;如果我们采用DP,那么有可能把解空间的大小降到多项式级。一般来说,解空间越小,寻找解就越快。这样就完成了优化。

典型问题

  1. 最长回文子串:(最佳解法应该是manacher方法)
    string longestPalindrome(string s) {
        int m = s.size();
        if (m == 0) {
            return "";
        }
        vector<vector<int> > dp(m, vector<int>(m, 0));
        int start = 0;
        int length = 1;

        for (int i = 0; i < m; i++) {
            // 单个字符属于回文,例如 abcd
            dp[i][i] = 1;

            // 连续两个字符相同属于回文,例如 abb
            if (i < m - 1) {
                if (s[i] == s[i + 1]) {
                    dp[i][i + 1] = 1;
                    start = i;
                    length = 2;
                }
            }
        }

        for (int len = 2; len <= m; len++) {
            // 用len来遍历字符串的长度
            for (int i = 0; i < m - len; i++) {
                // i遍历起点,j遍历终点
                int j = i + len;
                // 扩展长度
                if (dp[i + 1][j - 1] == 1 && s[i] == s[j]) {
                    dp[i][j] = 1;

                    if (j - i + 1 > length) {
                        start = i;
                        length = j - i + 1;
                    }
                }
            }
        }

        return s.substr(start, length);
    }
  1. 01背包问题:注意,背包问题都是无法通过贪心算法解决的

         // 背包容量为p,n个物品,第i个物品的价值为v(i),重量为w(i)
         int f[n][p];
         for(int i = 0; i <= n; i++) {
             f[i][0] = 0;
         }
         for(int i = 0; i <= p; i++) {
             f[0][p] = 0;
         }
         for(int i = 1; i <= n; i++) {
             for(int j = 0; j <= p; j++) {
                 if(j < w[i]) {
                     f[i][j] = f[i-1][j];
                 } else {
                     f[i][j] = max(f[i-1][j] + f[i-1][j-w[i]] + v[i])
                 }
                 
             }
         }
    
        // 对上面的代码进行优化可以得到
        int f[p] = {0};
        for(int i = 1; i <= n; i++) {
            for(int j = p; j >= w[i]; j--) {
                f[j] = max(f[j-w[i]] + f[j]);
                // 利用一维数组进行空间上的优化,注意此时遍历的顺序要反过来,这样才能利用上一轮循环得到的结果。
            }
        }
    
    
  2. 完全背包问题

    完全背包首先可以进行一轮筛选,如果有某个物品的质量比另一个的大,同时价值还比另一个的小,那么直接可以排除该物品

    但是完全背包问题不能使用贪心解法,可能无法得到最优解

        // 有n个物品,其价值问v[i],重量为w[i],数量都是无限的,背包容量为p
        for(int i = 0; i <= n; i++) {
            for(int j = 0; j <= p; j++) {
                f[i][j] = f[i-1][j];
                for(int k = 1; k * w[i] <= j; k++) {
                    f[i][j] = max(f[i][j], f[i][j-k*w[i]] + k*v[i])
                }
            }
        }
    
        // 将上述代码优化成一维数组
        int f[p] = {0};
        for(int i = 0; i <= n; i++) {
            for(int j = w[i]; j <= p; j++) {
                f[j] = max(f[p-w[i]] + v[i], f[p]);
                // 此处遍历的顺序是自左向右的
            }
        }
    
  3. 多重背包问题

    多重背包的解法类似于完全背包,就是k的限制条件比完全背包多了一条,因为多重背包的物品数量是有限的,
    也可以采用另一种解法,就是将多重背包转化为01背包的问题,就是把所有物品都看作是单独的,即使有些是完全相同的物品

        // 有n个物品,其价值问v[i],重量为w[i],数量为c[i],背包容量为p
        for(int i = 0; i <= n; i++) {
            for(int j = 0; j <= p; j++) {
                f[i][j] = f[i-1][j];
                // 这里多了一个判断条件
                for(int k = 1; k * w[i] <= j && k <= c[i]; k++) {
                    f[i][j] = max(f[i][j], f[i][j-k*w[i]] + k*v[i])
                }
            }
        }
    
        // 转化为01背包求解,构造一个新的v[i]和w[i],这时候的n = sum(c[i])
        // 此处可以稍作优化,即取一个物品的数量时,如果w[i]*c[i]超过了p就可以不取了
        int count = 0;
        int n = 0;
        for(int i = 0; i <= n; i++) {
            n += c[i];
            for(int j = 0; j <= c[i]; j++) {
                v_new[count] = v[i]
                w_new[count++] = w[i]
            }
        }
        int f[p] = {0};
        for(int i = 0; i <= n; i++) {
            for(int j = p; j >= w_new[i]; j--) {
                f[j] = max(f[p-w_new[i]] + v_new[i], f[p]);
                // 像01背包一样遍历
            }
        }
    
  1. 最长公共子序列问题:

        // 传入一个二维vector是为了记录方向,方便最后打印路径
        int lcs(string str1, string str2, vector<vector<int>>& vec) {
            int len1 = str1.size();
            int len2 = str2.size();
            vector<vector<int>> c(len1 + 1, vector<int>(len2 + 1, 0));
            for (int i = 0; i <= len1; i++) {
                for (int j = 0; j <= len2; j++) {
                    if (i == 0 || j == 0) {
                        c[i][j] = 0;
                    }
                    else if (str1[i - 1] == str2[j - 1]) {
                        c[i][j] = c[i - 1][j - 1] + 1;
                        vec[i][j] = 0;
                        // 做下标记,回头打印时遇到vec[i][j] == 0的时候打印str1[i-1]就可
                    }
                    else if (c[i - 1][j] >= c[i][j - 1]){
                        c[i][j] = c[i - 1][j];
                        vec[i][j] = 1;
                        // 打印时遇到vec[i][j] == 1时就递归回去i-1,即原路返回
                    }
                    else{
                        c[i][j] = c[i][j - 1];
                        vec[i][j] = 2;
                    }
                }
            }
    
            return c[len1][len2];
        }
    
        void print_lcs(vector<vector<int>>& vec, string str, int i, int j)
        {
            if (i == 0 || j == 0)
            {
                return;
            }
            if (vec[i][j] == 0)
            {
                print_lcs(vec, str, i - 1, j - 1);
                printf("%c", str[i - 1]);
            }
            else if (vec[i][j] == 1)
            {
                print_lcs(vec, str, i - 1, j);
            }
            else
            {
                print_lcs(vec, str, i, j - 1);
            }
        }
    
    
    
  2. 最长公共子串问题:

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

推荐阅读更多精彩内容

  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,333评论 0 2
  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,632评论 0 89
  • 看我主页简介免费C++学习资源,视频教程、职业规划、面试详解、学习路线、开发工具 每晚8点直播讲解C++编程技术。...
    编程小世界阅读 959评论 0 0
  • 树形动态规划,顾名思义就是树+DP,先分别回顾一下基本内容吧:动态规划:问题可以分解成若干相互联系的阶段,在每一个...
    Mr_chong阅读 1,477评论 0 2
  • 永远和旁人保持着距离,我这个人看似有情实则无情的很。时间默默一过,许多人就从我的世界里自动退出了。我忘了许多人,忘...
    鹿原先生和蓬蒿阅读 725评论 3 9