【算法】动态规划(三)

1、前言

如上一篇文章结尾,提到的动态规划读表,本文就围绕动态规划读表展开。

2、零钱问题

题目

考虑仅用1分、5分、10分、25分和50分这5种硬币支付某一个给定的金额。
例如需要支付11分钱,
有一个1分和一个10分、
一个1分和两个5分、
六个1分和一个5分、
十一个1分这4种方式。
请写一个程序,
1)计算一个给定的金额有几种支付方式。
2)使用硬币最少的数量
3)使用硬币最少的数量时的组合
注:假定支付0元有1种方式

要求1,2就是我们之前遇到的动态规划,只要结果,不求过程。而3的提问,就是索求过程,由于我们已经记录了整个递推的流程,因此,我们可以按照一定的规律找到整个流程,后面再说。

1)计算一个给定的金额有几种支付方式

暴力递归版本

    public static long exchange1(int[] coins, int aim) {
        return process(coins, 0, aim, 0);
    }
    // index代表取arr[index]的数,进行取1张,2张,3张时情况的枚举
    public static long process(int[] coins, int index, int aim, int alreadySum) {
        long res = 0;
        if (alreadySum == aim) {
            return 1;
        }
        if (index == coins.length) {
            if (alreadySum == aim) {
                return 1;
            }else{
                return 0;
            }
        }

        // 最多有i张 coins[index]
        for(int i = 0; coins[index] * i <= aim; i++) {
            if (i * coins[index] + alreadySum <= aim) {
                res += process(coins, index + 1, aim, i * coins[index] + alreadySum);
            }else {
                break;
            }
            
        }
        return res;
    }

动态规划思路:
创建一个二维数组dp[coins.length][aim + 1],i, j代表在coins[0~i]范围,组成j的方法有几种。
那么dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]就是我们的递推式,表示拿了i这个硬币的方法组成j的方法 = 不拿这个i硬币的方法 + dp[i][j - coins[i]]

    public static long exchange(int[] coins, int aim) {
        // i,j代表在0~i范围,组成j的方法有几种
        long[][] dp = new long[coins.length][aim + 1];
        // 组成0元的肯定都有1种方法,填写第一列
        for(int i = 0; i < coins.length; i++) {
            dp[i][0] = 1;
        }
        // 填写第一行,当j == coins[0]的整数倍时,有1种方法
        for (int i = 1; i <= aim; i++) {
            if(i % coins[0] == 0){
                dp[0][i]=1;//第一行中能够被arr[0]整除的数,即可以被换钱,记为1
            }else{
                dp[0][i]=0;
            }

        }
        for(int i = 1; i < coins.length; i++) {
            for(int j = 1; j <= aim; j++) {
                // 不拿i这个货币
                dp[i][j] = dp[i - 1][j];
                // 拿i这个货币
                if (j - coins[i] >= 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                }
            }
        }
        return dp[coins.length - 1][aim];
    }

2)使用硬币最少的数量

定义二维数组dp[coins.length][aim + 1],dp[i][j]表示在coins[0..i]组成j的最小硬币数量。
那么dp[i][j]可能来自两个值
1、不拿i这个硬币,那么dp[i][j]=dp[i - 1][j]
2、那i这个硬币,那么dp[i][j] = dp[i][j - coins[i]] + 1
然后取上面的较小值,就是dp[i][j]的值了

以coins=[1, 5, 10, 25, 50],aim=11作为例子,图解如下:


    public static int exchange3(int[] coins, int aim) {
        // dp[i][j]表示在coins[0..i]组成j的最小硬币数量
        int[][] dp = new int[coins.length][aim + 1];
        // 填写第一行,当j == coins[0]的整数倍时,有1种方法
        for (int i = 1; i <= aim; i++) {
            if(i % coins[0] == 0){
                dp[0][i] = i / coins[0];//第一行中能够被arr[0]整除的数,即可以被换钱,记为1
            }
        }
        for(int i = 1; i < coins.length; i++) {
            for(int j = 1; j <= aim; j++) {
                // 拿i这个货币
                if (j - coins[i] >= 0) {
                    int min2 = dp[i][j - coins[i]] + 1;
                    dp[i][j] = Math.min(dp[i - 1][j], min2);
                }else {
                    // 不拿i这个货币
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
    }

3)使用硬币最少的数量时的组合

由于存在以下转换方程:
1、不拿i这个硬币,那么dp[i][j]=dp[i - 1][j]
2、那i这个硬币,那么dp[i][j] = dp[i][j - coins[i]] + 1
然后取上面的较小值,就是dp[i][j]的值了

那么对于dp[i][j]可能是来自dp[i - 1],或者来自 dp[i][j - coins[i]] + 1,因此,我们先从i = coins.length - 1开始往上找,直至dp[i] != dp[i - 1],打印当前的coins[i]。
然后j - coins[j],继续往上找,直至i==0,图解如下,蓝色方块就是最优组合:


代码实现,在第二问的基础上,添加了寻找最佳组合的代码

    public static int exchange3(int[] coins, int aim) {
        // dp[i][j]表示在coins[0..i]组成j的最小硬币数量
        int[][] dp = new int[coins.length][aim + 1];
        // 填写第一行,当j == coins[0]的整数倍时,有1种方法
        for (int i = 1; i <= aim; i++) {
            if(i % coins[0] == 0){
                dp[0][i] = i / coins[0];//第一行中能够被arr[0]整除的数,即可以被换钱,记为1
            }
        }
        for(int i = 1; i < coins.length; i++) {
            for(int j = 1; j <= aim; j++) {
                // 拿i这个货币
                if (j - coins[i] >= 0) {
                    int min2 = dp[i][j - coins[i]] + 1;
                    dp[i][j] = Math.min(dp[i - 1][j], min2);
                }else {
                    // 不拿i这个货币
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        System.out.println("最佳组合:");
        int i = coins.length - 1;
        int j = aim;
        while(j >= 0 && i >= 0) {
            if (i > 0) {
                // 一直往上查找,直至i != i - 1
                while(dp[i][j] == dp[i - 1][j]) {
                    i--;
                    if (i == 0) {
                        break;
                    }
                }
                System.out.print(coins[i] + " ");
                j = j - coins[i];
                if (j <= 0) {
                    break;
                }
            }
        }
        System.out.println();
        return dp[coins.length - 1][aim];
    }

3、最长公共子序列

题目

给定两个字符串str1和str2,返回两个字符串的最长公共子序列。
例如,str1="1A2C3D4B56",str2="B1D23CA45B6A","123456"或者"12C4B6"都是
最长公共子序列,返回哪一个都行。

算法实现

创建一个二维数组dp[str1.length][str2.length],dp[i][j]代表,在str1[0..i]和str2[0..j]之间最长子序列长度
那么初始的第一列chs1[i] = str1[0~str1.length - 1],只要chs1[i]一旦=str2[0],那么[i+1~str1.length - 1]都将有dp[i][0]=1
同理,第一行一旦有chs2[i]=str1[0],那么[i+1~str2.length - 1]都将有dp[0][i]=1
初始化过程如下:

        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();
        // dp[i][j]代表,在str1[0..i]和str2[0..j]之间最长子序列长度
        int[][] dp = new int[chs1.length][chs2.length];
        int pre = 0;
        // 填充第一列
        for(int i = 0; i < chs1.length; i++) {
            if(pre == 1) {
                // 一旦之前有和str2[0]相等的字符,
                // 那么接下来的区间,dp[i][0]都等于1
                dp[i][0] = pre;
            }else {
                if(chs1[i] == chs2[0]) {
                    dp[i][0] = 1;
                    pre = 1;
                }
            }
        }
        // 填充第一行
        pre = 0;
        for(int i = 0; i < chs2.length; i++) {
            if(pre == 1) {
                // 一旦之前有和str2[0]相等的字符,
                // 那么接下来的区间,dp[i][0]都等于1
                dp[0][i] = pre;
            }else {
                if(chs2[i] == chs1[0]) {
                    dp[0][1] = 1;
                    pre = 1;
                }
            }
        }

那么对于非第一行,第一列的的dp[i][j]存在以下2种情况
1、当chs1[1] == chs2[2],dp[i][j] = dp[i - 1][j - 1] + 1
2、当chs1[1] != chs2[2], dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
对应的代码如下:

        for(int i = 1; i < chs1.length; i++) {
            for(int j = 1; j < chs2.length; j++) {
                // 若 chs1[i] == chs2[j],则在dp[i - 1][j - 1]基础上 + 1
                if (chs1[i] == chs2[j]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else {
                    // 若不等,则从dp[i - 1][j]和dp[i][j - 1]之间选一个最大的
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

这时,dp表已经填满了,接下来要开始读表了。由以上的递推式,我们可知,初始时,i=str1.len - 1,j=str2.len - 1。
1、dp[i][j]要一直与dp[i - 1][j]比较,直至dp[i][j] !=dp[i - 1][j]
2、dp[i][j]要一直与dp[i][j - 1]比较,直至dp[i][j] != dp[i][j - 1],此时的str2[j]就是其中一个子字符串。然后j再往左移动1位,再开始以上操作。
以str1="1A2C3D4B56",str2="B1D23CA45B6A"为例子,图解如下:


完整代码如下

    public static void commonLongestSeq(String str1, String str2) {
        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();
        // dp[i][j]代表,在str1[0..i]和str2[0..j]之间最长子序列长度
        int[][] dp = new int[chs1.length][chs2.length];
        int pre = 0;
        // 填充第一列
        for(int i = 0; i < chs1.length; i++) {
            if(pre == 1) {
                // 一旦之前有和str2[0]相等的字符,
                // 那么接下来的区间,dp[i][0]都等于1
                dp[i][0] = pre;
            }else {
                if(chs1[i] == chs2[0]) {
                    dp[i][0] = 1;
                    pre = 1;
                }
            }
        }
        // 填充第一行
        pre = 0;
        for(int i = 0; i < chs2.length; i++) {
            if(pre == 1) {
                // 一旦之前有和str2[0]相等的字符,
                // 那么接下来的区间,dp[i][0]都等于1
                dp[0][i] = pre;
            }else {
                if(chs2[i] == chs1[0]) {
                    dp[0][1] = 1;
                    pre = 1;
                }
            }
        }
        for(int i = 1; i < chs1.length; i++) {
            for(int j = 1; j < chs2.length; j++) {
                // 若 chs1[i] == chs2[j],则在dp[i - 1][j - 1]基础上 + 1
                if (chs1[i] == chs2[j]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else {
                    // 若不等,则从dp[i - 1][j]和dp[i][j - 1]之间选一个最大的
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        for(int i = 0; i < chs1.length; i++) {
            for(int j = 0; j < chs2.length; j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.println();
        }
        
        int i = chs1.length - 1;
        int j = chs2.length - 1;
        char[] results = new char[dp[i][j]];
        int k = dp[i][j] - 1;
        // 查找组成最长子序列的组合
        while(i >= 0 && j >= 0) {
            if(i > 0) {
                // 一直往上找,直至dp[i][j] != dp[i - 1][j]
                while(dp[i][j] == dp[i - 1][j]) {
                    i--;
                    if(i == 0) {
                        break;
                    }
                }
            }
            if (j > 0) {
                // j一直往左找,直至找到dp[i][j] != dp[i][j - 1]
                while(dp[i][j] == dp[i][j - 1]) {
                    j--;
                    if(j == 0) {
                        break;
                    }
                }
                results[k--] = chs2[j];
            }
            j--;
        }
        System.out.println(new String(results));
    }

总结

以上就是动态规划的读表题,其实不必特别在意如何推出找表的公式。基本上来说,就是一直往上找,然后再往左找(表述不好,请谅解)。那么,这就是动态规划的最后一篇了,想更加深刻的理解DP,只能做多点题目,增加点感觉了~

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

推荐阅读更多精彩内容

  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,279评论 0 18
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • 前言 上一篇,介绍了动态规划DP的概念,以及暴力递归和动态规划的关系。这一篇,将介绍几道经典的动态规划题 1、台阶...
    mapleYe阅读 810评论 0 0
  • 【程序1】 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一...
    阿里高级软件架构师阅读 3,286评论 0 19
  • MVP系列-Android平台-第1讲-初探MVP 内容一:什么是MVP?什么是MVC? 第一点:什么是MVP? ...
    Jason_儿阅读 351评论 0 2