动态规划(DP)
1.最大子序和(leetcode 53 S.)
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
常规:
class Solution {
public int maxSubArray(int[] nums) {
//定义和
int sum = 0;
//这里的j没必要了
int j = 0;
//令最大初始值为int的最小值
int max = Integer.MIN_VALUE;
for(int i=0;i<nums.length;++i){
j = i;
//如果i个连续和比第i个元素小,说明前i-1个元素不可能为最大和最大和为nums[i]
if(sum + nums[j] < nums[j]){
sum = nums[j];
}else{
//若前i个连续和大于等于第i个元素,最大和为前i个元素相加
sum += nums[j];
}
//如果当前最大和比max大
if(sum > max){
max = sum;
}
}
return max;
}
}
DP:
class Solution {
public int maxSubArray(int[] nums) {
//最大和数组
int[] dp = new int[nums.length];
//一个元素最大和为当前元素
dp[0] = nums[0];
int temp = nums[0];
for(int i = 1;i < nums.length;++i){
//这里发现用Math.max()函数效率低
if(dp[i - 1] + nums[i] < nums[i]){
dp[i] = nums[i];
}else{
dp[i] = dp[i - 1] + nums[i];
}
//dp[i] = Math.max(dp[i-1] + nums[i],nums[i]);
if(temp < dp[i]){
temp = dp[i];
}
//temp = Math.max(dp[i],temp);
}
return temp;
}
}
2.爬楼梯(leetcode 70 S.)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
分析:
假如n = 10,要想爬到楼顶,要么从第九层爬一个台阶上去,要么从第八层爬两个台阶上去
此时我们假设F(n) = 爬到第n层台阶有多少种方法
通过上面的分析我们知道爬到第十层的方法是第九层和第八层的和
也即 我们得到递推式F(n) = F(n - 1) + F(n - 2)
由递推式我们立马想到了用递归 首先找到终止条件
当只有一层楼梯的时候我们爬一层即可到达楼顶,即 F(1) = 1
当只有两层的时候我们可以一层一层的爬,也可以直接爬两层,即F(2) = 2
递归:
class Solution {
public int climbStairs(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
return climbStairs(n-1) + climbStairs(n-2);
}
}
这里可以看出当n很大时递归深度很深,时间复杂度为指数级别,导致超时,这里我们观察到climbStairs(n-2),climbStairs(n-3)....被调用的多次,如果我们可以找个容器将他的值存储下来,下次遇到的时候判断容器里面是否有这个函数的值,如果有直接拿出来用,如果没有计算以后把它放到容器中。
这种算法叫做备忘录算法
备忘录算法:
class Solution {
Map<Integer,Integer> map = new HashMap<>();
public int climbStairs(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
if(map.containsKey(n)){
return map.get(n);
}
int value = climbStairs(n-1) + climbStairs(n-2);
map.put(n,value);
return value;
}
}
但是使用备忘录算法算法时间复杂度还是很高,能不能不使用递归,这里我们正向思考
从第一层楼梯可以到达第二层或者第三层,第二层可以到达第三层或者第四层....以此类推
dp[1] | dp[2] | dp[3] | dp[4] | dp[5] | ... |
---|---|---|---|---|---|
1 | 2 | 3 | 5 | 8 | ... |
观察得知dp[i] = dp[i-1] + dp[i-2],i>=3
DP:
class Solution {
public int climbStairs(int n) {
//dp数组存放的是到达第n层共有多少方法
int[] dp = new int[n+1];
dp[1] = 1;
if(n > 1){
dp[2] = 2;
}
for(int i = 3;i <= n;++i){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
此时时间复杂度O(n) 空间复杂度O(n) 我们能不能优化空间?
由观察得:dp[i] = dp[i-1] + dp[i-2] 只与前两个数有关,那么我们没有必要存储所有的数了
优化DP:
class Solution {
public int climbStairs(int n) {
if(n == 1)
return 1;
//初始值
int a = 1;
int b = 2;
//temp代替数组
int temp;
for(int i = 3;i <= n;++i){
temp = a;
a = b;
b = b + temp;
}
return b;
}
}
3.买卖股票的最佳时机(leetcode 121 S.)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
分析:
观察题目我们知道一般有两个状态,手中有股票和手中无股票,我们记作0和1.
dp[0]表示手中无股的最大利润,这里分为两种情况,第一种是没有买也没有卖,这是利润为0,第二种情况是买卖结束了
dp[1]表示手中有股票
DP:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int[] dp = new int[2];
dp[0] = 0;
dp[1] = -prices[0];
for (int i = 1; i < len; i++) {
dp[0] = Math.max(dp[0], dp[1] + prices[i]);
dp[1] = Math.max(dp[1], -prices[i]);
}
return dp[0];
}
4.打家劫舍(leetcode 198 S.)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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 。
分析:
题目意思就是假如我偷了第一家,就不能偷第二家只能偷第三家、第四家...
若总共有10户人家,偷10户人家的最高金额就分为
1.偷了第九户人家(肯定不能偷第10户了)
2.没有偷第九户人家(这是可以偷第八户)
这时候要求出两种方式的最大值
得到关系式:
记第i户人家的金额是cost[i]
F(n) = max(F(n-1),F(n-2) + cost[n])
DP:
class Solution {
public int rob(int[] nums) {
if(nums.length == 0)
return 0;
int[] dp = new int[nums.length + 1];
dp[0] = nums[0];
if(nums.length > 1){
dp[1] = Math.max(nums[0],nums[1]);
}
for(int i = 2;i < nums.length;++i){
dp[i] = Math.max(dp[i-1],dp[i-2] + nums[i]);
}
return dp[nums.length-1];
}
}
cost数组:
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
2 | 7 | 9 | 3 | 1 |
dp数组:
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
2 | 7 | 11 | 11 | 12 |
这里我们观察结果只与前两个有关,我们可以定义临时变量存储优化空间为O(1)
class Solution {
public int rob(int[] nums) {
if(nums.length == 0)
return 0;
int a = nums[0];
if(nums.length == 1)
return a;
int b = Math.max(nums[0],nums[1]);
int temp;
for(int i = 2;i < nums.length;++i){
temp = a;
a = b;
b = Math.max(temp + nums[i],b);
}
return b;
}
}
5.区域检索-数组不可变(leetcode 303 S.)
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
说明:
你可以假设数组不可变。
会多次调用 sumRange 方法。
分析:
本题可以采用暴力求解,但是需要多次调用效率很低
那么可以在求解之前把所有的sumRange(0, i) i<=n 求出来 存放在dp数组中
所以
sumRange(0, 2) = dp[2];
sumRange(2, 5) = dp[5] - dp[1];
所以
sumRange(i, j) = dp[j] - dp[i-1] (i != 0)
sumRange(i, j) = dp[j] (i == 0)
DP:
class NumArray {
int[] dp;
public NumArray(int[] nums) {
dp = new int[nums.length];
if(nums.length > 0){
dp[0] = nums[0];
}
for(int i = 1;i < nums.length;++i){
dp[i] = dp[i-1] + nums[i];
}
}
public int sumRange(int i, int j) {
return (i == 0) ? dp[j] : dp[j] - dp[i-1];
}
}
6.判断子序列(leetcode 392 S.)
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
分析:
定义dp[i] [j] 表示i个s是否是j个t的子序列
class Solution {
public boolean isSubsequence(String s, String t) {
int sLen = s.length();
int tLen = t.length();
if(sLen == 0)
return true;
boolean[][] dp = new boolean[sLen+1][tLen+1];
for(int j = 0;j < tLen;++j){
dp[0][j] = true;
}
for(int i = 1;i <= sLen;++i){
for(int j = 1;j <= tLen;++j){
if(s.charAt(i - 1) == t.charAt(j - 1)){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = dp[i][j-1];
}
}
}
return dp[sLen][tLen];
}
}
本题使用DP效率是非常低的不是最优解
这里有个小小的优化
可以遍历s字符串 从t中查找 这里查到的index必须是越来越大的,如果index == -1那么匹配失败
代码:
class Solution {
public boolean isSubsequence(String s, String t) {
int index = 0;
for(int i = 0; i < s.length();++i){
if(t.indexOf(String.valueOf(s.charAt(i)),index) != -1){
index = t.indexOf(String.valueOf(s.charAt(i)),index) + 1;
continue;
}
return false;
}
return true;
}
}
7.使用最小花费爬楼梯(leetcode 746 S.)
数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 cost[i] (索引从0开始)。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
注意:
cost 的长度将会在 [2, 1000]。
每一个 cost[i] 将会是一个Integer类型,范围为 [0, 999]。
分析:
到达楼顶有两种方法
第一种从第一层开始到达楼顶
第二种从第二层开始到达楼顶
记dp[i]为从第i层到达楼顶
终点是从最后一层或者倒数第二层
这里可以得到关系式
dp[i] = cost[i] + min(dp[i+1],dp[i+2]) 这里的i是递减的
最终得到的结果就是dp[0]和dp[1]中最小的那个
代码:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len = cost.length;
int[] dp = new int[len];
dp[len-1] = cost[len-1];
dp[len-2] = cost[len-2];
for(int i = len-3;i >= 0;--i){
dp[i] = cost[i] + Math.min(dp[i+1],dp[i+2]);
}
return Math.min(dp[0],dp[1]);
}
}
8.除数博弈(leetcode 1025 S.)
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。
示例 1:
输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。
示例 2:
输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。
提示:
1 <= N <= 1000
9.最长回文子串(leetcode 5 M.)
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
分析:
最优子结构问题很容易想到动态规划
1.确定状态
2.找到状态转移方程
3.确定初值
确定状态
定义dp[] []二维数组,其中dp[i] [j]表示s串从i~j是否是回文
回文定义:一个字符串正着遍历和反着遍历结果相同那么该串就是回文串
例如:“aba”,"aa"是回文串,“abc”,"ab"就不是回文串
所以如果dp[i] [j]是回文串那么dp[i+1] [j-1]也一定是回文串
找到状态转移方程
如果s是空串 那么s本身就是回文串
当s[i] == s[j]时
如果i,j中只有一个字符,或者没有字符那么该串一定是回文串(j - i <=2)
否则判断i+1~j-1是不是回文(dp[i+1] [j-1] == true)那么该串也是回文串
如果该回文串的长度比之前记录的大那么该回文串为最大
代码:
class Solution {
public String longestPalindrome(String s) {
if(s.length() <= 1)
return s;
int len = s.length();
boolean[][] dp = new boolean[len][len];
int max = 1;
String res = s.substring(0,1);
for(int j = 1;j < len;++j){
for(int i = 0;i < j;++i){
//或运算的短路
if(s.charAt(i) == s.charAt(j) && (j - i <= 2 || dp[i+1][j-1])){
dp[i][j] = true;
if(j - i + 1 > max){
max = j - i + 1;
res = s.substring(i,j+1);
}
}
}
}
return res;
}
}
10.不同路径(leetcode 62 M.)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
向右 -> 向右 -> 向下
向右 -> 向下 -> 向右
向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
分析:
确定状态
定义dp数组,dp[i] [j]表示从起点到(i,j)共有多少路径。所以dp[m-1] [n-1]就是答案
找到状态转移方程
由于机器人只能向右或者向下走所以机器人只有从(i-1,j)或者(i,j-1)才能到达(i,j)
所以状态转移方程为:dp[i] [j] = dp[i-1] [j] + dp[i] [j-1];
确定初值
由状态转移方程我们无法算出dp[0] [j] 和dp[i] [0]的值,由观察知机器人到达dp[0] [j] 和dp[i] [0]均只有一条路径,那就是向右一直走,或者向下一直走
所以初值为:dp[i] [0] = 1;dp[0] [j] = 1;
代码:
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0;i < m;++i){
dp[i][0] = 1;
}
for(int i = 0;i < n;++i){
dp[0][i] = 1;
}
for(int i = 1;i < m;++i){
for(int j = 1;j < n;++j){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
这时候时间复杂度O(n^2),空间复杂度O(n^2)可以优化空间复杂度为O(n)
dp[i] [j] 只与dp[i-1] [j] 和dp[i] [j-1]有关所以只需要定义一维数组即可
代码:
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[m];
for(int i = 0;i < m;++i){
dp[i] = 1;
}
for(int j = 1;j < n;++j){
for(int i = 1; i < m;++i){
dp[i] = dp[i-1] + dp[i];
}
}
return dp[m-1];
}
}
11.不同路径Ⅱ(leetcode 63 M.)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
说明:m 和 n 的值均不超过 100。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
向右 -> 向右 -> 向下 -> 向下
向下 -> 向下 -> 向右 -> 向右
分析:
本题与上一题最大的区别在于加了一个障碍物,意思也就是经过障碍物到达终点的点全部无效
确定状态
记障碍物数组为a[] [],这里可以直接在障碍物数组进行修改a[i] [j]表示到(i,j)的方案数
找到状态转移方程
如果a[i] [j] = 1说明该处是障碍物不可到达,a[i] [j] = 0
否则该处可到达并且方案数a[i] [j] = a[i-1] [j] + a[i] [j-1]
确定初值
我们知道边界的情况下a[i] [0]或者a[0] [j] 取决于a[i-1] [0] 和a[0] [j]
也就是a[i] [0] = a[i-1] [0]; a[0] [j] = a[0] [j-1];
这里还需要考虑一个起点,如果起点是障碍物那么返回0
如果a[0] [0] = 0 那么a[0] [0] = 1;
代码:
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if(obstacleGrid[0][0] == 1){
return 0;
}
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
obstacleGrid[0][0] = 1;
for(int i = 1;i < m;++i){
if(obstacleGrid[i][0] == 1){
obstacleGrid[i][0] = 0;
continue;
}
obstacleGrid[i][0] = obstacleGrid[i-1][0];
}
for(int i = 1;i < n;++i){
if(obstacleGrid[0][i] == 1){
obstacleGrid[0][i] = 0;
continue;
}
obstacleGrid[0][i] = obstacleGrid[0][i-1];
}
for(int i = 1;i < m;++i){
for(int j = 1;j < n;++j){
if(obstacleGrid[i][j] == 1){
obstacleGrid[i][j] = 0;
continue;
}
obstacleGrid[i][j] = obstacleGrid[i-1][j] + obstacleGrid[i][j-1];
}
}
return obstacleGrid[m-1][n-1];
}
}
空间优化略
12.最小路径和(leetccode 64 M.)
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
分析:
最优子结构问题优先想DP
确定状态
定义dp[m] [n] 其中dp[i] [j] 表示从起点到(i,j)的最小路径
找到状态转移方程
显然(i,j)必定是由(i-1,j)或者(i,j-1)得到所以只需要找出两者的最小路径即可
所以状态转移方程为:dp[i] [j] = min(dp[i-1] [j],dp[i] [j-1]) + a[i] [j]
确定初值
边界上的点只有一种走法并且
dp[i] [0] = dp[i-1] [0] + a[i] [0]
dp[0] [j] = dp[0] [j-1] + a[0] [j]
代码:
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
for(int i = 1;i < m;++i){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int i = 1;i < n;++i){
dp[0][i] = dp[0][i-1] + grid[0][i];
}
for(int i = 1;i < m;++i){
for(int j = 1;j < n;++j){
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
}
空间优化略