Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【算法】动态规划(三)

【算法】动态规划(三)

作者头像
MapleYe
发布于 2020-03-28 12:57:47
发布于 2020-03-28 12:57:47
51000
代码可运行
举报
文章被收录于专栏:MapleYeMapleYe
运行总次数:0
代码可运行

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)计算一个给定的金额有几种支付方式

暴力递归版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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]]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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作为例子,图解如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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,图解如下,蓝色方块就是最优组合:

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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 初始化过程如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        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]); 对应的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        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"为例子,图解如下:

完整代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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,只能做多点题目,增加点感觉了~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【动态规划】心有惊雷,生似静湖 - 10. 完全背包问题
题目内容: 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
用户11369350
2025/02/18
820
【动态规划】心有惊雷,生似静湖 - 10. 完全背包问题
【算法】动态规划(二)
上一篇,介绍了动态规划DP的概念,以及暴力递归和动态规划的关系。这一篇,将介绍几道经典的动态规划题
MapleYe
2020/03/28
4250
漫画:最长公共子序列
解释:一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串,如下图示:
kunge
2020/10/22
1K0
算法练习(25) - 动态规划:最长子序列
问题 求解两个数组的最长子序列 例如 : int[] x = {1,2,3,4,4,5} int[] y = {1,4,5} 最长子串为 : 1,4,5 code public class _05LCSTest { @Test public void LCS_test() { String[] s1 = {"1","3","4","5"}; String[] s2 = {"0","1","3","5"}; printLCS(s1,s2);
惊羽-布壳儿
2022/06/15
1640
LeetCode *1143. 最长公共子序列(动态规划)
确定DP数组含义: dp[i][j]表示str1[0..i-1]与str2[0..j-1]的LCS(最长公共子序列)长度为dp[i][j]。
SakuraTears
2022/01/13
1730
LeetCode *1143. 最长公共子序列(动态规划)
【愚公系列】2023年12月 五大常用算法(三)-动态规划算法
动态规划算法的基本思想是将原问题分解为若干个子问题,并先求解子问题,再根据子问题的解得到原问题的解。这种方法的优点在于避免了重复计算,从而提高了算法的效率。
愚公搬代码
2023/12/14
2571
动态规划最长公共子序列(LCS)问题(Java实现)
动态规划最长公共子序列(LCS)问题(Java实现) 首先,明白一个公共子序列和公共子串的区别 公共子序列: 可以不连续 公共子串: 必须连续 问题分析 --- 求最长公共子序列,先明白两个概念 子序列 - 一个给定序列中删去若干元素后得到的序列 公共子序列 - 给定两个序列X,Y,当另一序列Z 既是X 的子序列,又是Y 的子序列时,就称Z 为X、Y 的公共子序列 明白上述两个概念后,我们就可以开始搜索最长公共子序列 这个问题可以使用暴力方法解决,但是由于要全部搜索一遍,时间复杂度为 O(n2<su
ruochen
2021/05/14
1.4K0
动态规划最长公共子序列(LCS)问题(Java实现)
动态规划 72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
MashiroT
2023/01/30
1700
dp经典问题
输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
Tim在路上
2020/08/04
4200
一文说清动态规划
动态规划(dynamic programming,简称 dp)是工程中非常重要的解决问题的思想,从我们在工程中地图软件上应用的最短路径问题,再在生活中的在淘宝上如何凑单以便利用满减券来最大程度地达到我们合理薅羊毛的目的 ,很多时候都能看到它的身影。不过动态规划对初学者来说确实比较难,dp状态,状态转移方程让人摸不着头脑,网上很多人也反馈不太好学。其实任何算法的学习都是有它的规律和套路的,只要掌握好它的规律及解题的套路,再加上大量的习题练习,相信掌握它不是什么难事。本文将会用比较浅显易懂地讲解来帮助大家掌握动态规划这一在工程中非常重要的思想,相信看完后,动态规划的解题套路一定能手到擒来(文章有点长,建议先收藏再看,看完后一定会对动态规划的认知上升到一个台阶!)
Crossin先生
2020/02/25
7980
牛逼了,原来大神都是这样学动态规划的...
动态规划(dynamic programming,简称 dp)是工程中非常重要的解决问题的思想,从我们在工程中地图软件上应用的最短路径问题,再在生活中的在淘宝上如何凑单以便利用满减券来最大程度地达到我们合理薅羊毛的目的 ,很多时候都能看到它的身影。
五分钟学算法
2020/02/25
1.8K0
C++ 算法进阶系列之聊聊动态规划的两把刷子
递归和动态规划是算法界的两个扛把子,想进入算法之门,则必须理解、掌握这两种算法的本质。一旦参悟透这2种算法的精髓,再加上对树、图等复杂数据结构的深入理解,可以解决大部分的算法问题。
一枚大果壳
2023/08/18
2540
C++ 算法进阶系列之聊聊动态规划的两把刷子
LeetCode中级算法-动态规划
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
码农帮派
2021/01/12
4680
从暴力递归到动态规划
 动态规划没有那么难,但是很多老师在讲课的过程中讲的并不好,由此写下一篇文章记录学习过程
mathor
2018/09/19
6990
从暴力递归到动态规划
常用算法思想之动态规划的后缀思想
思路:后缀是指要解决的子问题是原问题的后半部分,如果用字符串类描述,相当于子问题永远都是原问题的后半部分 str[i:]
爬蜥
2024/02/19
1600
常用算法思想之动态规划的后缀思想
TypeScript 实战算法系列(十):实现动态规划
前面的一系列文章跟大家分享了各种数据结构和算法的实现,本文将分享一些算法的设计技巧:分而治之、动态规划,使用这些技巧可以借算法来解决问题,提升自己解决问题的能力,欢迎各位感兴趣的开发者阅读本文。
一只图雀
2020/09/10
9040
【动态规划】完全背包问题
完全背包和 01 背包不同的就是每个物品可以选任意多次,01 背包是只能选 1 次或者不选,这道题也是分为恰好装满和可以不装满两个问题
2的n次方
2024/11/21
1330
【动态规划】完全背包问题
万字长文!动态规划的终极难题:字符匹配类
这些问题大多在 LeetCode 上面标的都是 hard 难度,弄清楚了这些套路后,回过头去看看推导过程,然后再看看二、三十行的代码量,不知道是否能给你一些新的感悟和认识?
五分钟学算法
2019/12/02
7630
Leetcode | 第一节:动态规划(上)
从去年7月到现在,我已经在北京的互联网公司呆了整整一年的时间。这中间经历过各种各样的酸甜苦辣,自己为了面试刷题的过程(从杉数到滴滴——未入门算法工程师再找实习工作记),也会经常听到北美同学面试的时候所遇到的各种艰难。是的,只要是互联网公司,无论是国内还是国外,总是要考察很多leetcode的东西。而leetcode如何刷,刷多少,刷到什么程度,其实各个公司也各不相同。但是事实上,leetcode的本质考察点是算法与数据结构,而除去基本的算法与数据结构外,leetcode困难的地方在于熟练度+一些技巧。然而技巧毕竟是存量,不是增量,我们刷多了,自然就有经验。所以这一个系列,我们不面向easy的题目,而更多关注hard和medium+的高频题,并通过大量的leetcode原题,来刻画出互联网公司究竟会考察哪些实际算法与数据结构的知识,以达到复习《算法与数据结构》的效果。
学弱猹
2021/08/10
6730
Leetcode | 第一节:动态规划(上)
看一遍就理解:动态规划详解
我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~
捡田螺的小男孩
2021/04/23
5960
看一遍就理解:动态规划详解
相关推荐
【动态规划】心有惊雷,生似静湖 - 10. 完全背包问题
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验