动态规划(英语:Dynamic programming,简称 DP)
是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。斐波那契数列 0,1,1,2,3,5,8,13,…有着一个相当简单的描述方式,它的每个数字都与前两个紧邻的数字相关。如果 F(n) 是第 n 个数字,那么我们会有 F(n) = F(n-1) + F(n-2)。这个在数学上称作递归方程或者递推关系。为了计算后面的项,它需要前面项的计算结果作为输入。
解决方案
自上而下:
你从最顶端开始不断地分解问题,直到你看到问题已经分解到最小并已得到解决,之后只用返回保存的答案即可。这叫做记忆存储(Memoization)。
自下而上:
你可以直接开始解决较小的子问题,从而获得最好的解决方案。在此过程中,你需要保证在解决问题之前先解决子问题。这可以称为表格填充算法(Tabulation,table-filling algorithm**)。
至于迭代和递归与这两种方法的关系,自下而上用到了迭代技术,而自上而下则用到了递归技术。
动态规划、分治法、贪心算法异同点
相同点:
动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。
不同点:
-
分治法
分治法中的各个子问题是独立的,利用子问题的解,合并成该问题的解。
-
贪心算法
只有同一个问题,依赖于当前已经做出的所有选择。
自顶向下处理,每一步,根据策略得到一个当前最优解。传递到下一步,从而保证每一步都是选择当前最优的。
-
动态规划
动态规划中的各个子问题是不独立的,动态规划任何一个i+1阶段都仅仅依赖 i 阶段的处理,而与i之前的选择无关。
自底向上处理,每一步,根据策略得到一个更小规模的问题。最后解决最小规模的问题,得到整个问题最优解。
121. 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
题解:
class Solution {
public int maxProfit(int[] prices) {
//思路:从最后一天往前遍历,看最后一天卖出能得到的最大利润,依次往前得出每天卖出的最大利润,取出最大值为最大利润,如果最大值还为负数,则
//最大利润为0,不进行交易
int maxProfit = 0;
for (int i=prices.length-1;i>=1;i--) { //从第二天到最后一天都可能是卖出的天数,但第一天不能卖,因为还没买
for (int j=i-1;j>=0;j--) { //从卖出的前一天到第一天为可能买的日期,得出利润
if (prices[i]-prices[j] > maxProfit) {
//利润有更大的需要更新
maxProfit = prices[i]-prices[j];
}
}
}
return maxProfit;
}
}
买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
题解:
public int maxProfit(int[] prices) {
//思路:该题将数值画成折线图,就知道股票涨跌的区间,那么我们就需要在上涨的最低值买入,在上涨的最高处卖出,就可以了
//遍历数组,得到上涨区间,获取最小值,上涨区间结束获取最大值,累加这个区间差价
int profit = 0;
int min = 0, max = 0;
int pos = 0;
while(pos < prices.length-1) {
//循环条件是倒数第二个结束,内层循环必须加上外层循环的限制,否则pos移动位置可以超出数组长度
while(pos < prices.length-1 && prices[pos] >= prices[pos+1]) {
//在下跌的底部获取最小值
pos++;
}
//下一个比当前值大,下跌区间结束,得到上涨区间最小值
min = prices[pos];
while(pos < prices.length-1 && prices[pos] <= prices[pos+1]) {
//在上涨的顶部获取最大值
pos++;
}
//下一个数比上一个小了,说明上涨区间到头了,得到上涨区间最大值
max = prices[pos];
profit += max - min;
}
return profit;
}
/**
* 解法二:
* 动态规划法:
* 思路:将问题转化为第n天能获得最大利润,依次从第1天、第2天、第3天...第n天来计算
* 每后一天需要依赖前面一天的结果
*
* 最多只能持有 一股 股票
*/
public int maxProfit(int[] prices) {
int n=prices.length;
int[][] dp = new int[n][2]; //每天有两种状态,持有股票和不持有股票
//当天不持有股票用0表示,当天持有股票用1表示
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i=1;i
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
题解1:
class Solution {
public int climbStairs(int n) {
//思路:用动态规划思想,要爬到n阶,可以通过最后爬一步和两步2中爬法到,到n阶的方法等于到n-1阶方法加上n-2阶方法,即f(n)=f(n-1)+f(n-2),这个f(n)方法就用递归实现
//此外,f(n)=f(n-1)+f(n-2),从n为3开始,该函数计算得出的是一个斐波那契数列,当前数为前两个数之和,所以有两种解法
if (n < 3) {
return n;
}
int a = 1;
int b = 2;
int cur = 0;
for (int i=3;i<=n;i++) {
cur = a + b;
a = b;
b = cur;
}
return cur;
}
}
题解2:
class Solution {
public int climbStairs(int n) {
//思路:用动态规划思想,要爬到n阶,可以通过最后爬一步和两步2中爬法到,到n阶的方法等于到n-1阶方法加上n-2阶方法,即f(n)=f(n-1)+f(n-2),这个f(n)方法就用递归实现
return getStepWay(n);
}
/**
* 计算n级台阶不同走法
* @param n
* @return
*/
public int getStepWay(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return getStepWay(n-1) + getStepWay(n-2);
}
}
1025. 除数博弈
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 False。假设两个玩家都以最佳状态参与游戏。
示例 1:
输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。
示例 2:
输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。
题解:
class Solution {
/**
* 思路:
* 动态规划,先从dp[1],dp[2]
* dp[n] 表示N=n时的结果,dp[n]类型为boolean类型 true为爱丽丝赢
* 那么dp[1]=false,dp[2]=true
* 然后往后推,如果i中有约数j,dp[i-j]=false,那么dp[i]=true
* 如果i中,所有dp[i-j]没有为false情况,那么dp[i]=false;
*
* 我们用DP来求解这个问题,首先new一个长度为N+1的数组,dp[i]表示i这个数是否可以赢,如果为true则N=i可以赢,为false则输。
* N=1,爱丽丝就肯定会输,所以我们首先让dp[1]=false;
* 然后我们从i=2开始,一直遍历到i=N
* 按照题意,我们让j每次从1到i-1的区间里取数,且需要满足
* 选出任一 x,满足 0 < x < N 且 N % x == 0 。
* 这个条件,如果发现dp[j]=false,那么dp[i]就一定会赢。
*/
public boolean divisorGame(int N) {
boolean[] dp = new boolean[N+1];
if (N == 1) {
return false;
}
dp[1] = false;
dp[2] = true;
for (int i=3;i<=N;i++) {
//i表示某次开始选择的数,用j表示下一次选的数,j为i的约数,所以j的范围为1到i/2
dp[i] = false;
for (int j=1;j<=i/2;j++) {
if (i%j==0 && dp[i-j]==false) {
//下一次人选的为false,那么该人就胜了,为ture
dp[i] = true;
break;
}
}
}
return dp[N];
}
/**
* 思路: 归纳法
* 如果数是奇数,因为只用1与本身才能让余数为0,所以必然会让接下来的数变为偶数
* 首先要明确的是,到谁是数为2,那么就是谁赢,所以如果刚开始数是偶数,那么每次就让爱丽丝选1,这样每次在奇数是就让鲍勃来选,再让数变为偶数,最后一定是到爱丽丝选时,数为2
* 同理,如果数为奇数,那么到鲍勃时数为偶数,鲍勃也这么做就能取得胜利,所以最后判断胜利条件,就是数的奇偶性
* @param N
* @return
*/
public boolean divisorGame(int N) {
return N%2==0;
}
}
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
题解:
class Solution {
/**
* 思路:
* 状态定义: 设动态规划列表dp ,dp[i]代表以元素nums[i]为结尾的连续子数组最大和。
*
* 我们发现dp[i]就是以数组下标为i的数做为结尾的最大子序列和,
* 注意是以i为结尾,比如说现在有一个数组{6,-3,-2,7,-15,1,2,2},为了不搞,我们就下标以1开始,dp[3]就是以-2为结尾的,
* 那么显然dp[3]的最大值就是1咯(6,-3,-2),dp[4]要以7结尾那么以7结尾的子序列最大和就是8(6,-3,-2,7)。
*
* 知道dp[i]是啥后,现在我们开始细细品一下上面这个递推式,求dp[i]的时候是不是有两种可能,
* 要么就是像上面的dp[4]一样,dp[3]求出来是1了,再加上自己array[4]是最大的,那么还有一种可能就是说如果dp[3]我求出来是-100,
* 那如果我也是dp[3]+array[4]的话是-93,这时候dp[3]反而是累赘,最大就是自己(因为前面定义了必须以i为结尾,也就说必须以7结尾)。
*
* @param nums
* @return
*/
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i=1;i max) {
max = dp[i];
}
}
return max;
}
}
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
题解:
class Solution {
/**
* 要解前n间房能偷的最大金额,那就尝试先从前1、2、3间能偷的最大金额找规律
Sn表示前n间房能偷的最大金额 Hn表示第n间房的金额
示例: [1,2,3,1]
前1间房能偷的最大金额 S1 = H1 = 1
前2间房能偷的最大金额 S2 = max(S1,H1) = 2
从第三间房开始,就有2种偷法了,就是偷第n间房和不偷第n间房
偷第n间房,那就不能偷n-1间房,能偷n-2间房 那么最大金额不一样的取决于最近这三间房怎么偷,因为前Sn-2都一样
前3间房能偷的最大金额 S3 = max(S2, H3+S1) = 4
前n间房能偷的最大金额 Sn = max(Sn-1, Hn+Sn-2)
*/
public int rob(int[] nums) {
int length = nums.length;
if(length <= 1) {
return nums[0];
}
int[] dp = new int[length]; //dp[i]表示前i间房能偷的最大金额
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(int i=2;i
打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
题解:
class Solution {
public int eachRob(int[] nums) {
int length = nums.length;
if(length <= 1) {
return nums[0];
}
int[] dp = new int[length]; //dp[i]表示前i间房能偷的最大金额
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(int i=2;i
最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
题解:
class Solution {
/**
* 思路:
* 状态定义: 设动态规划列表dp ,dp[i]代表以元素nums[i]为结尾的连续子数组最大和。
*
* 我们发现dp[i]就是以数组下标为i的数做为结尾的最大子序列和,
* 注意是以i为结尾,比如说现在有一个数组{6,-3,-2,7,-15,1,2,2},为了不搞,我们就下标以1开始,dp[3]就是以-2为结尾的,
* 那么显然dp[3]的最大值就是1咯(6,-3,-2),dp[4]要以7结尾那么以7结尾的子序列最大和就是8(6,-3,-2,7)。
*
* 知道dp[i]是啥后,现在我们开始细细品一下上面这个递推式,求dp[i]的时候是不是有两种可能,
* 要么就是像上面的dp[4]一样,dp[3]求出来是1了,再加上自己array[4]是最大的,那么还有一种可能就是说如果dp[3]我求出来是-100,
* 那如果我也是dp[3]+array[4]的话是-93,这时候dp[3]反而是累赘,最大就是自己(因为前面定义了必须以i为结尾,也就说必须以7结尾)。
*
* @param nums
* @return
*/
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i=1;i max) {
max = dp[i];
}
}
return max;
}
}
参考:https://www.bilibili.com/video/BV18g4y1i7f9?spm_id_from=333.337.search-card.all.click&vd_source=40c24e77b23dc2e50de2b7c87c6fed59