动态规划
通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问
题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规
划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
同时,我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。
基本动态规划:一维
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
思路:
这是十分经典的斐波那契数列题。我们用 f(x)表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:
f(x)=f(x−1)+f(x−2)
它意味着爬到第 x级台阶的方案数是爬到第 x−1级台阶的方案数和爬到第 x−2级台阶的方案数的和。
f(0)=1
代码:
class Solution {
public:
int climbStairs(int n) {
vector<int> a(n+1,1);
for (int i = 2; i <= n; i++)
a[i] = a[i - 1] + a[i - 2];
return a[n];
}
};
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路:
因为不能相邻此本题的状态转移方程为 dp[i] = max(dp[i-1],nums[i-1] + dp[i-2])
代码:
class Solution {
public:
int rob(vector<int>& nums) {
int s = nums.size();
if (s == 2)
return fmax(nums[0], nums[1]);
if (s >= 3)
nums[2] = fmax(nums[0] + nums[2], nums[1]);
for (int i = 3; i < s; i++)
nums[i] = fmax(nums[i - 3] + nums[i], fmax(nums[i - 2] + nums[i], nums[i - 1]));
return nums[s - 1];
}
};
413. 等差数列划分
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
示例 1:
输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
思路:
3个连续为1个等差数列,4个连续为3个,5个连续为6个,总结此规律,若nums[i]-nums[i-1]==nums[i-1]-nums[i-2],则记a[i]=a[i-1]+1,最后a的总和即为所求
代码:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
vector<int> a(nums.size(),0);
for (int i = 2; i < nums.size(); i++) {
if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2])
a[i] = a[i - 1] + 1;
}
return accumulate(a.begin(),a.end(),0);
}
};
基本动态规划:二维
64. 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路:
二维dp数组,顺序查找,dp[i][j]=min{dp[i][j-1]+v,dp[i-1][j-1]+v}
代码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for (int i = 1; i < m; i++)
grid[i][0] += grid[i - 1][0];
for (int i = 1; i < n; i++)
grid[0][i] += grid[0][i - 1];
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++)
grid[i][j] += fmin(grid[i - 1][j], grid[i][j - 1]);
}
return grid[m - 1][n - 1];
}
};
542. 01 矩阵
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
思路:
①首先把每个源点 0 入队,然后从各个 0同时开始一圈一圈的向 1 扩散(每个 1 都是被离它最近的 0 扩散到的 ),扩散的时候可以设置 int[][] dist 来记录距离(即扩散的层次)并同时标志是否访问过。对于本题是可以直接修改原数组 int[][] matrix 来记录距离和标志是否访问的,这里要注意先把 matrix 数组中 1 的位置设置成 -1 (设成Integer.MAX_VALUE啦,m * n啦,10000啦都行,只要是个无效的距离值来标志这个位置的 1 没有被访问过就行)
②对于任一点 (i,j),距离 0 的距离为:
f(i,j)={1+min(f(i−1,j),f(i,j−1),f(i+1,j),f(i,j+1)) if matrix[i][j] == 1
0 if matrix[i][j] == 0}
因此我们用 dp[i][j] 来表示该位置距离最近的 0的距离。
我们发现 dp[i][j] 是由其上下左右四个状态来决定,无法从一个方向开始递推!
于是我们尝试将问题分解:
距离 (i,j) 最近的 0 的位置,是在其 「左上,右上,左下,右下」4个方向之一;
因此我们分别从四个角开始递推,就分别得到了位于「左上方、右上方、左下方、右下方」距离 (i,j)的最近的 0 的距离,取 min 即可;
更加简化的算法:左上到右下,右下到左上两次动态搜索
代码:
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
if (mat.empty()) return {};
int n = mat.size(), m = mat[0].size();
vector<vector<int>> dp(n, vector<int>(m, INT_MAX - 1));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (mat[i][j] == 0)
dp[i][j] = 0;
else {
if (j > 0)
dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
if (i > 0)
dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
}
}
}
for (int i = n - 1; i >= 0; --i) {
for (int j = m - 1; j >= 0; --j) {
if (mat[i][j] != 0) {
if (j < m - 1)
dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
if (i < n - 1)
dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
}
}
}
return dp;
}
};
221. 最大正方形
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
思路:
对于在矩阵内搜索正方形或长方形的题型,一种常见的做法是定义一个二维 dp 数组,其中dp[i][j] 表示满足题目条件的、以 (i, j) 为右下角的正方形或者长方形的属性。对于本题,则表示以 (i, j) 为右下角的全由 1 构成的最大正方形边长。如果当前位置是 0,那么 dp[i][j] 即为 0;如果当前位置是 1,我们假设 dp[i][j] = k,其充分条件为 dp[i-1][j-1]、dp[i][j-1] 和dp[i-1][j] 的值必须都不小于 (k − 1),否则 (i, j) 位置不可以构成一个边长为 k 的正方形。同理,如果这三个值中的的最小值为 k − 1,则 (i, j) 位置一定且最大可以构成一个边长为 k 的正方形。
dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j])) + 1;
代码:‘.
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int m = matrix.size(), n = matrix[0].size(), max = 0;
vector<vector<int>> a(m, vector<int>(n));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
a[i][j] = matrix[i][j] - '0';
if (i > 0 && j > 0) {
if (matrix[i][j] == '1')
a[i][j] = min(min(a[i][j - 1], a[i - 1][j]), a[i - 1][j - 1]) + 1;
}
if (a[i][j] > max)
max = a[i][j];
}
}
return pow(max, 2);
}
};
分割类型题
279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
思路:
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。我们定义一个一维矩阵 dp,其中 dp[i] 表示数字 i 最少可以由几个完全平方数相加构成。在本题中,位置 i 只依赖 i - k2 的位置,如 i - 1、i - 4、i - 9 等等,才能满足完全平方分割的条件。因此 dp[i] 可以取的最小值即为 1 + min(dp[i-1], dp[i-4], dp[i-9] · · ·
d[0]=0
代码:
class Solution {
public:
int numSquares(int n) {
vector<int> a(n+1, INT_MAX - 1);
a[0] = 0;
for (int j = 0; j <= n; j++) {
for (int i = sqrt(j); i > 0; i--)
a[j] = min(a[j], a[j - pow(i,2)] + 1);
}
return a[n];
}
};
91. 解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
思路:
因为有一位数,两位数,类似70爬楼梯,只是要注意若数字开头为0,一定要和上一位组成解码,去掉上一位方法数应该和上两位相等,而且当相邻数字大于26时,只能取单
dp[i]=dp[i-2]+dp[i-1]
(分情况考虑:当前值为0,它与前一位不能组成数时(不在1-26间)返回0,当前值不为0,它与前一位不能组成数时(不在11-26间(前1位是0时小于11)),a[i]=a[i-1])
代码:
class Solution {
public:
int numDecodings(string s) {
vector<int> a(s.size() + 1, 1);
int b;
if (s[0] == '0')
return 0;
for (int i = 2; i < s.size() + 1; i++) {
if (s[i - 1] == '0') {
b = (s[i - 2] - '0') * 10;
if (b * 10 < 1 || b > 26)
return 0;
else
a[i] = a[i - 2];
}
else {
b=(s[i - 2] - '0') * 10 + (s[i - 1] - '0');
if ( b > 26 || b < 11)
a[i] = a[i - 1];
else
a[i] = a[i - 1] + a[i - 2];
}
}
return a[s.size()];
}
};
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
(注意:分割后要各段一定是单词才是true)
思路:
类似于279完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置 0,需要初始化值为真。
dp[i] = dp[i] || dp[i - len];(当前位置切割如果能获得一个单词,查看在该单词开头处的切割是否合理)
(节省时间小办法:①按长度排序,如果遍历到单词长于当前长度,直接下一位②如果遍历到的单词对应的长度为l,那么a[j-l]不为true就说明匹不匹配都是false,就可以直接跳过当前单词)
代码:
class Solution {
public:
static bool comp(const string& a, const string& b) {
return a.size() < b.size();
}
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> a(s.size() + 1, false);
a[0] = true;
int l;
string b;
sort(wordDict.begin(), wordDict.end(), comp);
for (int j = 0; j < s.size(); j++) {
for (auto w : wordDict) {
l = j - w.size() + 1;
if (l >= 0) {
if (a[l]) {
b.assign(s.begin() + l, s.begin() + j + 1);
if (w.compare(b) == 0) {
a[j + 1] = true;
break;
}
}
}
else
break;
}
}
return a[s.size()];
}
};
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
思路:
对于子序列问题,第一种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示以 i 结尾的子序列的性质。在处理好每个位置后,统计一遍各个位置的结果即可得到题目要求的结果。
在本题中,dp[i] 可以表示以 i 结尾的、最长子序列长度。对于每一个位置 i,如果其之前的某个位置 j 所对应的数字小于位置 i 所对应的数字,则我们可以获得一个以 i 结尾的、长度为 dp[j]+ 1 的子序列。为了遍历所有情况,我们需要 i 和 j 进行两层循环,其时间复杂度为 O(n^2)。
本题还可以使用二分查找将时间复杂度降低为 O(n log n)。我们定义一个 dp 数组,其中 dp[k]存储长度为 k+1 的最长递增子序列的最后一个数字。我们遍历每一个位置 i,如果其对应的数字大于 dp 数组中所有数字的值,那么我们把它放在 dp 数组尾部,表示最长递增子序列长度加 1;**如果我们发现这个数字在 dp 数组中比数字 a 大、比数字 b 小,则我们将 b 更新为此数字,使得之后构成递增序列的可能性增大。**以这种方式维护的 dp 数组永远是递增的,因此可以用二分查找加速搜索。
代码:
class Solution {
public:
void half(int tar, vector<int>& a, int cnt) {
int l = 0, mid;
while (l < cnt) {
mid = (l + cnt) / 2;
if (tar > a[mid])
l = mid + 1;
else
cnt = mid;
}
a[l] = tar;
}
int lengthOfLIS(vector<int>& nums) {
vector<int> a(nums.size(), INT_MAX);
int cnt = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] < a[cnt])
half(nums[i], a, cnt);
else if(nums[i] > a[cnt])
a[++cnt] = nums[i];
}
return cnt == 0 ? 1 : cnt + 1;
}
};
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
思路:
对于子序列问题,第二种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示到位置 i 为止的子序列的性质,并不必须以 i 结尾。这样 dp 数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。
在本题中,我们可以建立一个二维数组 dp,其中 dp[i][j] 表示到第一个字符串位置 i 为止、到第二个字符串位置 j 为止、最长的公共子序列长度。这样一来我们就可以很方便地分情况讨论这两个位置对应的字母相同与不同的情况了
考虑动态规划的边界情况:
当 i=0时,text1[0:i]为空,空字符串和任何字符串的最长公共子序列的长度都是 0,因此对任意 0≤j≤n ,有 dp[0][j]=0;
当 j=0时,text2[0:j]为空,同理可得,对任意 0≤i≤m,有 dp[i][0]=0。
因此动态规划的边界情况是:当 i=0或 j=0时,dp[i][j]=0。
当 i>0且 j>0时,考虑 dp[i][j] 的计算:
当 text1[i−1]=text2[j−1]时,将这两个相同的字符称为公共字符,考虑 text1[0:i−1] 和 text2[0:j−1]的最长公共子序列,再增加一个字符(即公共字符)即可得到 text1[0:i]和 text2[0:j]的最长公共子序列,因此 dp[i][j]=dp[i−1][j−1]+1。
当 text1[i−1]≠text2[j−1]时,考虑以下两项:
text1[0:i−1]和 text2[0:j]的最长公共子序列;
text1[0:i]和 text2[0:j−1]的最长公共子序列。
要得到 text1[0:i]和 text2[0:j]的最长公共子序列,应取两项中的长度较大的一项,因此 dp[i][j]=max(dp[i−1][j],dp[i][j−1])。
代码:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size(), vector<int>(text2.size(), 0));
for (int i = 0; i < text1.size(); i++) {
for (int j = 0; j < text2.size(); j++) {
if (text1[i] == text2[j]) {
if(i > 0 && j > 0)
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = 1;
}
else {
if (j)
dp[i][j] = max(dp[i][j - 1], dp[i][j]);
if (i)
dp[i][j] = max(dp[i - 1][j], dp[i][j]);
}
}
}
return dp[text1.size() - 1][text2.size() - 1];
}
};
背包问题
背包问题是一种组合优化的 NP 完全问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为无界背包问题或完全背包问题
我们可以定义一个二维数组 dp存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 dp[i][j]= dp[i-1][j],即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j) {
if (j >= w) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
我们可以进一步对 0-1 背包进行空间优化,将空间复杂度降低为 O(W)。假设我们目前考虑物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j,我们可以得到 dp[2][j]= max(dp[1][j], dp[1][j-2] + 3)。这里可以发现我们永远只依赖于上一排 i = 1 的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉 dp 矩阵的第一个维度,在考虑物品 i 时变成 dp[j]= max(dp[j], dp[j-w] + v)。这里要注意我们在遍历每一行的时候必须逆向遍历,这样才能够调用上一行物品 i-1 时 dp[j-w] 的值;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到j 之前就已经被更新成物品 i 的值了。
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = W; j >= w; --j) {
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}
在完全背包问题中,一个物品可以拿多次。如图上半部分所示,假设我们遍历到物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j = 5,最多只能装下 2 个该物品。那么我们的状态转移方程就变成了 dp[2][5] = max(dp[1][5], dp[1][3] + 3, dp[1][1] + 6)。如果采用这种方法,假设背包容量无穷大而物体的体积无穷小,我们这里的比较次数也会趋近于无穷大,远超 O(NW) 的时间复杂度。
怎么解决这个问题呢?我们发现在 dp[2][3] 的时候我们其实已经考虑了 dp[1][3] 和 dp[2][1]的情况,而在时 dp[2][1] 也已经考虑了 dp[1][1] 的情况。因此,如图下半部分所示,对于拿多个物品的情况,我们只需考虑 dp[2][3] 即可,即 dp[2][5] = max(dp[1][5], dp[2][3] + 3)。这样,我们就得到了完全背包问题的状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i。
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j) {
if (j >= w) {
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v);
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
同样的,我们也可以利用空间压缩将时间复杂度降低为 O(W)。这里要注意我们在遍历每一行的时候必须正向遍历,因为我们需要利用当前物品在第 j-w 列的信息。
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = w; j <= W; ++j) {
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}
口诀:0-1 背包对物品的迭代放在外层,里层的体积或价值逆向遍历;完全背包对物品的迭代放在里层,外层的体积或价值正向遍历。
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:
由题,两个子集的总和肯定确定,可以看作0-1背包问题
代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2)
return false;
vector<int> dp(sum / 2 + 1, sum / 2);
for (int i = 0; i < nums.size(); i++) {
for (int j = sum / 2; j >= nums[i]; j--) {
dp[j] = min(dp[j], dp[j - nums[i]] - nums[i]);
if (j == sum / 2 && dp[j] == 0)
return true;
}
}
return false;
}
};
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
思路:
这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。我们在这里直
接展示三维空间压缩到二维后的写法。
for (int i = m; i >= count0; --i) {
for (int j = n; j >= count1; --j) {
dp[i][j] = max(dp[i][j], 1 + dp[i-count0][j-count1]);
}
}
代码:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int cnt1, cnt0;
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i < strs.size(); i++) {
cnt0 = cnt1 = 0;
for (int j = 0; j < strs[i].size(); j++) {
if (strs[i][j] == '0')
cnt0++;
else
cnt1++;
}
for (int o = m; o >= cnt0; o--) {
for (int p = n; p >= cnt1; p--)
dp[o][p] = max(dp[o][p], dp[o - cnt0][p - cnt1] + 1);
}
}
return dp[m][n];
}
};
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路:
完全背包问题,只是求max改为min
分割类型规划
F(11)=min(F(11-1),F(11-2),F(11-5))+1=3
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX - 1);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++)
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
return dp[amount] == INT_MAX - 1 ? -1 : dp[amount];
}
};
字符串编辑
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector> dp(word1.size() + 1, vector(word2.size() + 1, INT_MAX - 1));
for (int i = 0; i < word1.size() + 1; i++)
dp[i][0] = i;
for (int i = 0; i < word2.size() + 1; i++)
dp[0][i] = i;
for (int i = 1; i <= word1.size(); i++) {
for (int j = 1; j <= word2.size(); j++) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
}
}
return dp[word1.size()][word2.size()];
}
};
插入一个字符
删除一个字符
替换一个字符
思路:
类似1143求最长公共子序列,我们使用一个二维数组 dp[i][j],表示将第一个字符串到位置 i 为止,和第二个字符串到位置 j 为止,最多需要几步编辑。当第 i 位和第 j 位对应的字符相同时,dp[i][j] 等 于 dp[i-1][j-1];当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1,插入 i 位置/删除 j 位置的消耗是dp[i][j-1] + 1,插入 j 位置/删除 i 位置的消耗是 dp[i-1][j] + 1。
dp[i][j] = min(dp[i-1][j-1] + ((word1[i] == word2[j])? 0: 1),min(dp[i-1][j] + 1, dp[i][j-1] + 1));
代码:
在这里插入代码片
650. 只有两个键的键盘
最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作:
Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
Paste(粘贴):粘贴 上一次 复制的字符。
给你一个数字 n ,你需要使用最少的操作次数,在记事本上输出 恰好 n 个 ‘A’ 。返回能够打印出 n 个 ‘A’ 的最少操作次数。
思路:
不同于以往通过加减实现的动态规划,这里需要乘除法来计算位置,因为粘贴操作是倍数增加的。
对每一个格子i(i个A),如果i可以被j除尽,说明j个A可以通过复制粘贴得到i个A,复制粘贴次数为i / j。
每个格子的意义:得到目前数量个A需要的最少操作次数
递推公式:dp[i]=min(dp[i],dp[j]+i/j),其中i % j == 0
代码:
class Solution {
public:
int minSteps(int n) {
vector<int> dp(n + 1, 2000);
dp[1] = 0;
int mid;
for (int i = 1; i <= n; i++) {
mid = i / 2;
for (int j = mid; j > 0; j--) {
if (i % j == 0)
dp[i] = min(dp[j] + i / j, dp[i]);
}
}
return dp[n];
}
};
10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入:s = “aa” p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
思路:
我们可以使用一个二维数组 dp,其中 dp[i][j] 表示以 i 截止的字符串是否可以被以 j 截止的正则表达式匹配。
j指向的是匹配单字符,dp[i][j] = dp[i][j-1];
指向的是数字,dp[i][j] = dp[i-1][j-1]&&(s[i]==p[j]);
指向的是*,dp[i][j] 见代码
代码:
class Solution {
public:
bool isMatch(string s, string p) {
vector<vector<bool>> dp(s.size() + 1, vector<bool>(p.size() + 1, true));
for (int i = 1; i <= s.size(); i++)
dp[i][0] = false;
for (int i = 1; i <= p.size(); i++) {//两个for先对边界dp做计算
if (p[i - 1] == '*')
dp[0][i] = dp[0][i - 2];
else
dp[0][i] = false;
}
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= p.size(); j++) {
if (p[j - 1] == '.')//如果匹配到了’.’,那么必定要匹配一个字符,所以是dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1];
else if (p[j - 1] == '*') {//如果匹配到了’*’,那么需要分情况讨论
if ((p[j - 2] == '.' || s[i - 1] == p[j - 2]) && dp[i - 1][j]) //如果p前一个字符是'.',或者s当前字符和p前一个字符匹配,那么我们就要讨论这个*号到底匹不匹配字符,利用动态规划,如果dp[i - 1][j]为true,说明这个*号是可以消耗一个匹配额度的
dp[i][j] = true;
else//如果dp[i - 1][j]是false或者sp不匹配的话(如”a”,”ab*a*”),说明这个*号只能匹配0个字符了,那么就是p前移两个字符的情况:dp[i][j] = dp[i][j - 2]
dp[i][j] = dp[i][j - 2];
}
else//如果匹配的是数字,情况就很简单
dp[i][j] = s[i - 1] == p[j - 1] ? dp[i - 1][j - 1] : false;
}
}
return dp[s.size()][p.size()];
}
};
股票交易
股票交易类问题通常可以用动态规划来解决。对于稍微复杂一些的股票交易类问题,比如需要冷却时间或者交易费用,则可以用通过动态规划实现的状态机来解决
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路:
我们可以遍历一遍数组,在每一个位置 i 时,记录 i 位置之前所有价格中的最低价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益即可。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int min1 = INT_MAX, max1 = INT_MIN;
for (int i = 0; i < prices.size(); i++) {
min1 = min(min1, prices[i]);
max1 = max(max1, prices[i] - min1);
}
return max1;
}
};
188. 买卖股票的最佳时机 IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路:
如果 k 大约总天数,那么我们一旦发现可以赚钱就进行买卖。如果 k 小于总天数,我们可以建立两个动态规划数组 buy 和 sell,对于每天的股票价格,buy[j] 表示在第 j 次买入时的最大收益,sell[j] 表示在第 j 次卖出时的最大收益。
buy[j] = max(buy[j], sell[j-1] - prices[i]);
sell[j] = max(sell[j], buy[j] + prices[i]);(要么一直什么也不做,要么接着上一次买卖比较)
代码:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (k >= prices.size()) {//交易笔数大于天数,那么bi’shu只要赚钱就交易
int sum = 0, min1 = INT_MAX;
for (int i = 0; i < prices.size(); i++) {
min1 = min(min1, prices[i]);
sum += prices[i] - min1;
min1 = prices[i];
}
return sum;
}
vector<int> buy(k + 1, INT_MIN), sale(k + 1, 0);//这样设置buy的初始值是要保证一定会买,不然一直都不买的话0就是最大收益了
for (int i = 0; i < prices.size(); i++) {
for (int j = 1; j <= k; j++) {//在第i天计算截止到当天时完成1-k笔交易的最大收益
buy[j] = max(buy[j], sale[j - 1] - prices[i]);//要么今天不买,要么今天买
sale[j] = max(sale[j], buy[j] + prices[i]);//要么今天不卖,要么今天卖
}
}
return sale[k];
}
};
309. 最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
思路:
我们可以使用状态机来解决这类复杂的状态转移问题,通过建立多个状态以及它们的转移方式,我们可以很容易地推导出各个状态的转移方程。如图所示,我们可以建立四个状态来表示带有冷却的股票交易,以及它们的之间的转移方式。
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
// f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
for (int i = 1; i < pricesSize; ++i) {
f[i][0] = fmax(f[i - 1][0], f[i - 1][2] - prices[i]);
f[i][1] = f[i - 1][0] + prices[i];
f[i][2] = fmax(f[i - 1][1], f[i - 1][2]);
}
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<int> n0(n + 1, 0), n1(n + 1, 0), y(n + 1, INT_MIN);//n0是无股票非冷冻期,n1是无股票在冷冻期,y是有股票,初始值也是保证一定会买
for (int i = 1; i <= n; i++) {//画出状态机即可得到如下方程
n0[i] = max(n0[i - 1], n1[i - 1]);
n1[i] = y[i - 1] + prices[i - 1];
y[i] = max(y[i-1], n0[i - 1] - prices[i - 1]);
}
return max(n0[n], n1[n]);
}
};
练习
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
思路:
和第 198 题的不同之处是,这道题中的房屋是首尾相连的,第一间房屋和最后一间房屋相邻,因此第一间房屋和最后一间房屋不能在同一晚上偷窃。
分别取 (start,end)=(0,n−2) 和 (start,end)=(1,n−1) 进行计算,取两个 dp[end] 中的最大值,即可得到最终结果。
代码:
class Solution {
public:
int rob1(int l,int r, vector<int> nums) {//注意不能对nums引用,否则影响下一次bu
int s = r - l + 1;
if (s == 2)
return fmax(nums[l], nums[l+1]);
if (s >= 3)
nums[l+2] = fmax(nums[l] + nums[l+2], nums[l+1]);
for (int i = 3 + l; i <= r; i++)
nums[i] = fmax(nums[i - 3] + nums[i], fmax(nums[i - 2] + nums[i], nums[i - 1]));
return nums[s - 1 + l];
}
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1)
return nums[0];
return max(rob1(0, n - 2, nums), rob1(1, n - 1, nums));
}
};
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
思路:
初始化为0
dp[i]=max(dp[i],dp[i-1]+v[i])(以i为结尾的最大和)
代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int max1 = nums[0];
for (int i = 1; i < nums.size(); i++)
max1 = max(nums[i] = max(nums[i], nums[i] + nums[i - 1]), max1);
return max1;
}
};
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
思路:
dp[i]为和为i时的最大乘积,dp[i]=max{max(1*(i-1),1*dp[i-1])…,max((i-1)*1,(i-1)*dp[1])}
代码:
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1, INT_MIN);
dp[0] = 0, dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i / 2; j++)
dp[i] = max(dp[i], max(dp[j] * (i - j), max((i - j) * j, dp[i - j] * j)));
}
return dp[n];
}
};
583. 两个字符串的删除操作
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: “sea”, “eat”
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
思路:
1143求最长公共子序列,剩下的个数就是最小操作步数
代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++)
dp[i][0] = i;
for (int i = 1; i <= n; i++)
dp[0][i] = i;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++)
dp[i][j] = word1[i - 1] == word2[j - 1] ? dp[i - 1][j - 1] : min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
return dp[m][n];
}
};
646. 最长数对链
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
思路:
1.贪心 类似435,无重叠区间
2.动态规划 类似300最长递增子序列的变种,根据数对的第一个数排序所有的数对,dp[i] 存储以 pairs[i] 结尾的最长链的长度。当 i < j 且 pairs[i][1] < pairs[j][0] 时,扩展数对链,更新 dp[j] = max(dp[j], dp[i] + 1)。
代码:
class Solution {
public:
static bool comp(vector<int>& a, vector<int>& b) {
return a[1] < b[1];
}
int findLongestChain(vector<vector<int>>& pairs) {
int cnt = 1, last;
sort(pairs.begin(), pairs.end(), comp);
last = pairs[0][1];
for (int i = 1; i < pairs.size(); i++) {
if (pairs[i][0] > last) {
last = pairs[i][1];
cnt++;
}
}
return cnt;
}
};
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
思路:
加状态
dp[i][0]:第i位置结束上升dp[i][0]
①如果nums[i] < nums[i - 1],分情况讨论,如果nums[i]<i之前的最小值,那么在这个位置上升的最长长度肯定只有1,否则的话就取代上一位的上升位置,即dp[i][0] = dp[i - 1][0],可能会出现漏洞,比如nums[i]是i之前第二小的数,不过因为最后是取的dp[i][0]和dp[i][1]的最大值,而这种情况下dp[i][1]肯定更大,所以为方便直接在这种情况下略去dp[i][0]的计算
②如果nums[i] < nums[i - 1],要么是接着上一个数序列+1,要么取代上一个数的上升位置,即dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + 1)
dp[i][1]:第i位置结束下降dp[i][1],同上
代码:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
vector<vector<int>> dp(nums.size(), vector<int>(2));
dp[0][0] = dp[0][1] = 1;
int min1 = nums[0], max1 = nums[0], max0 = 1;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1])
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + 1);
else if (nums[i] < nums[i - 1])
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + 1);
else {
dp[i][1] = dp[i-1][1];
dp[i][0] = dp[i-1][0];
}
max0 = max(max0, max(dp[i][1], dp[i][0]));
max1 = max(max1, nums[i]);
min1 = min(min1, nums[i]);
}
return max0;
}
};
494. 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
思路:
0-1背包问题
记数组的元素和为 sum,添加 -号的元素之和为 neg,则其余添加 +的元素之和为 sum−neg,得到的表达式的结果为
(sum−neg)−neg=sum−2⋅neg=target
这样只要找齐所有neg的可能就行了
代码:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0) - target, k;
if (sum % 2 || sum <0 )
return 0;
sum /= 2;
vector<int> dp(sum + 1);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = sum; j >= nums[i]; j--)
dp[j] += dp[j - nums[i]];//不选nums[i]和选nums[i]的方案数相加
}
return dp[sum];
}
};
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
思路:
类似188
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票的累计最大收益
for (int i = 1; i < pricesSize; ++i) {
f[i][0] = fmax(f[i - 1][0], f[i - 1][2] - prices[i]);
f[i][1] = fmax(f[i - 1][1], f[i - 1][0] + prices[i] - fee);
}
代码:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int max1 = 0;
vector<int> buy(prices.size() + 1, -60000), sale(prices.size() + 1, 0);
for (int i = 1; i <= prices.size(); i++) {
buy[i] = max(sale[i - 1] - prices[i - 1], buy[i - 1]);
sale[i] = max(buy[i - 1] + prices[i - 1] - fee, sale[i - 1]);
max1 = max(max1, max(buy[i], sale[i]));
}
return max1;
}
};