目录
1. 什么是动态规划
动态规划(Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其子问题,再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
以上定义来自维基百科,看定义感觉还是有点抽象。用简单的话来说就是,给定一个问题,我们把它拆成一个个子问题进行求解,在求解子问题的过程中把子问题的答案保存起来,再根据子问题的答案反推,得出原问题的解。
2. 动态规划问题的核心思想
动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算。
3. 背包问题
待更新
4. LeetCode 动态规划
4.1. 一维动态规划
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
class Solution {
public int climbStairs(int n) {
int p = 1, q = 1, r = 2;
if (n == 1) {
return 1;
}
for (int i = 1; i <= n - 1; i++) {
r = p + q;
p = q;
q = r;
}
return r;
}
}
118. 杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
class Solution {
// 定义一个公共方法,接收一个整数参数numRows,并返回一个二维列表List<List<Integer>>
public List<List<Integer>> generate(int numRows) {
// 创建一个名为ret的二维列表,用于存储帕斯卡三角形的每一行
List<List<Integer>> ret = new ArrayList<List<Integer>>();
// 外层循环,用于生成每一行,i表示当前行号
for (int i = 0; i < numRows; ++i) {
// 创建一个名为row的一维列表,用于存储当前行的数字
List<Integer> row = new ArrayList<Integer>();
// 内层循环,用于生成当前行的每一个数字,j表示当前行的列号
for (int j = 0; j <= i; ++j) {
// 判断当前列是否是第一列或最后一列,帕斯卡三角形的每行的第一列和最后一列都是1
if (j == 0 || j == i) {
// 如果是第一列或最后一列,则添加1到当前行
row.add(1);
} else {
// 如果不是第一列或最后一列,则当前数字是上一行的前一个数字和当前数字之和
// ret.get(i - 1).get(j - 1)是上一行的前一个数字
// ret.get(i - 1).get(j)是上一行的当前数字
row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j));
}
}
// 将当前行添加到二维列表ret中
ret.add(row);
}
// 返回生成的帕斯卡三角形的二维列表
return ret;
}
}
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
}
279. 完全平方数
给你一个整数 n,返回和为 n 的完全平方数的最少数量 。
完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
class Solution {
public int numSquares(int n) {
// 创建一个整型数组 f,其大小为 n + 1,用于存储从 0 到 n 的每个数组成的最少完全平方数的数量
int f[] = new int[n + 1];
// 初始化 f[0] 为 0,因为组成 0 的完全平方数数量为 0
f[0] = 0;
// 使用一个循环,从 1 遍历到 n,计算每个数 i 组成的最少完全平方数的数量
for (int i = 1; i <= n; i++) {
// 初始化 f[i] 为 Integer.MAX_VALUE,表示初始情况下组成 i 的最少完全平方数的数量是最大的
f[i] = Integer.MAX_VALUE;
// 使用一个内层循环,从 1 遍历到 j*j <= i,找到所有小于等于 i 的完全平方数
for (int j = 1; j * j <= i; j++) {
// 计算 f[i - j * j] + 1(即组成 i - j*j 的最少完全平方数的数量加 1),并与当前的 f[i] 比较,取较小值
// 这是因为如果 i 可以由 j*j 和其他一些完全平方数组成,那么 f[i] 应该是 f[i - j * j] 加上 j*j 这个完全平方数
f[i] = Math.min(f[i - j * j] + 1, f[i]);
}
}
// 最后,返回 f[n],即组成 n 的最少完全平方数的数量
return f[n];
}
}
322. 零钱兑换
给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
class Solution {
public int coinChange(int[] coins, int amount) {
// 定义dp数组,行数为硬币种类数+1,列数为目标金额+1
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// 初始化:第0行(没有硬币时)需要设置为一个大数,表示无法凑成
for (int j = 1; j <= amount; j++) {
dp[0][j] = Integer.MAX_VALUE;
}
// 迭代每种硬币
for (int i = 1; i <= n; i++) {
// 迭代每个金额
for (int j = 0; j <= amount; j++) {
// 当前硬币面值大于当前金额,不选择当前硬币
if (j < coins[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
// 选择和不选择当前硬币,取最小值
if (dp[i][j - coins[i - 1]] != Integer.MAX_VALUE) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
}
// 返回结果,如果为Integer.MAX_VALUE,表示无法凑成,返回-1
return dp[n][amount] == Integer.MAX_VALUE ? -1 : dp[n][amount];
}
}
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
// 创建一个一维数组dp,dp[i]表示s中前i个字符组成的子串是否可以被字典中的单词拼接而成
boolean[] dp = new boolean[n + 1];
dp[0] = true; // 空字符串总是可以被拼接的
// 遍历字符串的每一个字符
for (int i = 1; i <= n; i++) {
// 检查所有可能的分割点
for (int j = 0; j < i; j++) {
// 如果前j个字符可以被拼接,并且从j到i的子串在字典中
if (dp[j] && wordDict.contains(s.substring(j, i))) {
dp[i] = true; // 那么前i个字符也可以被拼接
break; // 找到一个合适的分割点即可
}
}
}
// 如果dp[n]为true,则整个字符串可以被字典中的单词拼接而成
return dp[n];
}
}
300. 最长递增子序列
给你一个整数数组 nums,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
// 创建一个一维数组 dp,dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
int[] dp = new int[n];
// 初始化第一个元素的最长递增子序列长度为1
dp[0] = 1;
// 最长递增子序列的长度至少为1
int maxLength = 1;
// 遍历数组
for (int i = 1; i <= n - 1; i++) {
// 本来应该在前面初始化,但放在这可以节省时间
dp[i] = 1;
// 遍历所有在 i 之前的元素
for (int j = 0; j <= i - 1; j++) {
// 如果 nums[i] > nums[j],那么可以构成递增子序列
if (nums[i] > nums[j]) {
// 更新 dp[i] 为 dp[j] + 1 和当前 dp[i] 中的最大值
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 更新最长递增子序列的长度
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}
}
152. 乘积最大子数组
给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32 位整数。
class Solution {
public int maxProduct(int[] nums) {
int length = nums.length; // 获取数组长度
long[] maxF = new long[length]; // 创建一个数组来存储到当前位置为止的最大乘积
long[] minF = new long[length]; // 创建一个数组来存储到当前位置为止的最小乘积
for (int i = 0; i < length; i++) {
maxF[i] = nums[i]; // 初始化最大乘积数组,每个位置的初始值为数组对应位置的元素
minF[i] = nums[i]; // 初始化最小乘积数组,每个位置的初始值为数组对应位置的元素
}
for (int i = 1; i < length; ++i) {
// 更新最大乘积数组
maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
// 更新最小乘积数组
minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
// 如果最小乘积小于int的最小值,则重置为当前元素
if (minF[i] < (-1 << 31)) {
minF[i] = nums[i];
}
}
int ans = (int) maxF[0]; // 初始化结果为最大乘积数组的第一个元素
for (int i = 1; i < length; ++i) {
// 更新结果为最大乘积数组中的最大值
ans = Math.max(ans, (int) maxF[i]);
}
return ans; // 返回最大乘积
}
}
416. 分割等和子集
给你一个只包含正整数的非空数组 nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public boolean canPartition(int[] nums) {
//根据数组长度判断数组是否可以被划分,如果n<2,则不可能将数组分割成元素和相等的两个子集,直接返回false
if (nums.length < 2) {
return false;
}
//计算整个数组的元素和sum,如果sum是奇数,则不可能将数组分割成元素和相等的两个子集,直接返回false
//找到整个数组的最大元素maxNum
int sum = 0;
int maxNum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
maxNum = Math.max(maxNum, nums[i]);
}
if ((sum % 2) == 1) {
return false;
}
//sum是偶数
int target = sum / 2;
if (maxNum > target) {
return false;
}
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
for (int j = target; j >= num; j--) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}
}
32. 最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
}
}
4.2. 二维动态规划
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
//初始化第1行和第1列为1,因为只有1种路径
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 - 1; i++) {
for (int j = 1; j <= n - 1; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
64. 最小路径和
给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length;
int columns = grid[0].length;
int[][] dp = new int[rows][columns];
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
}
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
//如果s的长度等于1,则s是回文串,返回s
if (len == 1) {
return s;
}
int begin = 0;//最长回文串的开始索引
int maxLen = 1;//最长回文串的长度
//dp[i][j]=true表示从第i个字符到第j个字符组成的字符串是回文串
boolean[][] dp = new boolean[len][len];
//从回文串长度为1开始循环
for (int L = 1; L <= len; L++) {
//从第1个字符开始循环
for (int i = 0; i < len; i++) {
//j表示回文串最右侧字符下标
int j = i + L - 1;
if (j >= len) {
break;
}
//第i个字符与第j个字符不同,则从第i个字符到第j个字符组成的字符串肯定不是回文串
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
//如果j - i = 0/1/2,则从第i个字符到第j个字符组成的字符串肯定是回文串
if ((j - i) < 3) {
dp[i][j] = true;
} else {
//状态转移方程
dp[i][j] = dp[i + 1][j - 1];
}
}
if (dp[i][j] && (j - i + 1) > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列 ,返回 0。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
char c1 = text1.charAt(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.charAt(j - 1);
if (c1 == c2) {
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[m][n];
}
}
72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// 有一个字符串为空串
if (m * n == 0) {
return m + n;
}
int[][] dp = new int[m + 1][n + 1];
// 边界状态初始化
for (int i = 0; i < m + 1; i++) {
dp[i][0] = i;
}
for (int j = 0; j < n + 1; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
int left = dp[i - 1][j] + 1;
int down = dp[i][j - 1] + 1;
int left_down = dp[i - 1][j - 1];
if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
left_down += 1;
}
dp[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return dp[m][n];
}
}