【算法】动态规划(二)

前言

上一篇,介绍了动态规划DP的概念,以及暴力递归和动态规划的关系。这一篇,将介绍几道经典的动态规划题

1、台阶问题

题目

有n级台阶,一个人每次上一级或者两级,问有多少种可以走完n级台阶的方法

思路

想走到第n级台阶,有两种途径
1、在n-1级上一级
2、在n-2级上两级
因此,我们可以知道f(n) = f(n - 1) + f(n - 2) ,其实答案就是斐波那契数列

代码实现

递归版本

    public static int step1(int n) {
        // 对于第n级台阶方法 = f(n - 1) + f(n - 2)
        if(n == 1 || n == 2) {
            return n;
        }
        return step1(n - 1) + step1(n - 2);
    }

动态规划版本

    public static int step2(int n) {
        if(n == 1 || n == 2) {
            return n;
        }
        int[] dp = new int[1000];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }

2、最长递增子序列长度

题目

给定数组arr,返回arr的最长递增子序列长度。返回arr的最长递增子序列长度。
比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9],
所以返回这个子序列的长度5

算法实现

我们构建一个以为数组dp,dp[i]的表示以arr[i]结尾时,最长递增子序列的长度。计算dp[i]时,先判断arr[i]是否大于arr[0..i],若大于,则找到dp[i..1]中最大的值+1,否则dp[i]置为1。最后返回dp数组中最大的数,即为最长递增子序列长度

    public static int commonLongestAscSeq(int arr[]) {
        if (arr == null) {
            return 0;
        }
        if (arr.length == 1) {
            return 1;
        }
        int max = 0;
        // dp[i]代表以arr[i]为结尾的情况下,arr[0..i]的最大递增子序列长度
        int[] dp = new int[arr.length];
        dp[0] = 1;
        for(int i = 1; i < arr.length; i++) {
            // 用于标记arr[i]是否比arr[0~i-1]的数都小
            boolean isSmallThanPre = true;
            // 对于arr[i],我们要找0~i-1的最大递增子序列,然后在其基础上+1
            for(int j = 0; j < i; j++) {
                if(arr[i] > arr[j]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                    isSmallThanPre = false;
                }
            }
            // 如果都比i~1的数小,那么就置为1
            if (isSmallThanPre) {
                dp[i] = 1;
            }
            max = Math.max(dp[i], max);
        }
        return max;
    }

3、01背包问题

题目

给定 n 种物品和一个容量为 bag的背包,物品 i 的重量是 weights[i],其价值为 values[i]
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
例如weights = { 2, 2, 6, 5, 4 },values = { 6, 3, 5, 4, 6 },bag=10,那么总价值最大为15,当选择v[0],v[1],v[4]时,6 + 3 + 6价值最大,重量为2 + 2 + 4 = 8。01背包问题就是每件物品最多拿一件,还有其延伸版本,完全背包问题等等,这里就不描述了。

思路

创建一个二维数组[物品的数量 + 1][背包的大小 + 1],dp[i][j]表示,在拿第i件物品时,j背包容量为j时,所能获得最大价值。那么对于dp[i][j],有两种情况
1、不拿这件物品时,dp[i][j]的价值更高 -> dp[i][j] = dp[i - 1][j]
2、拿这件物品时,dp[i][j]的价值更高,因此,我们在不拿i这件物品时(i - 1),且重量减去i物品重量( j - w[i])的最优解基础上 加上该物品的价值 -> dp[i][j] = v[i] + dp[i - 1][j - w[i - 1]]
得到了以上递推式后,我们就可以进行动态规划了

整个填表的过程如下所示:


算法实现

    public static int maxValue2(int[] weights, int[] values, int bag) {
        // dp[i][j]表示,在0~i件物品选择,j背包容量为j时,所能获得最大价值
        int[][] dp = new int[weights.length + 1][bag + 1];
        for(int i = 1; i < dp.length; i++) {
            for(int j = 1; j <= bag; j++) {
                // 容量不够放下i,所以不拿
                if (j < weights[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                }else {
                    int max1 = dp[i - 1][j];
                    int max2 = values[i - 1] + dp[i - 1][j - weights[i - 1]];
                    dp[i][j] = Math.max(max1, max2);
                }
            }
        }
        return dp[weights.length][bag];
    }

4、最小代价

题目

给定两个字符str1和str2,再给定三个整数ic,dc和rc,分别代表插入、删除和替换一个
字符的代价。返回将str1编辑成str2的最小代价。
例如,str1="abc", str2="adc", ic=5,dc=3, rc=2。从"abc"->"adc",把"b"替换
成“d”的代价最小,所以返回2.
再比如str1="abc", str2="adc", ic=5,dc=3, rc=100。从"abc"->"adc",先删除"b",
然后插入"d"是代价最小的,所以返回8.

思路

创建一个二维数组dp[str1.len + 1][str2.len + 1],dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价。
那么对于第一列代价都是删除代价

// 填写第一列,那么就是str1[0~i] -> ""
for(int i = 1; i <= chs1.length; i++) {
    dp[i][0] = i * dc;
}

同理,对于第一行都是插入代价,那么就是"" -> str2[0~j]

// 填写第一行,那么就是"" -> str2[0~j]
for(int j = 1; j <= chs2.length; j++) {
    dp[0][j] = j * ic;
}

那么而对于非第一行,第一列,dp[i][j]的值可能来自以下4中情况,注意,我们创建的dp数组是[str1.len + 1][str2.len + 1]的,因此求i,j的问题,我们需要回原数组,就需要i-1,j-1
1、【不操作】当str1[i - 1] == str2[i - 1],说明str1[0..i-1] -> str2[0..j-1]只需要str1[0..i-2] 编辑成 str2[0..j-2]的代价,也就是dp[i - 1][j - 1]
2、【删除】先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1],因此此时花销为dp[i - 1][j] + dc
3、【插入】先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等,因此此时花销为ic + dp[i][j - 1]
4、【替换】先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等,因此此时花销为dp[i - 1][j - 1] + rc

以str1 = "ab12cd3",str2 = "abcdf",ic=5,dc=3,rc=2为例,蓝色部分为初始的第一行,第一列的数字,整个填表的过程如下:


算法实现

    public static int convertFirstToSecond(String str1, String str2, int ic, int dc, int rc) {
        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();
        // dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价
        int[][] dp = new int[chs1.length + 1][chs2.length + 1];
        // 填写第一列,那么就是str1[0~i] -> ""
        for(int i = 1; i <= chs1.length; i++) {
            dp[i][0] = i * dc;
        }
        // 填写第一行,那么就是"" -> str2[0~j]
        for(int j = 1; j <= chs2.length; j++) {
            dp[0][j] = j * ic;
        }
        for(int i = 1; i <= chs1.length; i++) {
            for(int j = 1; j <= chs2.length; j++) {
                if(chs1[i - 1] == chs2[j - 1]) {
                    // 说明str1[0~i-1] -> str2[0~j-1]只需要
                    // str1[0~i-2] -> str2[0~j-2]的代价,也就是dp[i - 1][j - 1]
                    dp[i][j] = dp[i - 1][j - 1];
                }else {
                    // 若当前字符不等,则有三种情况
                    // 1、先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1]
                    // 因此此时花销为dp[i - 1][j] + dc
                    int min1 = dc + dp[i - 1][j];
                    // 2、先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等
                    // 因此此时花销为ic + dp[i][j - 1]
                    int min2 = ic + dp[i][j - 1];
                    dp[i][j] = Math.min(min1, min2);
                    // 3、先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等
                    // 因此此时花销为dp[i - 1][j - 1] + rc
                    int min3 = dp[i - 1][j - 1] + rc;
                    dp[i][j] = Math.min(dp[i][j], min3);
                }
            }
        }
        return dp[chs1.length][chs2.length];
    }

总结

其实动态规划,也可以简单的认为是一个填表的过程,找出转换方程后,我们先填写显而易见的初始数值,然后再根据初始数字推出下一行的数据,直至整张表填完。然而有些动态规划的题目,不是那么直接的填表,例如,零钱问题的最优解。因此,下一篇就动态规划的读表展开~

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

推荐阅读更多精彩内容

  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,275评论 0 18
  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,644评论 0 89
  • 富豪被查出绝症后,在医院狂撒现金,称命都没了要钱何用? 这是绝望而歇斯底里撒钱的情景……当一个人生命走到尽头,不能...
    清風长春阅读 1,007评论 0 0
  • 2017年7月29日天气晴星期六 我特别特别开心,因为我得了奖品手表、电子书、奖状,我还要努力。
    琦琦花仙子小月阅读 193评论 2 3