一、动态规划
1)动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)
相同点:找到重复子问题
不同点:最优子结构、中途可以淘汰次优解
2)使用DP解决问题的步骤
- 找到重复子问题
- 状态定义
- 写出DP方程
二、动态规划相关题目
1、LeetCode62:不同路径
一个机器人位于一个m x n
网格的左上角 (起始点在下图中标记为“Start” )
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)
问总共有多少条不同的路径?
例如,上图是一个7 x 3
的网格。有多少可能的路径?
示例1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例2:
输入: m = 7, n = 3
输出: 28
题解:
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < n; ++i) dp[0][i] = 1;
for (int i = 0; i < m; ++i) dp[i][0] = 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];
}
2、LeetCode63:不同路径 II
一个机器人位于一个m x n
网格的左上角 (起始点在下图中标记为“Start” )
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用1和0来表示
示例1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
题解:
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if (obstacleGrid[0][0] == 1) return 0;
obstacleGrid[0][0] = 1;
for (int i = 1; i < m; ++i)
obstacleGrid[i][0] = (obstacleGrid[i][0] == 0 && obstacleGrid[i - 1][0] == 1) ? 1 : 0;
for (int i = 1; i < n; ++i)
obstacleGrid[0][i] = (obstacleGrid[0][i] == 0 && obstacleGrid[0][i - 1] == 1) ? 1 : 0;
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (obstacleGrid[i][j] == 1) {
obstacleGrid[i][j] = 0;
} else {
obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1];
}
}
}
return obstacleGrid[m - 1][n - 1];
}
3、LeetCode120:三角形最小路径和
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为11(即 2 + 3 + 5 + 1 = 11 2 + 3 + 5 + 1 = 11 2+3+5+1=11)
1)递归(自顶向下):
public int minimumTotal(List<List<Integer>> triangle) {
int row = triangle.size();
return helper(0, 0, row, triangle);
}
private int helper(int level, int i, int row, List<List<Integer>> triangle) {
if (level == row - 1) {
return triangle.get(level).get(i);
}
int left = helper(level + 1, i, row, triangle);
int right = helper(level + 1, i + 1, row, triangle);
return Math.min(left, right) + triangle.get(level).get(i);
}
2)递归+缓存(自顶向下):
public int minimumTotal(List<List<Integer>> triangle) {
int row = triangle.size();
Integer[][] memory = new Integer[row][row];
return helper(0, 0, memory, row, triangle);
}
private int helper(int level, int i, Integer[][] memory, int row, List<List<Integer>> triangle) {
if (memory[level][i] != null) {
return memory[level][i];
}
if (level == row - 1) {
return memory[level][i] = triangle.get(level).get(i);
}
int left = helper(level + 1, i, memory, row, triangle);
int right = helper(level + 1, i + 1, memory, row, triangle);
return memory[level][i] = Math.min(left, right) + triangle.get(level).get(i);
}
3)动态规划(自底向上):
public int minimumTotal(List<List<Integer>> triangle) {
int row = triangle.size();
int[] minlen = new int[row + 1];
for (int level = row - 1; level >= 0; level--) {
for (int i = 0; i <= level; i++) { //第i行有i+1个数字
minlen[i] = Math.min(minlen[i], minlen[i + 1]) + triangle.get(level).get(i);
}
}
return minlen[0];
}
4、LeetCode1143:最长公共子序列
给定两个字符串text1和text2,返回这两个字符串的最长公共子序列
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串
例如,"ace"是"abcde"的子序列,但"aec"不是"abcde"的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列
若这两个字符串没有公共子序列,则返回0
示例1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3
示例2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3
示例3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0
题解:
public int longestCommonSubsequence(String text1, String text2) {
char[] s1 = text1.toCharArray();
char[] s2 = text2.toCharArray();
int[][] dp = new int[s1.length + 1][s2.length + 1];
for (int i = 1; i < s1.length + 1; i++) {
for (int j = 1; j < s2.length + 1; j++) {
if (s1[i - 1] == s2[j - 1]) {
//如果末端相同
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
//如果末端不同
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[s1.length][s2.length];
}
5、LeetCode53:最大子序和
给定一个整数数组nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为6
题解:
//最大子序和=当前元素自身或者包含之前的元素
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; ++i) {
dp[i] = Math.max(0, dp[i - 1]) + nums[i];
}
Arrays.sort(dp);
return dp[dp.length - 1];
}
优化后:
public int maxSubArray(int[] nums) {
int result = nums[0], sum = 0;
for (int num : nums) {
if (sum > 0) sum += num;
else sum = num;
result = Math.max(result, sum);
}
return result;
}
6、LeetCode152:乘积最大子序列
给定一个整数数组nums,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)
示例1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6
示例2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组
解析:
https://leetcode-cn.com/problems/maximum-product-subarray/solution/hua-jie-suan-fa-152-cheng-ji-zui-da-zi-xu-lie-by-g/
题解:
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE, imax = 1, imin = 1;
for (int i = 0; i < nums.length; i++) {
if (nums[i] < 0) {
int tmp = imax;
imax = imin;
imin = tmp;
}
imax = Math.max(imax * nums[i], nums[i]);
imin = Math.min(imin * nums[i], nums[i]);
max = Math.max(max, imax);
}
return max;
}
7、LeetCode322:零钱兑换
给定不同面额的硬币coins和一个总金额amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1
示例1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例2:
输入: coins = [2], amount = 3
输出: -1
题解:
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
dp[0] = 0;
for (int i = 1; i < dp.length; ++i) {
int cost = amount + 1;
for (int j = 0; j < coins.length; ++j) {
if (i - coins[j] >= 0 && dp[i - coins[j]] != amount + 1)
cost = Math.min(cost, dp[i - coins[j]] + 1);
}
dp[i] = cost;
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
8、LeetCode198:打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额
示例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
题解:
public int rob(int[] nums) {
int len = nums.length;
if (len == 0)
return 0;
int[] dp = new int[len + 1];
dp[0] = 0;
dp[1] = nums[0];
for (int i = 2; i <= len; i++) {
//在当前位置n房屋可盗窃的最大值,要么就是n-1房屋可盗窃的最大值,要么就是n-2房屋可盗窃的最大值加上当前房屋的值
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
return dp[len];
}
优化后:
public int rob(int[] num) {
int prevMax = 0;
int currMax = 0;
for (int x : num) {
int temp = currMax;
currMax = Math.max(prevMax + x, currMax);
prevMax = temp;
}
return currMax;
}
9、LeetCode213:打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额
示例1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的
示例2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)
偷窃到的最高金额 = 1 + 3 = 4
题解:
public int rob(int[] nums) {
int len = nums.length;
if (len == 0) return 0;
if (len == 1) return nums[0];
//去掉第一个元素和去掉最后一个元素的数组分别进行dp
return Math.max(helper(nums, 0, len - 2), helper(nums, 1, len - 1));
}
private int helper(int[] nums, int start, int end) {
int preMax = 0, curMax = 0;
for (int i = start; i <= end; ++i) {
int tmp = curMax;
curMax = Math.max(preMax + nums[i], curMax);
preMax = tmp;
}
return curMax;
}