动态规划
- 动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
- 动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
- 参考讲解
点这里 - 动态规划解题步骤
核心思想是递推,难点在于想清楚 状态 dp[i] 代表什么,然后构造状态转移矩阵,利用初始条件递推出最终结果
1.将原问题拆分成子问题
2.确认状态
3.确认边界状态(初始条件)
4.状态转移方程
练习题
- LeetCode 70 爬楼梯
将原问题拆分成子问题
原问题:在爬楼梯时,每次可向上走1阶台阶或2阶台阶,问有n阶楼梯有多少种上楼的方式?
子问题:上n-1阶有多少种?上n-2阶有多少种?
确认状态
第 i 个状态 即为上第 i 阶台阶的所有走法数量
确认边界状态(初始条件):
边界状态为1阶台阶与2阶台阶的走法,1 阶台阶有1种走法,2阶台阶有2种走法, 即dp[1] = 1,dp[2] = 2。0阶赋予0,可以认为是只有一种“不走”的方法。
状态转移方程
dp[i] = dp[i-1] + dp[i-2]; (i>=3)
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
- LeetCode 198 打家劫舍
子问题
只考虑前两个房间时,偷金额最高的那个
如果偷第三个房间,则意味着第二个房间不偷,也就是第三个房间金额 + 第一个房间的金额
如果不偷第三个房间,则宝藏数量等于前两个房间金额和
确认状态
int [] nums; // 各个房间的金额数
int [] flags = new int [n]; // 记录各个房间有没有被偷,若flag = 0 则没偷, flag = 1 则偷了。
int [] dp = new int [n]; // dp[i]表示前i个房间偷到的最大金额数
初始状态
第一个房间:
Condistion 1 :dp[0] = nums[0] ; flags[0] = 1;Condistion 2 :dp[0] = 0; flags[0] = 0;
第二个房间:
Condistion 1 :when flags[1] = 1; dp[1] = nums[1]; Condistion 2 :whenflags[1] = 0; dp[1] = dp[0]; 选 Condistion 1 还是 Condistion 2呢? 比较 两种情况下dp[1]的大小
推广到前i个房间: (i>=2)
when flags[i] = 1, dp[i] = nums[i] + dp[i-2]when flags[i] = 0; dp[i] = dp[i-1]
状态转移方程
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2;i<n;i++)
dp[i] = max(nums[i] + dp[i-2],dp[i-1])
class Solution {
public int rob(int[] nums) {
if(nums.length == 0)
return 0;
int [] dp = new int[nums.length];
dp[0] = nums[0];
if(nums.length == 1)
return nums[0];
dp[1] = (nums[0]>nums[1]) ? nums[0] : nums[1];
for(int i = 2;i<nums.length;i++)
dp[i] = ((nums[i] + dp[i-2]) > dp[i-1]) ? (nums[i]+dp[i-2]) : dp[i-1];
return dp[nums.length-1];
}
}
- LeetCode 53 最大子段和
子问题
只考虑第一个元素,则最大子段和为其本身 DP[0] = nums[0]
考虑前两个元素,最大子段和为nums[0],num[1],nums[0] + num[1] 中的最大值,设为DP[1]
考虑前三个元素,最大子段和为DP[1],num[2],num[1]+num[2]中的最大值,而这种情况下,DP[2]可能会等于DP[1],我们假设第三个元素包含在最后的子串里,则
DP[2] = Max(DP[1]+nums[2] , nums[2])
确认状态
DP[i] 为 以nums[i]结尾的子段的最大最短和
例如 DP[1]则为以nums[1]结尾的最大字段和
初始状态
dp[0] = nums[0]
dp[1] = max(dp[0]+nums[1] , nums[1])
状态转移方程:
dp[i] = max(dp[i-1]+nums[i],nums[i])
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len == 0)
return 0;
int [] dp = new int[len];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i<len;i++){
dp[i] = (dp[i-1]+nums[i] > nums[i]) ? dp[i-1]+nums[i] : nums[i];
if (dp[i]>max)
max = dp[i];
}
return max;
}
}
- LeetCode 322 找零钱
将原问题拆分成子问题
原问题:不同面额的硬币 coins 和一个总金额 amount。计算可以凑成总金额所需的最少的硬币个数。
子问题:amount-coin1/coin2/coin3后如何凑成这个新amount?计算所需最少的硬币个数。
确认状态
DP[i]表示构造金额i需要的最小钞票数
确认边界状态(初始条件)
DP[coin] = 1 其他的都为 -1例如coins = [1, 2, 5], amount = 11 则 dp[1]、dp[2]、dp[5] =1。现在已知 DP[coin] 推出DP[0~amount]。
状态转移方程
dp[i] = min(dp[i-1], dp[i-2], dp[i-5]) + 1
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] == amount+1 ? -1 : dp[amount];
}
}
- LeetCode 174 地下城游戏
子问题
每一格前往迷宫右下角需要的最少健康点数
状态表示:
dp[i][j]表示从位置i,j到右下角所需的最少健康点数。则从右下角往左上角遍历。
边界:
初始时,右下角如果为负,则需要的健康点数为-右下角+1。否则,需要的健康点数为1即可(因为小于等于0时即死亡)
最终结果:dp[0][0]
状态转移:
dp[i][j]可以往右走或者往下走。
往右走时,右边格子需要的健康点数为:dp[i][j + 1]。则如果dungeon[i][j]为负或者为正且小于dp[i][j+1]时dp[i][j]需要dp[i][j + 1]-dungeon[i][j]
如果dungeon[i][j]为正且大于dp[i][j + 1],则只需1点健康值,保证不死即可
往下走同理。
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int n = dungeon.length, m = dungeon[0].length;
int[][] dp=new int[n][m];
dp[n - 1][m - 1] = dungeon[n - 1][m - 1] > 0 ? 1 : (-dungeon[n - 1][m - 1] + 1);
for (int i = m - 2; i >= 0; i--)
dp[n - 1][i] = Math.max(1, dp[n - 1][i + 1] - dungeon[n - 1][i]);
for (int i = n - 2; i >= 0; i--)
dp[i][m - 1] = Math.max(1, dp[i + 1][m - 1] - dungeon[i][m - 1]);
for (int i = n - 2; i >= 0; i--) {
for (int j = m - 2; j >= 0; j--) {
dp[i][j] = Math.max(1, Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]);
}
}
return dp[0][0];
}
}