动态规划特点:
题型:求最值
核心:穷举
三要素:
1. 重叠子问题
2. 状态转移方程(最关键)
3. 最优子结构
解题套路(dong哥经验总结):
0,明确base case
1,明确 状态
2,明确 选择
3,明确dp函数/数组的定义
// 初始化 base case
dp[0][0][...] = base
// 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
暴力递归解法
/* 暴力递归解法 */
public int fib(int n) {
// base case
if (n == 0 || n == 1) {
return n;
}
// 递推关系
return fib(n - 1) + fib(n - 2);
}
带备忘录的递归解法 :memo避免重复计算,自顶向下递归解法,自底向上回溯答案
/* 带备忘录的递归解法 */
public int fib(int n) {
// 备忘录全初始化为 0
int[] meno = new int[n + 1];
// 进行带备忘录的递归
return helper(meno, n);
}
private int helper(int[] meno, int n) {
// base case
if (n == 0 || n == 1) {
return n;
}
// 已经计算过,不用再计算了
if (meno[n] != 0) {
return meno[n];
}
meno[n] = helper(meno, n - 1) + helper(meno, n - 2);
return meno[n];
}
dp数组的迭代解法:自底向上迭代解法
/* dp 数组的迭代解法 */
public int fib(int n) {
if (n == 0) {
return 0;
}
int[] dp = new int[n + 1];
// base case
dp[0] = 0;
dp[1] = 1;
// 状态转移
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
优化空间复杂度:时间复杂度O(N),空间复杂度O(1)
/* 优化空间复杂度 */
public int fib(int n) {
// base case
if (n == 0 || n == 1) {
return n;
}
// 递推关系
int prev = 0;
int curr = 1;
for (int i = 2; i <= n; i++) {
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
暴力解法:超时
/* 暴力递归解法 */
/**
* 状态:目标金额 aomunt
* 选择:coins 数组中列出的所有硬币面额
* 函数定义:凑出总金额amount,至少需要coinChange(coins,amount)枚硬币
* base case:aomount == 0时,需要0枚硬币;aomount < 0是,不可能凑出
*
* coinChange([1,2,5],11)
* = min(coinChange([1,2,5],10),coinChange([1,2,5],9),coinChange([1,2,5],6))
*/
public int coinChange(int[] coins, int amount) {
// base case
if(amount == 0) {
return 0;
}
if(amount < 0) {
return -1;
}
int res = Integer.MAX_VALUE;
for (int coin:coins){
// 计算子问题的结果
int subProblem = coinChange(coins,amount - coin);
// 子问题无解则跳过
if(subProblem == -1){
continue;
}
// 在子问题中选择最优解,然后加一
res = Math.min(res,subProblem+1);
}
return res == Integer.MAX_VALUE? -1:res;
}
带备忘录递归解法:自顶向下递归解法O(coins.size() * amount)
/**
* 自顶向下递归解法
*/
// 备忘录
int[] memo;
public int coinChange(int[] coins, int amount) {
memo = new int[amount +1];
// memo 数组全都初始化为特殊值
Arrays.fill(memo,-666);
return dp(coins,amount);
}
private int dp(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
if (amount < 0) {
return -1;
}
// 备忘录,防止重复计算
if (memo[amount] != -666) {
return memo[amount];
}
int res = Integer.MAX_VALUE;
for (int coin : coins) {
// 计算子问题的结果
int subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) {
continue;
}
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
}
memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
return memo[amount];
}
自底向上迭代解法:
/**
* 自底向上迭代解法
*/
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount +1];
// dp 数组全都初始化为特殊值 amount+1
Arrays.fill(dp,amount +1);
// base case
dp[0] = 0;
// 外层for循环在遍历所有状态的所有取值
for(int i = 0; i < dp.length; i++){
// 内层 for 循环在求所有选择的最小值
for(int coin : coins){
// 子问题无解,跳过
if((i - coin) < 0){
continue;
}
// 状态转移
dp[i] = Math.min(dp[i], 1+dp[i - coin]);
}
}
// 看看金额 amount 能不能凑出来
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
动态规划问题的本质:
1,如何穷举
写状态转移放错,暴力秋菊所有可行解
2,如何聪明的穷举
用备忘录消除重叠子问题,写出自顶向下的解法
进一步,可以写出自底向上的迭代解法
再进一步,可能可以优化控件复杂度
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// base case
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}
// 穷举状态
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 状态转移
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
}
}
}
return dp[m][n];
}
public int longestPalindromeSubseq(String s) {
int[][] dp = new int[s.length()][s.length()];
// base case
for (int i = 0; i < s.length(); i++) {
dp[i][i] = 1;
}
// 穷举状态
for (int i = 1; i < s.length(); i++) {
for (int j = i - 1; j >= 0; j--) {
// 状态转移
if (s.charAt(i) == s.charAt(j)) {
if (j == i - 1) {
dp[j][i] = 2;
} else {
dp[j][i] = dp[j + 1][i - 1] + 2;
}
} else {
dp[j][i] = Math.max(dp[j + 1][i], dp[j][i - 1]);
}
}
}
return dp[0][s.length() - 1];
}