动态规划
背包类的问题
组合类
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
public int combinationSum4(int[] nums, int target) {
int n=nums.length;
int []dp=new int[target+1]; //背包0算一个
dp[0]=1;
for (int i = 0; i <= target; i++) {
for (int j = 0; j < n; j++) {
if(i>=nums[j]){
dp[i]+=dp[i-nums[j]];
}
}
}
return dp[target];
}
我认为这里需要注意的不是什么遍历的顺序问题,不应该这么刻意去记
只需要记得在不考虑顺序的时候(组合)背包的循环应该在里面,这使得每一个货物只能遍历一次
而在考虑顺序的排列问题的时候,背包的遍历顺序应该在外面,然后货物的遍历在里面,
这是因为货物在里面在不同的背包大小的时候可以重复计算。
排列类问题
不区分
- 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
在这里插入代码片
public int coinChange(int[] coins, int amount) {
int max=Integer.MAX_VALUE;
int []dp=new int[amount+1];
for (int i = 0; i < amount+1; i++) {
dp[i]=max;
}
dp[0]=0; //因为始终上来说的话amount为0表示的是不用取,即方法为0
for (int i = 0; i < coins.length; i++) { //表示选取第几个物品
for (int j = coins[i]; j <= amount; j++) {
if(dp[j-coins[i]]!=max){
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
}
return dp[amount]==max ? -1:dp[amount];
}
在这里插入代码片
思路:我想说的是其实重点在于调试的过程,以及dp[i]的含义,首先dp[j]:
凑足总额为j所需钱币的最少个数为dp[j],然后注意的是在选定第i件物品的时候,
是从coin[i]开始的,然后以及选定了coin[i]的物品进背包后,就相当于从dp[j-coin[i]]
开始放置物品的,那么此时的话,需要注意的是当dp[j-coins[i]]!=max是没有意义的,
因为之前的容量下无法放下东西,最后得到的递归方程式是
:dp[j]=Math.min(dp[j],dp[j-coins[i]]+1)
在这里我意识到了什么时候应该取矩阵的维度是n+1,就是说,当你的背包数为0占一个位置的时候需要取n+1
股票类问题
编辑距离类问题
值得注意的是在编辑距离相关的问题中,其实dp[][]的下标是可以从i,j开始的,只不过不是很方便,因为如果dp[i][j]代表的是下标为i和j的字符串之间的距离的话,会导致dp[0][0] ,以及相应的dp[i][0]需要在初始化的时候增加一个判断,因为如果s.chatAt(i)==t.charAt(0)的时候这意味着dp[i][0]的值为1,而如果以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j],就不需要在初始化的时候考虑这个问题,因为此时的话,dp[i][1]才是表示的i-1与0的距离,可以从dp[i-1][0]开始进行递推。
在这里插入代码片
这种类型的题目的话,一般都是当两者相等的时候一个关系,然后就是else的时候又是一个关系,递推就完事了,但这里
最主要的就是理解dp数组的含义是什么,dp[i][j]通常表示的是字符串之间的转换关系,可以是匹配关系什么的,有多少
个子序列这种,然后可以通过递推进行。初始化的条件通常是重要的,这与实际的情况有关,例如不同的子序列这道题。
115.不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
在这里插入代码片
public int numDistinct(String s, String t) {
int m=s.length();
int n=t.length();
int [][]dp=new int[m+1][n+1];
for (int i = 0; i <=m; i++) {
dp[i][0]=1; //可以通过删除s的值达到t
}
for (int i = 1; i <=m ; i++) {
for (int j = 1; j <= n; j++) {
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j];//只能通过前面的s来进行匹配了
}
}
}
return dp[m][n];
}
在这里插入代码片
思路:dp[i][j]的含义是有s转成t的方法的个数,则初始化的时候,要先
for (int i = 0; i <=m; i++) {
dp[i][0]=1; //可以通过删除s的值达到t
}
这是因为可以对s进行删除得到t;
s.charAt(i-1)==t.charAt(j-1)的时候,可以得到dp[i][j]
其值等于dp[i-1][j]+dp[i-1][j-1],因为第一个dp[i-1][j]代表的是把当前的s[i-1]
删去,看看之前是否可以匹配t[j-1]的,dp[i-1][j-1],还有就是不考虑当前的即
不需要考虑当前s子串和t子串的最后一位字母的情况,因为当前这一位一定可以匹配了。
最长连续公共子序列(利扣自己的改进版)
最长连续公共子序列(Longest Common Substring)问题是指在两个字符串中找到一个最长的子串,使得这个子串在两个字符串中都出现过,并且连续出现。
以下是求解最长连续公共子序列的一种动态规划算法:
假设给定两个字符串 A 和 B,它们的长度分别为 m 和 n。我们可以定义一个二维数组 dp,其中 dp[i][j] 表示以 A[i-1] 和 B[j-1] 结尾的最长连续公共子序列的长度。这里的 A[i-1] 和 B[j-1] 表示字符串 A 和 B 中从 0 开始的第 i-1 和 j-1 个字符。
动态规划的递推式如下:
当 i=0 或 j=0 时,dp[i][j] = 0。
当 A[i-1] = B[j-1] 时,dp[i][j] = dp[i-1][j-1] + 1。
当 A[i-1] ≠ B[j-1] 时,dp[i][j] = 0。
最长连续公共子序列的长度就是数组 dp 中的最大值,最长连续公共子序列本身可以通过回溯数组 dp 来构造。
在这里插入代码片
public static int longestCommonSubstring(String A, String B) {
int m = A.length();
int n = B.length();
int[][] dp = new int[m+1][n+1];
int maxLength = 0;
// 动态规划求解最长连续公共子序列
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (A.charAt(i-1) == B.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
maxLength = Math.max(maxLength, dp[i][j]);
} else {
dp[i][j] = 0;
}
}
}
return maxLength;
// return dp[m][n];
}
最短公共超序列(每日一题)
给出两个字符串 str1 和 str2,返回同时以 str1 和 str2 作为子序列的最短字符串。如果答案不止一个,则可以返回满足条件的任意一个答案。
输入:str1 = “abac”, str2 = “cab”
输出:“cabac”
解释:
str1 = “abac” 是 “cabac” 的一个子串,因为我们可以删去 “cabac” 的第一个 "c"得到 “abac”。
str2 = “cab” 是 “cabac” 的一个子串,因为我们可以删去 “cabac” 末尾的 “ac” 得到 “cab”。
最终我们给出的答案是满足上述属性的最短字符串。
在这里插入代码片
思路:相当于先计算最长的公共子串,dp[i][j]表示的是最长的公共子串的长度,计算应该很简单,然后再进行判断dp[i][j]与dp[i-1][j],dp[i][j-1],dp[i-1][j-1]之间的关系,其中dp[i-1][j]表示的是将i-2为下标的情况下和i-1为下标的情况一致,这种情况的话表明就是i-1是没有出现过的,应该加上它;dp[i][j-1]的原理一致。
在这里插入代码片
public class SuperSequece {
public String shortestCommonSupersequence(String str1, String str2) {
int m=str1.length();
int n=str2.length();
int [][]dp=new int[m+1][n+1];
for (int i = 1; i <=m ; i++) {
for (int j = 1; j <=n ; j++) {
if(str1.charAt(i-1)==str2.charAt(j-1)){ //最长公共子序列
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
int i=m;
int j=n;
StringBuffer sb=new StringBuffer();
while (i>0 || j>0){
if(i==0){
j--;
sb.append(str2.charAt(j));
} else if (j==0) {
i--;
sb.append(str1.charAt(i));
} else {
if (dp[i][j]==dp[i-1][j]) { //意味着i-1下标没有出现过
i--;
sb.append(str1.charAt(i));
}else if(dp[i][j]==dp[i][j-1]){
j--;
sb.append(str2.charAt(j));
}else {
i--;
j--;
sb.append(str1.charAt(i));
}
}
}
return sb.reverse().toString();
}
public static void main(String[] args) {
String str1="abac";
String str2="cab";
SuperSequece superSequece=new SuperSequece();
System.out.println(superSequece.shortestCommonSupersequence(str1,str2));
}
}
序列DP类题目
一开始使用复杂度为n3的动态规划导致超时了,后来以空间换时间,将每一个数据和索引存在hashmap中,使得在寻找符合要求的l的时候就很方便了,直接在hashmap中查找,找到的话直接返回索引,找不到就是-1
dp[j][l]=dp[l][i]+1;
且l在i和j之间
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n=arr.length;
int [][]dp=new int[n][n];
int max=0;
HashMap<Integer,Integer> hashMap=new HashMap<>();
for (int i = 0; i < n; i++) {
hashMap.put(arr[i],i);
}
for (int i = 0; i < n; i++) {
for (int j = i+2; j <n ; j++) {
int l=hashMap.getOrDefault(arr[j]-arr[i],-1);
if(i<l && l<j){
dp[j][l]=dp[l][i]+1;
max=Math.max(max,dp[j][l]);
}
}
}
return max==0 ? 0:max+2;
}
}