动态规划
- 解决问题
- Max / Min
- Yes / No
- Count(*)
- Can’t sort / swap
- 四要素
- 状态 state
- 初始化 init
- 方程 function
- 结果 result
- 常见类型
- 矩阵DP
- 序列DP
- 双序列DP
- 背包问题
题 62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。
输入: m = 3, n = 2
输出: 3
My Answer:
class Solution {
public int uniquePaths(int m, int n) {
//state
int[][] f = new int[n][m];
//init
for(int i = 0; i < n; i++) f[i][0] = 1;
for(int i = 0; i < m; i++) f[0][i] = 1;
//function
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
//result
return f[n-1][m-1];
}
}
题 63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
My Answer:
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if(obstacleGrid[0][0] == 1){
return 0;
}
int n = obstacleGrid.length;
int m = obstacleGrid[0].length;
//state f 表示从[0][0]走到[i][j]有多少种不同的走法
int [][] f = new int[n][m];
//init
f[0][0] = 1;
int init_n = 1;
int init_m = 1;
for(int i = 1; i < n; i++){
if(obstacleGrid[i][0] == 1) init_n = 0;
f[i][0] = init_n;
}
for(int i = 1; i < m; i++){
if(obstacleGrid[0][i] == 1) init_m = 0;
f[0][i] = init_m;
}
//function
if(n > 1 && m >1){
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
if(obstacleGrid[i][j] == 1){
f[i][j] = 0;
}else{
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
}
}
//result
return f[n-1][m-1];
}
}
题 53. 最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
My Answer:
class Solution {
/* public int[][] matrix(int[] nums){
// DP 预处理解法 超时
int n = nums.length;
int[][] A = new int[n][n];
for(int i = 0; i < n; i++){
A[i][i] = nums[i];
}
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
A[i-j-1][i] = A[i-j][i] + nums[i-j-1];
}
}
return A;
}
public int maxSubArray(int[] nums) {
//state f[i]表示以前i项以i结尾的最大子数组和
int n = nums.length;
int [] f = new int[n];
int [][] sums = matrix(nums);
//init & function
for(int i = 0; i < n; i++){
f[i] = nums[i];
for(int j = 0; j < i; j++){
f[i] = Math.max(f[i],nums[j] + sums[j+1][i]);
}
}
//result
int result = f[0];
for(int i = 0; i < n; i++){
result = Math.max(result,f[i]);
}
return result;
} */
public int maxSubArray(int[] nums) {
int n = nums.length;
if(n == 1) return nums[0];
int sum = nums[0];
int maxsum = nums[0];
for(int i = 1; i < n; i++){
sum = Math.max(nums[i],sum + nums[i]);
maxsum = Math.max(maxsum,sum);
}
return maxsum;
}
}
题 64. 最小路径和
给定一个包含非负整数的 m x n
网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
My Answer:
class Solution {
public int minPathSum(int[][] grid) {
//state
int n = grid.length;
int m= grid[0].length;
int [][] f = new int[n][m];
//init
f[0][0] = grid[0][0];
for(int i = 1; i < n; i++){
f[i][0] = f[i-1][0] + grid[i][0];
}
for(int j = 1; j < m; j++){
f[0][j] = f[0][j-1] + grid[0][j];
}
//function
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
f[i][j] = Math.min(f[i-1][j],f[i][j-1]) + grid[i][j];
}
}
//result
return f[n-1][m-1];
}
}
题 70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
My Answer:
class Solution {
public int climbStairs(int n) {
//state 表示 爬到第i个台阶有多少种方案
int[] f = new int[n + 1];
if (n == 0) return 0;
if (n == 1) return 1;
if (n == 2) return 2;
//init
f[0] = 0; f[1] = 1; f[2] = 2;
//function
if(n > 2){
for(int i = 3; i <= n; i++){
f[i] = f[i-1] + f[i-2];
}
}
//result
return f[n];
}
}
题 72. 编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
My Answer:
class Solution {
public int minDistance(String word1, String word2) {
//state f[i][j] 表示 word1 的前 i 个字符配上 word2 的前 j 个字符最少需要编辑几次使他们相等
int n = word1.length();
int m = word2.length();
if(n == 0) return m;
if(m == 0) return n;
int [][] f = new int[n+1][m+1];
//init
for(int i = 0; i <= n; i++) f[i][0] = i;
for(int i = 0; i <= m; i++) f[0][i] = i;
//function
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
f[i][j] = Math.min(f[i-1][j-1],f[i-1][j] + 1);
f[i][j] = Math.min(f[i][j],f[i][j-1] + 1);
}else{
f[i][j] = Math.min(f[i][j-1],f[i-1][j]);
f[i][j] = Math.min(f[i][j],f[i-1][j-1]) + 1;
}
}
}
//result
return f[n][m];
}
}
题 97. 交错字符串
给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。
示例 1:
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出: true
示例 2:
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出: false
My Answer:
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
//state f[i][j] 表示第一个字符串前i个字符和第二个字符串前j个字符是否能够组成第3个字符串的前i+j个字符
int n = s1.length();
int m = s2.length();
int p = s3.length();
if(n + m != p) return false;
if(n == 0 && m ==0) return true;
if(m == 0) return s1.equals(s3);
if(n == 0) return s2.equals(s3);
boolean [][] f = new boolean[n+1][m+1];
//init
f[0][0] = true;
for(int i = 1; i <= n; i++){
f[i][0] = s1.charAt(i-1) == s3.charAt(i-1) && f[i-1][0];
}
for(int i = 1; i <= m; i++){
f[0][i] = s2.charAt(i-1) == s3.charAt(i-1) && f[0][i-1];
}
//function
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
f[i][j] = (f[i][j-1] && s2.charAt(j-1) == s3.charAt(i+j-1)) || ((f[i-1][j] && s1.charAt(i-1) == s3.charAt(i+j-1)));
}
}
//result
return f[n][m];
}
}
题 115. 不同的子序列
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,”ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
示例 1:
输入: S = "rabbbit", T = "rabbit"
输出: 3
解释:
如下图所示, 有 3 种可以从 S 中得到 “rabbit” 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
示例 2:
输入: S = "babgbag", T = "bag"
输出: 5
解释:
如下图所示, 有 5 种可以从 S 中得到 “bag” 的方案。
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^ ^
babgbag
^ ^^
babgbag
^ ^^
babgbag
^^^
My Answer:
class Solution {
public int numDistinct(String s, String t) {
//state
int n = s.length();
int m = t.length();
if(n == 0) return 0;
if(m == 0) return 1;
int [][] f = new int[n+1][m+1];
//init
for(int i = 0; i <= m; i++) f[0][i] = 0;
for(int i = 0; i <= n; i++) f[i][0] = 1;
//function
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(s.charAt(i-1) == t.charAt(j-1)){
f[i][j] = f[i-1][j] + f[i-1][j-1];
} else {
f[i][j] = f[i-1][j];
}
}
}
//result
return f[n][m];
}
}
题 120. 三角形最小路径和
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
My Answer:
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if (triangle == null || triangle.size() == 0) return -1;
if (triangle.get(0) == null || triangle.get(0).size() == 0) return -1;
int n = triangle.size();
int [][] f = new int[n][n];
//init
f[0][0] = triangle.get(0).get(0);
for(int i = 1; i < n; i++){
f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
//function
for(int i = 1; i < n; i++){
for(int j = 1; j < i; j++){
f[i][j] = Math.min(f[i - 1][j], f[i - 1][j - 1]) + triangle.get(i).get(j);
}
}
//result
int best = f[n - 1][0];
for (int i = 1; i < n; i++) {
best = Math.min(best, f[n - 1][i]);
}
return best;
}
}
题 121. 买卖股票的最佳时机
给定一个数组,它的第 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。
My Answer:
class Solution {
public int maxProfit(int[] prices) {
//state 前i天以第i天价格售出的最大利润
int n = prices.length;
int [] f = new int [n];
//init function
for(int i = 0; i < n; i++){
f[i] = 0;
for(int j = 0; i > j; j++){
if(prices[j] < prices[i]){
f[i] = f[i] > prices[i] - prices[j] ? f[i] : prices[i] - prices[j];
}
}
}
//result
int result = 0;
for(int i = 0; i < n; i++){
result = Math.max(result,f[i]);
}
return result;
}
}
题 132. 分割回文串 II
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
示例:
输入: "aab"
输出: 1
解释: 进行一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
My Answer:
class Solution {
public int minCut(String s) {
//state 前i个字符最少分割次数
int n = s.length();
int[] f = new int[n + 1];
//init
f[0] = -1;
//function
boolean [][] check = getCheck(s,n);
for(int i = 1; i <= n; i++){
f[i] = f[i - 1] + 1;
for(int j = 0; j < i; j++){
if(check[j][i-1]){
f[i] = Math.min(f[i],f[j]+1);
}
}
}
//result
return f[n];
}
public boolean[][] getCheck (String s,int n){
boolean[][] check = new boolean[n][n];
int i, j, p;
for (i = 0; i < n; ++i) {
for (j = 0; j < n; ++j) {
check[i][j] = false;
}
}
for (p = 0; p < n; ++p) {
i = j = p;
while (i >= 0 && j < n && s.charAt(i) == s.charAt(j)) {
check[i][j] = true;
--i;
++j;
}
}
for (p = 0; p < n-1; ++p) {
i = p;
j = p + 1;
while (i >= 0 && j < n && s.charAt(i) == s.charAt(j)) {
check[i][j] = true;
--i;
++j;
}
}
return check;
}
}
题 139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
My Answer:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
if (s == null || s.length() == 0) {
return true;
}
//state 前i个字符是否能被完美拆分
int maxLength = getMaxLength(wordDict);
int n = s.length();
boolean[] f = new boolean[n + 1];
//init
f[0] = true;
//function
for(int i = 1; i <= n; i++){
f[i] = false;
for(int j = 1; j <= maxLength && i >= j; j++){
if(f[i-j] && wordDict.contains(s.substring(i-j,i))){
f[i] = true;
break;
}
}
}
//result
return f[n];
}
private int getMaxLength(List<String> dict) {
int maxLength = 0;
for (String word : dict) {
maxLength = Math.max(maxLength, word.length());
}
return maxLength;
}
}
题 198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
示例 2:
输入: [2,7,9,3,1]
输出: 12
My Answer
class Solution {
public int rob(int[] nums) {
//state f[i] 表示前i个房间最高金额
int n = nums.length;
int [] f = new int[n+1];
//init
if(n == 0) return 0;
f[0] = 0;
f[1] = nums[0];
//function
for(int i = 2; i <= n; i++){
f[i] = Math.max(f[i-1],f[i-2] + nums[i-1]);
}
//result
return f[n];
}
}
题 300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
My Answer:
class Solution {
public int lengthOfLIS(int[] nums) {
//state f[i] 表示前i个数中以第i个结尾的LIS的长度
int n = nums.length;
int [] f = new int[n];
//init & function
for(int i = 0; i < n; i++){
f[i] = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i])
f[i] = f[i] > f[j] + 1 ? f[i] : f[j] + 1;
}
}
//result
int result = 0;
for(int i = 0; i < n; i++){
result = Math.max(result,f[i]);
}
return result;
}
}
题 303. 区域和检索 - 数组不可变
给定一个整数数组 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 方法。
My Answer:
//有趣的想法
class NumArray {
private:
vector<int> sum;
int n;
public:
NumArray(vector<int> const &nums) {
n = nums.size();
sum.resize(n + 1);
for (int i = 0; i < n; ++i) {
sum[i + 1] = sum[i] + nums[i];
}
}
int sumRange(int i, int j) {
return sum[j + 1] - sum[i];
}
~NumArray() {
sum.clear();
}
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(i,j);
*/
题 338. 比特位计数
给定一个非负整数 num
。对于 0 ≤ i ≤ num
范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2
输出: [0,1,1]
示例 2:
输入: 5
输出: [0,1,1,2,1,2]
My Answer:
class Solution {
public int NumberOf1(int n) {
int count = 0;
while(n! = 0){
count++;
n = n & (n - 1);//每次都消除该数中最右边的1
}
return count;
}
public int[] countBits(int num) {
int [] res = new int[num + 1];
for(int i = 0; i <= num; i++){
res[i] = NumberOf1(i);
}
return res;
}
}
题 413. 等差数列划分
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,以下数列为等差数列:
1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9
以下数列不是等差数列。
1, 1, 2, 5, 7
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),
P 与 Q 是整数且满足 0 <= P < Q < N
。
如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], ..., A[Q - 1], A[Q]
是等差的。并且 P + 1 < Q
。
函数要返回数组 A 中所有为等差数组的子数组个数。
示例:
A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
My Answer:
class Solution {
public int numberOfArithmeticSlices(int[] A) {
//state f[i] 表示前i个数以A[i]结尾的等差数列数量
int n = A.length;
int[] f = new int[n];
//init
if(n < 3) return 0;
//function
for(int i = 0; i < n; i++){
f[i] = 0;
if(i < 2) continue;
if(A[i] - A[i-1] == A[i-1] - A[i-2]){
f[i] = f[i-1] + 1;
}
}
//result
int result = 0;
for(int i = 0; i < n; i++){
result += f[i];
}
return result;
}
}
题 494. 目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例 1:
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
注意:
- 数组的长度不会超过20,并且数组中的值全为正数。
- 初始的数组的和不会超过1000。
- 保证返回的最终结果为32位整数。
My Answer:
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (S > sum || (sum + S) % 2 == 1)
return 0;
return subsetSum(nums, (sum + S) / 2);
}
private int subsetSum(int[] nums, int S) {
int[] dp = new int[S + 1];
dp[0] = 1;//C(0,0)=1
for (int i = 0; i < nums.length; i++) {
for (int j = S; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[S];
}
}
题 746. 使用最小花费爬楼梯
数组的每个索引做为一个阶梯,第 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]。
My Answer:
class Solution {
public int minCostClimbingStairs(int[] cost) {
//state f[i] 表示从走到阶梯i最小的花费
int[] f = new int[cost.length];
if(cost.length == 1){
return cost[0];
}
if(cost.length == 2){
return cost[1];
}
//init
f[0] = cost[0];
f[1] = cost[1];
//function
if(cost.length > 2){
for(int i = 2; i < cost.length; i++){
f[i] = Math.min(f[i-1],f[i-2]) + cost[i];
}
}
//result
return Math.min(f[cost.length - 1],f[cost.length - 2]);
}
}