注:此篇宏观看待动态规划问题(分步解决问题)
日升时奋斗,日落时自省
目录
1、斐波那契数列模型(爬楼梯)
2、路径问题(地下城)
3、简单多状态问题(买卖股票IV)
4、子数组系列(单词拆分)
5、子序列问题(最长等差数列)
6、回文串问题(回文子串)
7、两个数组的dp问题(最长重复子数组)
8、01背包问题(分割等和子集)
9、完全背包问题(零钱兑换II)
1、斐波那契数列模型(爬楼梯)
来源力扣:746. 使用最小花费爬楼梯 - 力扣(LeetCode)
圈了这么几个地方,就是想要到最后一个格子的时候需要的最小花费
状态表示:dp[i]表示第i个位置,最小花费 (题上想要啥满足就是)
状态转移方程:
初始化(最后两个位置):dp[m-1]=cost[m-1];dp[m-2]=cost[m-2];
前者的数据需要的后者来支持,所以这里初始化后面的数据,我们从后面开始动态规划
填表顺序:
从右往左
返回值:min(dp[0],dp[1]) 因为我们可能是从0或者1出发的,所以这里取0或者1最小值
代码:
public int minCostClimbingStairs(int[] cost) {int m=cost.length;int[] dp=new int[m];dp[m-1]=cost[m-1];dp[m-2]=cost[m-2];for(int i=m-3;i>=0;i--){dp[i]=Math.min(dp[i+1],dp[i+2])+cost[i];}return Math.min(dp[0],dp[1]);}
2、路径问题(地下城)
来源力扣:174. 地下城游戏 - 力扣(LeetCode)
状态分析:
状态表示(明显是一个二维表):
错误分析:dp[i][j]表示从起点出发,到[i][j]位置的时候,所需血量最小
正确分析:dp[i][j]:表示从[i,j]出发,到终点
初始化:
边界初始化,上图已经给了
填表顺序:
从下往上,从有往左
返回值:dp[0][0]
代码:
public int calculateMinimumHP(int[][] dungeon) {int n=dungeon.length;int m=dungeon[0].length;int[][] dp=new int[n+1][m+1];for(int i=0;i<m-1;i++){dp[n][i]=Integer.MAX_VALUE;}for(int j=0;j<n-1;j++){dp[j][m]=Integer.MAX_VALUE;}dp[n-1][m]=1;dp[n][m-1]=1;for(int i=n-1;i>=0;i--){for(int j=m-1;j>=0;j--){dp[i][j]=Math.min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];dp[i][j]=Math.max(1,dp[i][j]);}}return dp[0][0];}
3、简单多状态问题(买卖股票IV)
来源力扣:188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
状态分析:
f[i][j](表示买入状态): 到第i天 ,完成j次交易
g[i][j](表示卖出状态):到第i天,完成j次交易
状态转移方程:
状态转移:
f[i][j]=max(f[i-1][j],g[i-1][j]-p[i]);
由两部分构成
一部分前一天啥也不做还是买入状态就是 i-1,次数还是当前次数j;
另一部分前一天卖出次数仍然没有改变(买入态)i-1,次数还是当前次数j;
g[i][j]=max(f[i-1][j-1]+p[i],g[i-1][j]);
由两部分构成
一部分前一天啥也不做还是卖出状态就是 i-1,次数还是当前次数j;
另一部分前一天买入次数改变状态(卖出态)i-1,次数+1,我们需要的是上一次的j-1;
初始化:
填表顺序:
从上往下,从左往右
返回值:需要找到最后一行的最大值,相当于是最后一天(没有交易,交易1次....交易n次的结果)
代码:
public int maxProfit(int k, int[] prices) {int n=prices.length;int[][] f=new int[n][k+1];int[][] g=new int[n][k+1];f[0][0]=-prices[0];g[0][0]=0;//初始化for(int i=1;i<3;i++){f[0][i]=-0x3f3f3f;g[0][i]=-0x3f3f3f;}//状态转义for(int i=1;i<n;i++){for(int j=0;j<k+1;j++){f[i][j]=Math.max(f[i-1][j],g[i-1][j]-prices[i]);g[i][j]=g[i-1][j];if(j-1>=0){g[i][j]=Math.max(g[i][j],f[i-1][j-1]+prices[i]);}}}int ret=0;//从最后一行动规中找最大值for(int i=0;i<k+1;i++){ret=Math.max(ret,g[n-1][i]);}return ret;}
4、子数组系列(单词拆分)
来源力扣:139. 单词拆分 - 力扣(LeetCode)
状态分析:
状态转义方程:
决定dp[i]的就是两部分是否为 true
dp[j-1]&& substring(j,i+1)
但是为了判断方便这里将单词存到hash表中为了判断截取是否包含单词
初始化:
开始的时候dp[0]就是空串,啥也没有同样为真,因为单词序列中也没有 dp[0]=true
填表顺序:
从左往右
返回值:dp[n]
代码:
public boolean wordBreak(String s, List<String> wordDict) {/** 优化 将字典里面的单词存到哈希表中* */Set<String> hash=new HashSet<>(wordDict);int n=s.length();boolean[] dp=new boolean[n+1];dp[0]=true;s=" "+s; //加空串是为了动规 不用去处理下标映射问题for(int i=1;i<=n;i++){//第二次for循环没有方向要求, j=1; j<=i ;j++ 也行for(int j=i;j>=1;j--){if(dp[j-1]&&hash.contains(s.substring(j,i+1))){dp[i]=true;break;}}}return dp[n];}
5、子序列问题(最长等差数列)
注:子序列和子数组是不一样的
子序列是不一定连续的子集,子数组是连续的子集
来源力扣:1027. 最长等差数列 - 力扣(LeetCode)
状态分析:
动态转义方程:
那这么才能知道k下标的数字是不是在数组中,那就把下标和数字关联起来,来一个哈希关联起来
dp[i][j]=dp[2b-c][i]+1
然后记录数据
初始化:等差数列至少需要3个的,基本数量为2,a需要b、c才能确定
填表顺序:从前往后,因为第三个数据需要依赖前两个值才能确定找不找到
返回值:统计的个数
代码:
public int longestArithSeqLength(int[] nums) {Map<Integer,Integer> hash=new HashMap<>();//首先将第一个值hash.put(nums[0],0);int n=nums.length;int[][] dp=new int[n][n];//进行初始化为 个数2for(int i=0;i<n;i++){Arrays.fill(dp[i],2);}int ret=2; //计算个数为2for(int i=1;i<n;i++){ //相当于 b//第二个值从 b位置开始 j的位置相当于cfor(int j=i+1;j<n;j++){//计算 a 的 2b-cint a=2*nums[i]-nums[j];//此时判断 hash 是否包含 a if(hash.containsKey(a)){dp[i][j]=dp[hash.get(a)][i]+1;//计算次数ret=Math.max(dp[i][j],ret);}}//将本次数字添加到hashhash.put(nums[i],i);}return ret;}
6、回文串问题(回文子串)
来源力扣:647. 回文子串 - 力扣(LeetCode)
状态分析:
状态转义方程:
根据状态分析写
如果s[i]!=s[j] 说明dp[i][j]==false;这个基本不用写
后面三个条件可以直接合成一个三目运算
如果s[i]==s[j]则 dp[i][j]=i+1<j?dp[i+1][j-1]:true
如果i+1<j 说明不是相邻也不是重叠那就需要区间[i+1,j-1]是true
初始化:不用初始化
填表顺序:其实需要看状态分析 中的dp[i][j]可能会依赖到dp[i+1][j-1]
从下往上,从左往右 i表示行 j表示列
返回值:统计有多少个子串,把dp中为true都计数一下
代码:
public int countSubstrings(String s) {int n=s.length();//创建后默认为false 为false的情况不用处理boolean[][] dp=new boolean[n][n];int ret=0;for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++){//只需要相等设置为trueif(s.charAt(i)==s.charAt(j)){dp[i][j]=i+1<j?dp[i+1][j-1]:true;}//如果是true说明本身计数if(dp[i][j]==true){ret++;}}}return ret;}
注:为啥第二次for循环从i开始 因为j等于>=i所以j从i开始
7、两个数组的dp问题(最长重复子数组)
力扣来源:718. 最长重复子数组 - 力扣(LeetCode)
状态分析:
状态转义方程:
如果两个数组数值相同的话
不等的情况下就不用处理了,相等的话满足动规dp[i][j]=dp[i-1][j-1]+1
初始化:
填表顺序:
从上往下,从左往右
返回值:统计的个数
代码:
public int findLength(int[] nums1, int[] nums2) {int m=nums1.length;int n=nums2.length;int[][] dp=new int[m+1][n+1];//计数器int ret=0;for(int i=1;i<=m;i++){for(int j=1;j<=n;j++){//如果两个值相等的话if(nums1[i-1]==nums2[j-1]){//相当于个数加一dp[i][j]=dp[i-1][j-1]+1;//求取最大长度ret=Math.max(ret,dp[i][j]);}}}return ret;}
8、01背包问题(分割等和子集)
01背包问题:就是满足于
牛客来源:416. 分割等和子集 - 力扣(LeetCode)
状态分析:
状态转义方程:
初始化:看见 dp[i-1][j-num[i]],需要依赖上一行,所以需要一个扩展行
填表顺序:
从左往右,从上往下
返回值:dp[n][sum/2]
代码:
public boolean canPartition(int[] nums) {int n=nums.length;int sum=0;//求所有和for(int x:nums)sum+=x;//如果是奇数 就没有相等的情况if(sum%2==1)return false;//求子集的一半int aim=sum/2;//行要多一行 aim 可以不加1 这里只是为了方便理解boolean[][] dp=new boolean[n+1][aim+1];//一行所有的值 为 truefor(int i=0;i<=n;i++){dp[i][0]=true;}for(int i=1;i<=n;i++){//从 i 开始 避开开始的位置for(int j=1;j<=aim;j++){//不选择 第i个位置 dp[i][j]=dp[i-1][j];//选择第i个位置 需要条件的 当前值 大于 数值if(j>=nums[i-1]){//选i 和 不选i 的情况 只要能构成就可以子集一半就行dp[i][j]=dp[i][j]||dp[i-1][j-nums[i-1]];}}}return dp[n][aim];}
优化:
代码:
简单点说:直接把行都删了就行 (这里算是优化版的优化,直接让数组从大于nums[i-1]的位置开始的就不用判断j>nums[i-1])
public boolean canPartition(int[] nums) {int n=nums.length;int sum=0;for(int x:nums)sum+=x;if(sum%2==1)return false;int aim=sum/2;boolean[] dp=new boolean[aim+1];dp[0]=true;for(int i=1;i<=n;i++){for(int j=aim;j>=nums[i-1];j--){dp[j]=dp[j]||dp[j-nums[i-1]];}}return dp[aim];}
9、完全背包问题(零钱兑换II)
力扣来源:518. 零钱兑换 II - 力扣(LeetCode)
状态分析:
状态转义方程:
但是 能不能选 i 是需要满足条件的 满足条件就是 j-n*coins>=0 就能满足
初始化:
填表顺序:从左往右,从上往下
返回值: dp[n][amount]
代码:
public int change(int amount, int[] coins) {int n=coins.length;int[][] dp=new int[n+1][amount+1];//设置第一个值 为 1dp[0][0]=1;for(int i=1;i<=n;i++){//从 0 开始 for(int j=0;j<=amount;j++){//不选 idp[i][j]=dp[i-1][j];//选 i 的时候 就需要 j>=coins 才能加上if(j>=coins[i-1]){dp[i][j]+=dp[i][j-coins[i-1]];}}}return dp[n][amount];}
空间优化:
简单点说,去掉行,填表顺序是从左往右
代码:
public int change(int amount, int[] coins) {int n=coins.length;int[] dp=new int[amount+1];dp[0]=1;for(int i=1;i<=n;i++){for(int j=coins[i-1];j<=amount;j++){dp[j]+=dp[j-coins[i-1]];}}return dp[amount];}