动态规划问题是算法里一个比较难的部分了,每次遇到动态规划的问题都感觉很不好解决。目前的认知是读到一个题我知道应当用动态规划的方式来解决,但要上手去写总感觉无从下手。这一篇文章用来记录我在LeetCode刷题时遇到的动态规划问题及解答的方法。
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//可以视作背包问题来求解
//一般的背包问题,需要考虑物品的价值、重量
//对于此题,子集的数目即总的价值,0/1都代表重量(两个维度)
//dp[i][j] 含义: 最多有i个0和j个1的strs的最⼤⼦集的⼤⼩为dp[i][j]
int[][]dp=new int[m+1][n+1];
for(String s:strs){
int zeros=0;
int ones=0;
for(char c:s.toCharArray()){
if(c=='0') zeros++;
else ones++;
}
//从后向前填表是为了上一轮的结果不被覆盖
//从可用的部分去减该字符串包含的
//如果选当前字符串dp[i][j]=dp[i-zeros][j-ones]+1
//如果不选当前字符串dp[i][j]=dp[i][j]
for(int i=m;i>=zeros;i--){
for(int j=n;j>=ones;j--){
//状态转移方程
dp[i][j]=Math.max(dp[i][j],dp[i-zeros][j-ones]+1);
}
}
}
return dp[m][n];
}
}
其中还包含了一个dp常用知识点,滚动数组,把本来三维的dp数组降到了二维。官方的三维解法是dp[i][j][k]
代表前 i 个字符串中,使用 j 个 0 和 k 个 1 的情况下最多可以得到的字符串数量。
- Leetcode 494 目标和
写动态规划前先贴一个回溯解法
class Solution {
int count=0;
public int findTargetSumWays(int[] nums, int target) {
backTrace(nums,target,0,0);
return count;
}
//对每个数组加符号有两种 正或负
public void backTrace(int[] nums,int target,int index,int sum){
if(index==nums.length){
if(sum==target){
count++;
}
}
else{
backTrace(nums,target,index+1,sum+nums[index]);
backTrace(nums,target,index+1,sum-nums[index]);
}
}
}
动态规划方法:首先分析题意,对题目所求进行转化。记数组的元素和为sum,添加-号的元素之和为neg,则其余添加+的元素之和为sum−neg,得到的表达式的结果为(sum-neg)-neg=target.即neg=(sum-target)/2.
则sum-targe必须为偶数,遇到非偶数的情况可以提前返回。
令dp[i][j]表示在前i个元素中进行选取,使元素和为j的方案数。
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int i:nums)
sum+=i;
int neg=sum-target;
//不符合要求时提前退出
if(neg<0||neg%2!=0) return 0;
int n = nums.length;
neg = neg / 2;
int[][] dp = new int[n + 1][neg + 1];
//不选取任何数字 使结果为0的方案有一种
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
}
- Leetcode 1049. 最后一块石头的重量 II
三叶姐的题解给的很详细,自己也是看了她的解析才写出来。解决动态规划问题,需要具备的一种很重要的能力就是题意转化。
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum = 0;
for (int i : stones) sum += i;
int t = sum / 2;
int[][] dp = new int[n + 1][t + 1];
for (int i = 1; i <= n; i++) {
int x = stones[i - 1];
for (int j = 0; j <= t; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= x) dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - x] + x);
}
}
//这里需要减去两个dp[n][t]的道理类似于目标和这道题的neg
return Math.abs(sum - dp[n][t] - dp[n][t]);
}
}
还有另一个大佬写的解法看上去可能会清晰一些
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum = 0;
for(int num:stones){
sum += num;
}
//背包容量上限为石头总重量的一半
int dp[][] = new int[n+1][sum/2+1];
for(int i=1;i<=n;i++){
for(int j=0;j<=sum/2;j++){
//当我能把这块石头放进背包时,我会比较放或不放,选择最大值
if(j>=stones[i-1]){
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-stones[i-1]] + stones[i-1]);
} else {
//这块石头放不进背包时,只能跳过
dp[i][j] = dp[i-1][j];
}
}
}
//dp[n][sum/2]的最大值为sum/2,因此最理想的结果为0
return sum-dp[n][sum/2]*2;
}
}
- Leetcode879 盈利计划
没想到遇到了困难级别的动态规划。本来认为是二维dp,想了一阵子卡住了,解不下去,就看了题解发现是三维dp。
三叶姐的题解 对负数的处理很妙
class Solution {
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
int mod=(int)1e9+7;
//总员工数n和minProfit是边界条件
//仍然视作背包问题 最多装n个物品 保证其价值最小为minProfit (两个限制)
// 前i个任务 选人最大为j 得到的最小利润k
int[][][]dp=new int[group.length+1][n+1][minProfit+1];
//任何工作都不选 则结果为0 做一个预处理
for(int i=0;i<=n;i++)dp[0][i][0]=1;
for(int i=1;i<=group.length;i++){
int gro=group[i-1];
int pro=profit[i-1];
for(int j=0;j<=n;j++){
for(int k=0;k<=minProfit;k++){
//不选
dp[i][j][k]=dp[i-1][j][k];
if(j>=gro){
int u=Math.max(k-pro,0);
dp[i][j][k]+=dp[i-1][j-gro][u];
dp[i][j][k]%=mod;
}
}
}
}
return dp[group.length][n][minProfit];
}
}
- 518. 零钱兑换 II
完全背包问题,物品不限制取用的次数
class Solution {
public int change(int amount, int[] coins) {
//完全背包 每个物品不限制取用次数
//不限制体积的情况下求能凑成amount的方案数
//dp[i]表示金额为i情况下的方案数目
int[] dp=new int[amount+1];
//边界初始化
dp[0]=1;
for(int coin:coins){
for(int i=0;i<=amount;i++){
if(i>=coin)
dp[i]=dp[i]+dp[i-coin];
}
}
return dp[amount];
}
}
- Leetcode 279 完全平方数
跟零钱兑换问题一个思路。做了几天dp发现确定边界/初始条件也是相当重要
class Solution {
public int numSquares(int n) {
//类似于完全背包 每个完全平方数不限制选用次数
//dp[i]表示容量为i时的数字个数
int[] dp=new int[n+1];
for(int i=0;i<=n;i++)dp[i]=i;//最多由i个1组成当前数字
for(int i=0;i<=n;i++){
for(int j=1;j*j<=i;j++){
dp[i]=Math.min(dp[i-j*j]+1,dp[i]);
}
}
return dp[n];
}
}
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//dp[i]表示字符串以i结尾时是否可拆分
boolean [] dp=new boolean[s.length()+1];
dp[0]=true;
for(int i=1;i<=s.length();i++){
for(int j=0;j<i;j++){
if(dp[j]&&wordDict.contains(s.substring(j,i))){
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
}
【不定期更新】