笔记来自代码随想录,题目加自己题解。
动态规划五部曲
确定dp数组下标含义
确定递推公式(状态转移)
dp数组初始化
确定遍历顺序
举例推导
一周目 基础题目
二周目 背包问题 只讲两个关键问题:01背包和完全背包。
两种dp数组:二维dp和一维dp(滚动数组、空间优化)
两种遍历方式:物品遍历和背包遍历;前向遍历和后向遍历。
01背包问题 从最简单的01背包问题讲起。有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次 ,求解将哪些物品装入背包里物品价值总和最大。
dp[i][v]
表示从前i(0~i)件物品中任意选取,装入容量为v的背包,能获得的最大价值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 for (int j = 0 ; j < weight[0 ]; j++) dp[0 ][j] = 0 ; for (int j = weight[0 ]; j <= bagweight; j++) dp[0 ][j] = value[0 ]; for (int i = 1 ; i < weight.size (); i++) { for (int j = 0 ; j <= bagweight; j++) { if (j < weight[i]) dp[i][j] = dp[i - 1 ][j]; else dp[i][j] = max (dp[i - 1 ][j], dp[i - 1 ][j - weight[i]] + value[i]); } }
二维数组的空间优化:滚动数组 可以从递推公式中看到,第i个物品的状态只与第i-1个物品的状态相关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 for (int i = 1 ; i <= n; i++) { for (int v = V; v >= w[i]; v--) { dp[v] = max (dp[v],dp[v-w[i]] + c[i]); } } void test_1_wei_bag_problem () { vector<int > weight = {1 , 3 , 4 }; vector<int > value = {15 , 20 , 30 }; int bagWeight = 4 ; vector<int > dp (bagWeight + 1 , 0 ) ; for (int i = 0 ; i < weight.size (); i++) { for (int j = bagWeight; j >= weight[i]; j--) { dp[j] = max (dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagWeight] << endl; }
思考:能不能先遍历背包容量,再遍历物品呢?即改变两个for循环的位置
答案是不行,改变位置后,每个dp【j】只会放入价值最高的物品,只有一个物品。(j被i重复迭代,只会放入价值最高物品)
相关题目 分割等和子集 https://leetcode.cn/problems/partition-equal-subset-sum/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public boolean canPartition (int [] nums) { int sum = 0 ; for (int num: nums) sum+=num; if (sum%2 ==1 ) return false ; int bagWeight = sum/2 ; int [][] dp = new int [nums.length][bagWeight+1 ]; for (int j=0 ; j<=bagWeight; ++j){ if (j>=nums[0 ]) dp[0 ][j] = nums[0 ]; else dp[0 ][j] = 0 ; } for (int i=1 ; i<nums.length; ++i){ for (int j=0 ; j<=bagWeight; ++j){ if (j<nums[i]) dp[i][j] = dp[i-1 ][j]; else dp[i][j] = Math.max(dp[i-1 ][j], dp[i-1 ][j-nums[i]]+nums[i]); } } return dp[nums.length-1 ][bagWeight]==bagWeight? true :false ; } }
背包容量为集合总和的一半,每个元素可以选取一次,就相当于01背包
第一次做有点抽象,细想,求背包能装的最大容量,肯定会遍历所有元素
最后一块石头的重量 https://leetcode.cn/problems/last-stone-weight-ii/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 lass Solution { public int lastStoneWeightII (int [] stones) { int sum = 0 ; for (int stone: stones) sum+=stone; int bagWeight = sum/2 ; int [][] dp = new int [stones.length][bagWeight+1 ]; for (int j=0 ; j<=bagWeight; ++j) if (j>=stones[0 ]) dp[0 ][j] = stones[0 ]; for (int i=1 ; i<stones.length; ++i){ for (int j=0 ; j<=bagWeight; ++j){ if (j < stones[i]) dp[i][j] = dp[i-1 ][j]; else dp[i][j] = Math.max(dp[i-1 ][j], dp[i-1 ][j-stones[i]]+stones[i]); } } return sum-2 *dp[stones.length-1 ][bagWeight]; } }
这题体现了循序渐进的作用了,如果没做分割等和子集这道题,可能想不到
实际上就是分为重量接近的两堆石头,两个石头相撞后大的石头还是留在原来那一堆
目标和 https://leetcode.cn/problems/target-sum/‘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int findTargetSumWays (int [] nums, int target) { int sum = 0 ; for (int num: nums) sum+=num; if ((sum+target)%2 ==1 ) return 0 ; if (sum < Math.abs(target)) return 0 ; int bagWeight = (sum+target)/2 ; int [][] dp = new int [1 +nums.length][bagWeight+1 ]; dp[0 ][0 ] = 1 ; for (int i=1 ; i<= nums.length; ++i){ for (int j=0 ; j<=bagWeight; ++j){ if (j>=nums[i-1 ]) dp[i][j] = dp[i-1 ][j]+dp[i-1 ][j-nums[i-1 ]]; else dp[i][j] = dp[i-1 ][j]; } } System.out.println(Arrays.deepToString(dp)); return dp[nums.length][bagWeight]; } }
还是循序渐进的作用。初看题目?可能没什么思绪。其实还是分成两部分,一部分为+号,另一部分为-号,通过数学运算就能得出背包的容量
这道题不同的地方在于求数目。装满背包有多少种方法。
dp【j】 = dp【j】+dp【j-num【i】】 ,不选物品的方法数+选物品的方法数
一和零 https://leetcode.cn/problems/ones-and-zeroes/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { public int zeros (String s) { int count = 0 ; for (int i=0 ; i<s.length(); ++i) if (s.charAt(i) == '0' ) count++; return count; } public int ones (String s) { int count = 0 ; for (int i=0 ; i<s.length(); ++i) if (s.charAt(i) == '1' ) count++; return count; } public int findMaxForm (String[] strs, int m, int n) { int [][] dp = new int [1 +m][1 +n]; int oneNum = ones(strs[0 ]); int zeroNum = zeros(strs[0 ]); for (int k=0 ; k<strs.length; ++k){ int onex = ones(strs[k]); int zerox = zeros(strs[k]); for (int i=m; i>=zerox; --i) for (int j=n; j>=onex; --j) dp[i][j] = Math.max(dp[i][j], 1 +dp[i-zerox][j-onex]); } System.out.println(Arrays.deepToString(dp)); return dp[m][n]; } }
这道题相当于两个维度的背包,0的个数是一个维度,1的个数是一个维度
又因为一个字符串是一起放入的,所以状态转移的时候少了很多状态
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
完全背包问题 在完全背包问题中,每种物品有无限个,可以无限选取。写成二维形式的话,取了i件物品还能再取。求将哪些物品装入背包里物品价值总和最大。
dp[i][v] = max(dp[i-1][v], dp[i][i-w[i]]+c[i])
简化为 一维形式的话,公式和01背包一摸一样,不同的是正向枚举 。
1 2 3 4 5 for (int i = 1 ; i <= n; i++) { for (int v = w[i]; v <=V; v++) { dp[v] = max (dp[v], dp[v - w[i]] + c[i]; } }
理解正向枚举:
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
1 2 3 4 5 6 7 8 for (int j = 0 ; j <= bagWeight; j++) { for (int i = 0 ; i < weight.size(); i++) { if (j - weight[i] >= 0 ) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } cout << endl ; }
相关题目 组合总和 https://leetcode.cn/problems/combination-sum-iv/
1 2 3 4 5 6 for (int i=1 ; i<target; ++i){ for (int num:nums){ if (i-num>=0 ) dp[i] = dp[i] + dp[i-num]; } }
初始时,dp[i]为0,dp[i-num]可以看成最后一步跳num步台阶到达第i阶楼梯的方法数,这样就保证了分成的子问题互相不重叠。
零钱兑换 https://leetcode.cn/problems/coin-change-ii/description/
标准的完全背包,但不同的是求装满背包的组合数
该怎么办呢?先遍历物品再遍历背包!
先遍历物品保证了背包装入物品的顺序性,怎么理解?
比如遍历到第i个物品的时候,我们看转移方程
1 2 3 4 5 for (int i = 0 ; i < coins.size (); i++) { for (int j = coins[i]; j <= amount; j++) { dp[j] = dp[j] + dp[j - coins[i]]; } }
转移方程的后半部分保证了在背包容量为j时,dp[j] + dp[j - coins[i]] 是由0。。。i-1等物品组成背包方法数。
完全平方数 https://leetcode.cn/problems/perfect-squares/
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
完全背包问题,但是要求最少数量 ,因此要初始化为INT MAX
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int numSquares (int n) { int [] dp = new int [1 +n]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0 ] = 0 ; for (int i=1 ; i<=n; ++i){ for (int j=1 ; j*j<=i; ++j){ dp[i] = Math.min(dp[i], 1 +dp[i-j*j]); } } return dp[n]; } }
单词拆分 https://leetcode.cn/problems/word-break/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public boolean wordBreak (String s, List<String> wordDict) { boolean [] dp = new boolean [s.length()+1 ]; dp[0 ] = true ; for (int i=1 ; i<=s.length(); ++i){ for (String word : wordDict){ if (word.length() <= i){ String postfix = s.substring(i-word.length(), i); if (postfix.equals(word)){ dp[i] = dp[i] || dp[i-word.length()]; } } } } return dp[s.length()]; } }
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
先遍历物品,再遍历背包
回溯法记忆搜索,待做//todo
三周目 打家劫舍+股票问题 打家劫舍系列问题 打家劫舍1 https://leetcode.cn/problems/house-robber/
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public int rob (int [] nums) { int [] dp = new int [nums.length]; if (nums.length<=1 ) return nums[0 ]; dp[0 ] = nums[0 ]; dp[1 ] = Math.max(nums[0 ], nums[1 ]); for (int i=2 ; i<nums.length; ++i){ dp[i] = Math.max(dp[i-1 ], dp[i-2 ]+nums[i]); } return dp[nums.length-1 ]; } }
只能隔着一家偷,因此 dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
打家劫舍2 https://leetcode.cn/problems/house-robber-ii/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int robRange (int [] nums, int l, int r) { int [] dp = new int [nums.length]; dp[l] = nums[l]; dp[l+1 ] = Math.max(nums[l], nums[l+1 ]); for (int i=l+2 ; i<r; ++i){ dp[i] = Math.max(dp[i-1 ], dp[i-2 ]+nums[i]); } return dp[r-1 ]; } public int rob (int [] nums) { if (nums.length==1 ) return nums[0 ]; if (nums.length==2 ) return Math.max(nums[0 ], nums[1 ]); int a1 = robRange(nums, 1 , nums.length); int a2 = robRange(nums, 0 , nums.length-1 ); return Math.max(a1, a2); } }
环形队列,考虑偷的起始点。假设编号为0、1、2..N,那么偷第0家后,第N家必定不能偷,问题可以转为为0到N-1的打家劫舍问题。
同样地,由于第0家可偷可不偷,不偷第0家,问题就是1到N的打家劫舍问题。
打家劫舍3 https://leetcode.cn/problems/house-robber-iii/
(1)记忆化搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public Map<TreeNode, Integer> map = new HashMap <>(); public int rob (TreeNode root) { if (root==null ) return 0 ; if (map.containsKey(root)) return map.get(root); int ans1 = root.val; if (root.left!=null ) ans1+= rob(root.left.left)+rob(root.left.right); if (root.right!=null ) ans1+= rob(root.right.left)+rob(root.right.right); int ans2 = rob(root.left) + rob(root.right); int ans = Math.max(ans1, ans2); map.put(root, ans); return ans; } }
基于数组的DP往往是线性的,而树形状的DP还是利用函数递归+记忆化搜索更合适。
(2)优化-树形DP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int [] dpTree(TreeNode root){ int [] ans = new int [2 ]; if (root==null ) return new int []{0 ,0 }; int [] left = dpTree(root.left); int [] right = dpTree(root.right); ans[0 ] = Math.max(left[0 ], left[1 ]) + Math.max(right[0 ], right[1 ]); ans[1 ] = left[0 ]+right[0 ]+root.val; return ans; } public int rob (TreeNode root) { int [] ans = dpTree(root); return Math.max(ans[0 ], ans[1 ]); } }
这种写法将偷与不偷这两种状态合在一个数组中,然后在自底向上的更新。这是树形DP的技巧,见动态规划进阶。
买卖股票问题 买卖股票问题引入了状态转移,需要画图辅佐思路。
买卖股票的最佳时机1 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int maxProfit (int [] prices) { int [][] dp = new int [prices.length][3 ]; dp[0 ][0 ] = 0 ; dp[0 ][1 ] = -prices[0 ]; dp[0 ][2 ] = 0 ; for (int i=1 ; i<prices.length; ++i){ dp[i][0 ] = dp[i-1 ][0 ]; dp[i][1 ] = Math.max(dp[i-1 ][1 ], dp[i-1 ][0 ]-prices[i]); dp[i][2 ] = Math.max(dp[i-1 ][2 ], dp[i-1 ][1 ]+prices[i]); } return dp[prices.length-1 ][2 ]; } }
本题限制只能买卖股票一次,因此卖出股票后就不能再继续操作。
每一天引入三种状态:不持有股票、持有股票、首次卖出股票。每种状态具有几种操作,每种操作又能把第k天的状态变为第k+1天的状态。
买卖股票的最佳时机2 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/
(1)动态规划解法
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int maxProfit (int [] prices) { int [][] dp = new int [prices.length][2 ]; dp[0 ][0 ] = -prices[0 ]; dp[0 ][1 ] = 0 ; for (int i=1 ; i<prices.length; ++i){ dp[i][0 ] = Math.max(dp[i-1 ][0 ], dp[i-1 ][1 ]-prices[i]); dp[i][1 ] = Math.max(dp[i-1 ][1 ], dp[i-1 ][0 ]+prices[i]); } return dp[prices.length-1 ][1 ]; } }
只有两种状态:不持有股票、持有股票。
(2)贪心解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int maxProfit (int [] nums) { int hold = nums[0 ]; int ans = 0 ; for (int i=1 ; i<nums.length; ++i){ if (nums[i]>hold){ ans += nums[i]-hold; hold = nums[i]; }else if (nums[i]< hold){ hold = nums[i]; } } return ans; } }
买卖股票的最佳时机3 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int maxProfit (vector<int >& prices) { vector<vector<int >> dp (prices.size() , vector<int >(5 , 0 )); dp[0 ][1 ] = -prices[0 ]; dp[0 ][2 ] = 0 ; dp[0 ][3 ] = dp[0 ][2 ] - prices[0 ]; dp[0 ][4 ] = 0 ; for (int i=1 ; i<prices.size(); ++i){ dp[i][0 ] = dp[i-1 ][0 ]; dp[i][1 ] = max(dp[i-1 ][1 ], dp[i-1 ][0 ] - prices[i]); dp[i][2 ] = max(dp[i-1 ][2 ], dp[i-1 ][1 ] + prices[i]); dp[i][3 ] = max(dp[i-1 ][3 ], dp[i-1 ][2 ] - prices[i]); dp[i][4 ] = max(dp[i-1 ][4 ], dp[i-1 ][3 ] + prices[i]); } return dp[prices.size()-1 ][4 ]; } };
这道题限制最多买卖两次。
买卖股票的最佳时机4 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/
买卖股票的最佳时机5 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int maxProfit (int [] prices) { int N = prices.length; int [][] dp = new int [N][4 ]; dp[0 ][1 ] = -prices[0 ]; for (int i=1 ; i<prices.length; ++i){ dp[i][0 ] = Math.max(dp[i-1 ][0 ], dp[i-1 ][3 ]); dp[i][1 ] = Math.max(dp[i-1 ][1 ], Math.max(dp[i-1 ][0 ]-prices[i], dp[i-1 ][3 ]-prices[i])); dp[i][2 ] = dp[i-1 ][1 ]+prices[i]; dp[i][3 ] = dp[i-1 ][2 ]; } return Math.max(dp[N-1 ][2 ], Math.max(dp[N-1 ][3 ], dp[N-1 ][0 ])); }
每种状态都是自身描述,由当前天的操作转移到第二天的状态。因此持有股票当前天卖出,第二天为冷冻期。冷冻期无操作第二天是自由期。
买卖股票含手续费 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
画出状态转移图,一切不在话下!
四周目 字符串dp 子串、子序列、编辑距离、回文串
子串和子序列问题 kanade算法 求最大子串和
最长公共子串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Solution { public String LCS (String str1, String str2) { int [][] dp = new int [1 +str1.length()][1 +str2.length()]; int len = 0 ; int start = -1 ; for (int i=1 ; i<=str1.length(); ++i){ for (int j=1 ; j<=str2.length(); ++j){ if (str1.charAt(i-1 )==str2.charAt(j-1 )){ dp[i][j] = 1 + dp[i-1 ][j-1 ]; } if (dp[i][j] > len){ len = dp[i][j]; start = i - len; } } } return str1.substring(start, start+len); } }
dpij不能简单的设置为以i,j结尾两个字符串的最长公共子串长度,因为不知道这个公共子串的开始位置和结束位置。所以要设置为公共子串恰好以ij结尾。不匹配的话dpij就是0。
编辑距离系列 判断子序列 https://leetcode.cn/problems/is-subsequence/
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public boolean isSubsequence (String s, String t) { int [][] dp = new int [1 +s.length()][1 +t.length()]; for (int i=1 ; i<=s.length(); ++i){ for (int j=1 ; j<=t.length(); ++j){ if (s.charAt(i-1 ) == t.charAt(j-1 )) dp[i][j] = 1 + dp[i-1 ][j-1 ]; else dp[i][j] = dp[i][j-1 ]; } } return dp[s.length()][t.length()]==s.length(); } }
本题最好的解法是双指针,子序列只要求顺序一致,两个指针分别遍历一边s和t,时间复杂度在O(n+m)。
但为了编辑距离的抛砖引玉,确定dp数组的含义:dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度
以及递推公式的含义:
if (s[i - 1] == t[j - 1])
t中找到了一个字符在s中也出现了(这相当于从后向前匹配)
if (s[i - 1] != t[j - 1])
最难理解的就是s[i - 1] == t[j - 1])的情况,其实是从后向前匹配!
时间复杂度:O(n × m)
空间复杂度:O(n × m)
不同的子序列 https://leetcode.cn/problems/distinct-subsequences/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int numDistinct (String s, String t) { int [][] dp = new int [1 +s.length()][1 +t.length()]; for (int i=0 ; i<=s.length(); ++i) dp[i][0 ] = 1 ; for (int i=1 ; i<=s.length(); ++i){ for (int j=1 ; j<=t.length(); ++j){ if (s.charAt(i-1 ) == t.charAt(j-1 )) dp[i][j] = dp[i-1 ][j] + dp[i-1 ][j-1 ]; else dp[i][j] = dp[i-1 ][j]; } } return dp[s.length()][t.length()]; } }
dp数组的含义仍然:dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度
不过不同的是si与tj匹配成功后,tj其实有两种匹配方法:匹配最后一个,或者最后一个不匹配,向sj-1匹配。
对应 dp[i][j] = dp[i-1][j] + dp[i-1][j-1];
,小例子:s=babgba,t=b
特别要注意的是初始化的过程:dp【i】【0】全都为1,因为0自然匹配成功。
两个字符串的删除 https://leetcode.cn/problems/delete-operation-for-two-strings/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int minDistance (String s, String t) { int [][] dp = new int [1 +s.length()][1 +t.length()]; for (int i=0 ; i<=s.length(); ++i) dp[i][0 ] = i; for (int j=0 ; j<=t.length(); ++j) dp[0 ][j] = j; for (int i=1 ; i<=s.length(); ++i){ for (int j=1 ; j<=t.length(); ++j){ if (s.charAt(i-1 ) == t.charAt(j-1 )) dp[i][j] = dp[i-1 ][j-1 ]; else dp[i][j] = 1 + Math.min(dp[i-1 ][j], dp[i][j-1 ]); } } return dp[s.length()][t.length()]; } }
dp数组仍然是题目中的含义。让我们想一想失配的含义,自然是删除s中的一个,然后去与t匹配,或者删除t中的一个去与s匹配。对应 dp[i][j] = 1 + Math.min(dp[i-1][j], dp[i][j-1]);
匹配成功时,由于能够删除任意一个字符串的中一个字符,直接忽略这个字符就好了。
编辑距离 https://leetcode.cn/problems/edit-distance/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public int minDistance (String s, String t) { int [][] dp = new int [1 +s.length()][1 +t.length()]; for (int i=0 ; i<=s.length(); ++i) dp[i][0 ] = i; for (int j=0 ; j<=t.length(); ++j) dp[0 ][j] = j; for (int i=1 ; i<=s.length(); ++i){ for (int j=1 ; j<=t.length(); ++j){ if (s.charAt(i-1 ) == t.charAt(j-1 )) dp[i][j] = dp[i-1 ][j-1 ]; else { dp[i][j] = Math.min(dp[i-1 ][j], dp[i][j-1 ]); dp[i][j] = Math.min(dp[i][j], dp[i-1 ][j-1 ]); dp[i][j] ++; } } } return dp[s.length()][t.length()]; } }
虽然三种操作看着吓人,但仔细想想,增加操作就等价于删除(为什么?)
仍然考虑失配场景:si与tj匹配不成功。
替换操作:将si与tj替换为相同的字母,找下一步s[i-1]和t[i-1]
删除操作:删除si或tj,对应找dp[i-1][j], dp[i][j-1])
增加操作;我在si处增加一个字母,那么接下来怎么匹配?自然是si与ti-1匹配,因为ti已经匹配了。这不就是相当于删除ti吗?
明白这点后,题目也就不难了。
回文串 回文串就要利用回文串的性质,左右对称,因此可以从中间展开,也有双指针的解法。
回文子串 https://leetcode.cn/problems/palindromic-substrings/
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int countSubstrings (String s) { int N = s.length(); int ans = 0 ; boolean [][] dp = new boolean [N][N]; for (int i=0 ; i<N; ++i){ for (int j=0 ; j<=i; ++j){ if (s.charAt(i) == s.charAt(j)){ if (i-j<=1 ) dp[j][i] = true ; else dp[j][i] = dp[j+1 ][i-1 ]; } if (dp[j][i]) ans++; } } return ans; } }
最长回文子串 https://leetcode.cn/problems/longest-palindromic-substring/description/
给你一个字符串 s
,找到 s
中最长的回文子串。
(1)动态规划(常规写法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public String longestPalindrome (String s) { int l=0 ,r=0 ; int N = s.length(); boolean [][] dp = new boolean [N][N]; for (int i=0 ; i<N; ++i){ for (int j=0 ; j<=i; ++j){ if (s.charAt(i)==s.charAt(j)){ if (i-j<=1 ) dp[j][i] = true ; else dp[j][i] = dp[j+1 ][i-1 ]; } if (dp[j][i] && i- j> r-l){ l = j; r = i; } } } return s.substring(l, r+1 ); } }
(2)动态规划(以长度遍历,更容易理解的写法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public String longestPalindrome (String s) { int l=0 ,r=0 ; int N = s.length(); boolean [][] dp = new boolean [N][N]; for (int len=1 ; len<=N; ++len){ for (int i=0 ; i<N && i+len-1 <N; ++i){ int j = i+len-1 ; if (s.charAt(i) == s.charAt(j)) dp[i][j] = (i==j) || (i+1 ==j) || dp[i+1 ][j-1 ]; if (dp[i][j] && j-i>r-l){ r = j; l = i; } } } return s.substring(l, r+1 ); } }
(3)双指针(空间效率O(1)的解法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public int [] find(String s, int l, int r){ while ((l-1 >=0 ) && (r+1 <s.length()) && (s.charAt(l-1 )==s.charAt(r+1 ))){ --l; ++r; } int r_l = l; int r_r = r; return new int []{r_l, r_r}; } public String longestPalindrome (String s) { int l=0 , r=0 ; for (int i=0 ; i<s.length(); ++i){ int [] odd = find(s, i, i); int [] even = {0 ,0 }; if (i+1 < s.length() && s.charAt(i)==s.charAt(i+1 )) even = find(s, i, i+1 ); if (r-l < odd[1 ]-odd[0 ]){ l = odd[0 ]; r = odd[1 ]; } if (r-l < even[1 ]-even[0 ]){ l = even[0 ]; r = even[1 ]; } } return s.substring(l, r+1 ); } }
注意:substring函数是从l到r,最后一个索引不包含。
最长回文子序列 https://leetcode.cn/problems/longest-palindromic-subsequence/
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。
分割回文串的最小次数 https://leetcode.cn/problems/palindrome-partitioning-ii/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : int minCut (string s) { vector<int > dp (s.size(), 0 ) ; for (int i=0 ; i<s.size (); ++i) dp[i]=i; vector<vector<int >> ispalindromic (s.size (), vector <int >(s.size (), 0 )); for (int i = s.size ()-1 ; i>=0 ; --i){ for (int j = i; j < s.size (); ++j){ if (s[i]==s[j]){ if (j-i<=1 ) ispalindromic[i][j]=1 ; else ispalindromic[i][j] = ispalindromic[i+1 ][j-1 ]; } } } } for (int i=1 ;i<s.size ();++i){ if (ispalindromic[0 ][i]){ dp[i]=0 ; continue ; }else { for (int j=0 ;j<i;j++){ if (ispalindromic[j+1 ][i]) dp[i] = min (dp[i], 1 +dp[j]); } } } return dp[s.size ()-1 ]; } };