动态规划
0.什么是动态规划?
- 核心思想:动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算
- 动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:f(n-1)和f(n-2) 称为 f(n) 的最优子结构,f(n)= f(n-1)+f(n-2)就称为状态转移方程,f(1) = 1, f(2) = 2 就是边界,比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。
- 如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。
1.爬楼梯
问:假设你正在爬楼梯。需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。
f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)
即通用公式为: f(n) = f(n-1) + f(n-2)
动态规划解法:
class Solution {
public int climbStairs(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
int a = 1;
int b = 2;
int temp = 0;
for(int i=3; i<=n; i++){
temp = a+b;
a = b;
b = temp;
}
return temp;
}
}
递归解法:
为了避免重复计算超出运行时间,我们使用一个数组或者一个哈希map充当备忘录。
public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap(); //在函数体外定义
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)));
return tempMap.get(n);
}
}
2.最大子序和
问:给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:确定状态转移方程,其中dp[i]
用来表示,终点在i
的子序列的最佳子序和
dp[i] = Math.max((dp[i-1]+nums[i]), nums[i]); //初版
dp[i] = Math.max(dp[i - 1], 0) + nums[i]; //优化后
初版:
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length == 1)
return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
for(int i=1; i<nums.length; i++){
dp[i] = Math.max((dp[i-1]+nums[i]), nums[i]);
}
int res = dp[0];
for(int j=0; j<nums.length; j++){
if(res <= dp[j])
res = dp[j];
}
return res;
}
}
优化后:
public int maxSubArray(int[] nums) {
int length = nums.length;
int[] dp = new int[length];
//边界条件
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < length; i++) {
//转移公式
dp[i] = Math.max(dp[i - 1], 0) + nums[i];
//记录最大值
max = Math.max(max, dp[i]);
}
return max;
}
3.买卖股票的最佳时机
问:给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
状态转移方程:
dp[i][0] = 0; //第i天不持有股票的最大利润(1.没有买 2.卖了)
dp[i][1] = -prices[0]; //第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], -prices[i]);
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0)
return 0;
int length = prices.length;
int[][] dp = new int[length][2];
//边界条件
dp[0][0]= 0;
dp[0][1] = -prices[0];
for (int i = 1; i < 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], -prices[i]);
}
//毋庸置疑,最后肯定是手里没持有股票利润才会最大,也就是卖出去了
return dp[length - 1][0];
}
4.打家劫舍
问:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
状态转移方程:
//这里可以定义一个二维数组dp[length][2],其中dp[i][0]表示第i+1家没偷的最大总金额,dp[i][1]表示的是第i+1家偷了的最大总金额
dp[i][0]=max(dp[i-1][0],dp[i-1][1]);//他表示如果第i+1家没偷,那么第i家有没有偷都是可以的,我们取最大值即可
dp[i][1]=dp[i-1][0]+nums[i];//他表示的是如果第i+1家偷了,那么第i家必须没偷
//边界条件
dp[i][0]=0;
dp[0][1]=nums[0];
public int rob(int[] nums) {
//边界条件判断
if (nums == null || nums.length == 0)
return 0;
int length = nums.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;//第1家没偷
dp[0][1] = nums[0];//第1家偷了
//从第2个开始判断
for (int i = 1; i < length; i++) {
//下面两行是递推公式
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
//最后取最大值即可
return Math.max(dp[length - 1][0], dp[length - 1][1]);
}