写这类题型时候,要跟着下面步骤:
-
定义状态 :
dp[i](或dp[i][j])表示什么?------这是最关键的一步,定义错了后面全错。 -
转移方程 :
dp[i]由哪些更小的状态推来?(选 / 不选、从哪一步转移) -
初始化 :边界
dp[0]是多少?非法状态填什么(求最大填-INF,求方案数填0)? -
遍历顺序:先算的必须先于依赖它的状态(决定正序还是倒序)。
要点:状态定义是 DP 的灵魂。90% 的 DP bug 来自「状态含义模糊」
java
线性DP
// dp[i]:偷到第 i 间房的最大金额。选 i 就不能选 i-1
int rob(int[] nums) {
int n = nums.length;
// 滚动变量代替数组:f0=dp[i-2], f1=dp[i-1]
int f0 = 0, f1 = 0;
for (int x : nums) {
int cur = Math.max(f1, f0 + x); // 不偷 f1 / 偷 f0+x
f0 = f1; f1 = cur;
}
return f1;
}
0/1 背包
// dp[j]:容量 j 下的最优值。物品遍历在外,容量倒序在内
boolean canPartition(int[] nums) {
int sum = 0; for (int x : nums) sum += x;
if (sum % 2 != 0) return false;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int x : nums) // 每个物品
for (int j = target; j >= x; j--) // 容量【倒序】:保证每件只用一次
dp[j] = dp[j] || dp[j - x];
return dp[target];
}
完全背包
//
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 用 amount+1 当 INF
dp[0] = 0;
for (int c : coins) // 每个硬币
for (int j = c; j <= amount; j++) // 容量【正序】:允许重复使用
dp[j] = Math.min(dp[j], dp[j - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
最长递增子序列 LIS
// O(n^2) 版本:dp[i]=以 i 结尾的 LIS 长度
int lengthOfLIS(int[] nums) {
int n = nums.length, ans = 1;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++)
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
ans = Math.max(ans, dp[i]);
}
return ans;
}
状态机 DP
// 通用:dp[i][持有/不持有],每天在两个状态间转移
int maxProfit(int[] prices) {
int hold = Integer.MIN_VALUE, free = 0; // 持有 / 不持有 的最大利润
for (int p : prices) {
free = Math.max(free, hold + p); // 今天卖出
hold = Math.max(hold, free - p); // 今天买入
}
return free; // 最后一定不持有
}
区间 DP
// dp[i][j]:区间 [i,j] 内的最优解。按区间长度从小到大枚举
int minCost / longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1; // 长度 1 的区间
for (int len = 2; len <= n; len++) // 枚举区间长度
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1; // 区间右端
// 转移:由更短的子区间 [i+1][j] / [i][j-1] / [i+1][j-1] 推来
dp[i][j] = s.charAt(i) == s.charAt(j)
? dp[i+1][j-1] + 2
: Math.max(dp[i+1][j], dp[i][j-1]);
}
return dp[0][n-1];
}