0%

leetcode刷题系列之动态规划

这篇文章是leetcode刷题系列的第6部分——动态规划。这里把有代表性的题目发出来,共计56道。动态规划题目变化多端,目前旨在于习得通用的解题技巧,这些只是比较经典的动态规划题目。

leetcode刷题系列其它文章组织如下:

1. 数组

2. 链表

3. 字符串

4. 二叉树

5. 队列和栈

6. 动态规划

7. 数据结构设计

8. 刷题小知识点

64. Minimum Path Sum

给定一个m × n的网格,其中填充了非负数,请找到从左上到右下的路径,这将沿其路径的所有数字的总和最小化。

注意:您只能在任何时间点向下或向右移动。

Constraints:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

Example:

img

1
2
3
Input: grid = [[1,3,1],[1,5,1],[4,2,1]]
Output: 7
Explanation: Because the path 1 → 3 → 1 → 1 → 1 minimizes the sum.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// dp[i][j] 表示从起点走到第 i 行, 第 j 列的最小路径和
// dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
int minPathSum(vector<vector<int>>& grid)
{
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
if(i == 1 && j == 1)
dp[i][j] = grid[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}
return dp[m][n];
}

// 这样写可避免循环内每次的判断语句, 效率提升那么一丢丢
// dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
int minPathSum(vector<vector<int>>& grid)
{
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
dp[0][1] = 0;
dp[1][0] = 0;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
return dp[m][n];
}
72. Edit Distance

Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2.

You have the following three operations permitted on a word:

  • Insert a character
  • Delete a character
  • Replace a character

Example 1:

1
2
3
4
5
6
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')

Example 2:

1
2
3
4
5
6
7
8
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

Constraints:

  • 0 <= word1.length, word2.length <= 500
  • word1 and word2 consist of lowercase English letters.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 一般来说, 解决字符串的动态规划问题, 是用两个指针 i, j 分别指向两个字符串的尾
// 一步一步向前走, 缩小问题的规模
// 如果我们定义一个 dp 函数 dp(i, j)
// 表示将 word1[0, i] 变为 word2[0, j] 的最小编辑距离的话
// 那么我们要求的就是 dp(word1.size() - 1, word2.size() - 1)
// 那么 dp(i, j) 怎么求呢?
// 考虑之前的状态, 共有 dp(i - 1, j), dp(i, j - 1), dp(i - 1, j - 1) 三种
// 在编辑 word1 的第 i + 1 个字符时
// 1. 如果 word1[i] == word2[j], 那么直接跳过即可, dp(i, j) = dp(i - 1, j - 1)
// 2. 如果 word1[i] != word2[j] 我们有三种选择
// - 插入一个字符使得其与 word2[j] 匹配, 那么 dp(i, j) = dp(i, j - 1) + 1
// - 删除这个字符, word2[j] 没有得到匹配, 那么 dp(i, j) = dp(i - 1, j) + 1
// - 替换这个字符使得其与 word2[j] 匹配, 那么 dp(i, j) = dp(i - 1, j - 1) + 1
// 要求最小编辑距离的话, 这三种都试一下, 取最小值

// 上面相当于暴力解法, 把每一个操作都试一遍取最小值
// 会出现非常多的重叠子问题, 因此需要用备忘录优化一下
unordered_map<string, int> memo;
int minDistance(string word1, string word2)
{
return dp(word1, word2, word1.size() - 1, word2.size() - 1);
}

int dp(string& word1, string& word2, int i, int j)
{
// base case
// 如果 word1 走到头了, word2 没有, 那么只能插入 word2 剩下的所有字符
if(i < 0) return j + 1;
// 如果 word2 走到头了, word1 没有, 那么只能删除 word1 剩下的所有字符
if(j < 0) return i + 1;

string key = to_string(i) + "," + to_string(j);
if(memo.count(key)) return memo[key];
if(word1[i] == word2[j])
memo[key] = dp(word1, word2, i - 1, j - 1);
else
memo[key] = 1 + min({dp(word1, word2, i - 1, j),
dp(word1, word2, i, j - 1),
dp(word1, word2, i - 1, j - 1)});
return memo[key];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 dp table
int minDistance(string word1, string word2)
{
int m = word1.size();
int n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
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++)
{
if(word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = 1 + min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]});
}
}
return dp[m][n];
};
300. Longest Increasing Subsequence

给你一个整数数组nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。

Example 1:

1
2
3
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Example 2:

1
2
Input: nums = [0,1,0,3,2,3]
Output: 4

Example 3:

1
2
Input: nums = [7,7,7,7,7,7,7]
Output: 1

Constraints:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

Follow up:

  • Could you come up with the O(n2) solution?
  • Could you improve it to O(n log(n)) time complexity?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 动态规划 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
// 要求的目标为 dp 数组中的最大值
// base case 以 nums[i] 结尾的最长递增子序列至少要包含它自己 dp[0..] = 1
// dp[i] = max(dp[i], dp[j] + 1) j 属于 [0..i) 且 nums[j] < nums[i]
// 就是在前面找到结尾的比 nums[i] 小的子序列, 把 nums[i] 接到后面, 长度加 1 即可
// 可能有很多个满足条件的接法, 选择长度最大的接法
int lengthOfLIS(vector<int>& nums)
{
int n = nums.size();
vector<int> dp(n, 1);
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(nums[j] < nums[i])
dp[i] = max(dp[i], dp[j] + 1);

int res = *max_element(dp.begin(), dp.end());
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 既然题目提示我们这题的时间复杂度可以优化到 O(nlogn)
// logn 的复杂度只有二分法能办到了
// 我们把这些数分成许多堆, 按照下面定义的规则:
// 1. 依次从数组中拿出一个数, 首先第一个数就放在第 1 个堆就行
// 2. 之后每次取出的数必须放在所有堆顶的不小于它的数上面
// 3. 如果有多个堆顶的数都不小于它, 就放在最靠左边的堆上面
// 4. 如果没有这样的堆, 就从右边新起一个堆放
// 这个规则的限制下, 所有堆顶的元素从左到右始终都是有序的
// 这样我们就可以应用二分搜索, 来找第 1 个不小于待放数的堆了
// 那么这和我们的问题: 寻找最长递增子序列有什么关系呢?
// 可以证明, 最长递增子序列就是堆的个数
int lengthOfLIS(vector<int>& nums)
{
int n = nums.size();
vector<int> pilesTop;
// 最多分了 n 个堆, 我们提前分配空间提升效率
pilesTop.reserve(n);
for(int i = 0; i < n; i++)
{
// 寻找左边界二分搜索
auto left = lower_bound(pilesTop.begin(), pilesTop.end(), nums[i]);
// 如果没有合适的堆, 自己单放
if(left == pilesTop.end())
pilesTop.push_back(nums[i]);
// 否则, 放堆顶上
else
*left = nums[i];

}
return pilesTop.size();
}
354. Russian Doll Envelopes

给你一个二维整数数组envelopes,其中envelopes[i] = [wi, hi],表示第i个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

Example 1:

1
2
3
Input: envelopes = [[5,4],[6,4],[6,7],[2,3]]
Output: 3
Explanation: The maximum number of envelopes you can Russian doll is 3 ([2,3] => [5,4] => [6,7]).

Example 2:

1
2
Input: envelopes = [[1,1],[1,1],[1,1]]
Output: 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 按照信封的宽度递增排序, 宽度相等的信封按高度递减排序
// 最大嵌套信封数就是高度序列的最长递增子序列
// 动态规划解法
int maxEnvelopes(vector<vector<int>>& envelopes)
{
auto cmp = [](auto& a, auto& b){ return a[0] == b[0] ? b[1] < a[1] : a[0] < b[0]; };
sort(envelopes.begin(), envelopes.end(), cmp);
vector<int> dp(envelopes.size(), 1);
for(int i = 1; i < envelopes.size(); i++)
for(int j = 0; j < i; j++)
if(envelopes[i][1] > envelopes[j][1])
dp[i] = max(dp[i], dp[j] + 1);
int res = *max_element(dp.begin(), dp.end());
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 同样的使用二分搜索解法提升效率
int maxEnvelopes(vector<vector<int>>& envelopes)
{
auto cmp = [](auto& a, auto& b){ return a[0] == b[0] ? b[1] < a[1] : a[0] < b[0]; };
sort(envelopes.begin(), envelopes.end(), cmp);
int n = envelopes.size();
vector<int> nums(n);
for(int i = 0; i < n; i++)
nums[i] = envelopes[i][1];

vector<int> piles;
piles.reserve(n);
for(int i = 0; i < n; i++)
{
auto it = lower_bound(piles.begin(), piles.end(), nums[i]);
if(it == piles.end())
piles.push_back(nums[i]);
else
*it = nums[i];
}

return piles.size();
}
494. Target Sum

给定一个非负整数数组,a1, a2, ..., an和一个目标数S。现在你有两个符号+-。对于数组中的任意一个整数,你都可以从+-中选择一个符号添加在前面。

返回可以使最终数组和为目标数S的所有添加符号的方法数。

Example:

1
2
3
4
5
6
7
8
9
10
11
Input: nums is [1, 1, 1, 1, 1], S is 3. 
Output: 5
Explanation:

-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

There are 5 ways to assign symbols to make the sum of nums be target 3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 这道题虽然通常想到的都是 dfs 的方法
// 现在让我们再转化一下思路, 看它和动态规划有什么关系
// 数组中的数都是非负数, 把所有数得和表示为 sums
// 我们把前面加正号的数的和 表示为 sumA, 把前面加负号的数的和 表示为 sumB
// 有 sumA - sumB = target
// 移项得 sumA = target + sumB
// 两边加上 sumA 有 sumA + sumA = target + sumA + sumB = target + sums
// 最后 sumA = (target + sums) / 2 = newTarget右边都是已知的
// 也就是说我们把问题转化为了
// 从这些数中选出一个子集, 问我们有多少种选法可使得子集的和等于目标数
// 换种表达方式, 我们把目标数表示为一个背包的载重量
// 数组中的值为一个个石头的重量, 问我们有多少种装法可以把背包装满
// 这是动态规划中典型的背包问题
// 首先在装石头的过程中, 问题中的状态和选择有: 状态就是背包的可载重量和可选择的石头序列
// 选择就是装还是不装
// 由于有两个状态, 所以我们定义一个二维 dp 数组
// dp[i][j] 表示在前 i 个石头中, 背包的可载重量为 j 时, 有多少种装法
// 那么当 i = 0 时, 没有石头, 装法为 0
// 当 j = 0 时, 背包的可载重量为 0 时, 只有 1 种装法, 就是一个也不装
// 那么状态转移方程怎么写呢?
// 对于可选择前 i 个石头, 可载重量为 j 时
// 1. 如果第 i 个石头不装, 那么 dp[i][j] = dp[i - 1][j]
// 2. 如果第 i 个石头装, 那么 dp[i][j] = dp[i - 1][j - nums[i - 1]]
// 它们都取决于上一次装的情况, 两种情况要加起来
// 所以 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]]
// 我们最终要求的目标就是 dp[n][m], 其中 n 表示给定数组的长度, m 表示新目标和
int findTargetSumWays(vector<int>& nums, int target)
{
// 问题转化
int sum = accumulate(nums.begin(), nums.end(), 0);
// 如果总和都小于目标和(表示全加正号)或者 sum + target 为奇数(表示 sumA 都不是个整数)
if(sum < target || ((sum + target) & 1))
return 0;
int newTarget = (sum + target) / 2;

// 定义 dp 数组
vector<vector<int>> dp(nums.size() + 1, vector<int>(newTarget + 1));
// base case
// 注意 dp[0][0] 也是 1
for(int i = 0; i <= nums.size(); i++)
dp[i][0] = 1;

for(int i = 1; i < dp.size(); i++)
{
for(int j = 0; j <= newTarget; j++)
{
if(j - nums[i - 1] < 0)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
}
}
return dp[nums.size()][newTarget];
}
91. Decode Ways639. Decode Ways II

一条包含字母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,请计算并返回解码方法的总数 。

Example 1:

1
2
3
Input: s = "12"
Output: 2
Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).

Example 2:

1
2
3
Input: s = "226"
Output: 3
Explanation: "226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

Example 3:

1
2
3
4
5
Input: s = "0"
Output: 0
Explanation: There is no character that is mapped to a number starting with 0.
The only valid mappings with 0 are 'J' -> "10" and 'T' -> "20", neither of which start with 0.
Hence, there are no valid ways to decode this since all digits need to be mapped.

Example 4:

1
2
3
Input: s = "06"
Output: 0
Explanation: "06" cannot be mapped to "F" because of the leading zero ("6" is different from "06").

Constraints:

  • 1 <= s.length <= 100
  • s contains only digits and may contain leading zero(s).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int numDecodings(string s)
{
int n = s.size();
vector<int> dp(n + 1);
// base case 前两个字符组成的串单放用
dp[0] = 1;
if(s[0] != '0') dp[1] = 1;
for(int i = 2; i <= n; i++)
{
if(dp[i - 1] == 0) return 0;
// 单放
int num = stoi(s[i - 1]);
if(num >= 1 && num <= 9)
dp[i] += dp[i - 1];
// 拼着放
num = stoi(s.substr(i - 2, 2));
if(num >= 10 && num <= 26)
dp[i] += dp[i - 2];
}
return dp[n];
}
516. Longest Palindromic Subsequence

给定一个字符串s,找到其中最长的回文子序列,并返回该序列的长度。可以假设s的最大长度为1000

Example 1:

1
2
3
Input: s = "bbbab"
Output: 4
Explanation: One possible longest palindromic subsequence is "bbbb".

Example 2:

1
2
3
Input: s = "cbbd"
Output: 2
Explanation: One possible longest palindromic subsequence is "bb".
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// dp[i][j] 表示子串 s[i..j] 中的最长回文子序列
// if s[i] == s[j]
// dp[i][j] = dp[i + 1][j - 1] + 2
// else
// dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
// 我们的目标是求 dp[0][n - 1]
// 显然 i == j 时 dp[i][j] = 1, i > j 时 dp[i][j] = 0
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
// base case
for(int i = 0; i < n; i++)
dp[i][i] = 1;

for(int i = n - 2; i >= 0; i--)
for(int j = i + 1; j < n; j++)
if(s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

return dp[0][n - 1];
}
1143. Longest Common Subsequence

给定两个字符串text1text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回0

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace""abcde"的子序列,但"aec"不是"abcde"的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列。

Example 1:

1
2
3
Input: text1 = "abcde", text2 = "ace" 
Output: 3
Explanation: The longest common subsequence is "ace" and its length is 3.

Example 2:

1
2
3
Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.

Example 3:

1
2
3
Input: text1 = "abc", text2 = "def"
Output: 0
Explanation: There is no such common subsequence, so the result is 0.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列长度
// if text1[i] == text1[j]
// dp[i][j] = 1 + dp[i - 1][j - 1]
// else
// dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
// 我们的目标是求 dp[m][n]
// 显然 i == 0 || j == 0 时 dp[i][j] = 0
int longestCommonSubsequence(string text1, string text2)
{
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// base case
// ...
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
if(text1[i - 1] == text2[j - 1])
dp[i][j] = 1 + dp[i - 1][j - 1];
else
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
return dp[m][n];
}
583. Delete Operation for Two Strings

Given two strings word1 and word2, return the minimum number of steps required to make word1 and word2 the same.

In one step, you can delete exactly one character in either string.

Example 1:

1
2
3
Input: word1 = "sea", word2 = "eat"
Output: 2
Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".

Example 2:

1
2
Input: word1 = "leetcode", word2 = "etco"
Output: 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 可以看出来, 一番删除操作之后剩下的字符串就是它们的最长公共子序列
// 所以就相当于问删除了多少字符后可以变成最长公共子序列
int minDistance(string word1, string word2)
{
int lcs = longestCommonSubsequence(word1, word2);
return word1.size() - lcs + word2.size() - lcs;
}

// dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列长度
// if text1[i] == text1[j]
// dp[i][j] = 1 + dp[i - 1][j - 1]
// else
// dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
// 我们的目标是求 dp[m][n]
// 显然 i == 0 || j == 0 时 dp[i][j] = 0
int longestCommonSubsequence(string text1, string text2)
{
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// base case
// ...
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
if(text1[i - 1] == text2[j - 1])
dp[i][j] = 1 + dp[i - 1][j - 1];
else
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
return dp[m][n];
}
712. Minimum ASCII Delete Sum for Two Strings

给定两个字符串s1, s2,找到使两个字符串相等所需删除字符的ASCII值的最小和。

Example 1:

1
2
3
4
5
Input: s1 = "sea", s2 = "eat"
Output: 231
Explanation: Deleting "s" from "sea" adds the ASCII value of "s" (115) to the sum.
Deleting "t" from "eat" adds 116 to the sum.
At the end, both strings are equal, and 115 + 116 = 231 is the minimum sum possible to achieve this.

Example 2:

1
2
3
4
5
6
Input: s1 = "delete", s2 = "leet"
Output: 403
Explanation: Deleting "dee" from "delete" to turn the string into "let",
adds 100[d]+101[e]+101[e] to the sum. Deleting "e" from "leet" adds 101[e] to the sum.
At the end, both strings are equal to "let", and the answer is 100+101+101+101 = 403.
If instead we turned both strings into "lee" or "eet", we would get answers of 433 or 417, which are higher.

Note:

0 < s1.length, s2.length <= 1000.

All elements of each string will have an ASCII value in [97, 122].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 和 LCS 的思路有些许相似之处
// dp[i][j] 表示把 s1[0..i-1] 和 s2[0..j-1] 变相同所删除字符的最小和
// if s1[i - 1] == s2[j - 1]
// dp[i][j] = dp[i - 1][j - 1]
// else
// dp[i][j] = min(dp[i - 1][j] + s1[i - 1], dp[i][j - 1] + s2[j - 1])
// 我们要求的目标是 dp[m][n]
// 显然 i == 0 时 dp[i][j] = sum(s2[0..j-1])
// j == 0 时 dp[i][j] = sum(s1[0..i-1])
int minimumDeleteSum(string s1, string s2)
{
int m = s1.size();
int n = s2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// base case
for(int i = 1; i <= m; i++)
dp[i][0] = dp[i - 1][0] + s1[i - 1];
for(int j = 1; j <= n; j++)
dp[0][j] = dp[0][j - 1] + s2[j - 1];

for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i][j - 1] + s2[j - 1], dp[i - 1][j] + s1[i - 1]);
return dp[m][n];
}
368. Largest Divisible Subset

给你一个由无重复正整数组成的集合nums,请你找出并返回其中最大的整除子集answer,子集中每一元素对(answer[i], answer[j])都应当满足:

  • answer[i] % answer[j] == 0,或answer[j] % answer[i] == 0

    如果存在多个有效解子集,返回其中任何一个均可。

Example 1:

1
2
3
Input: nums = [1,2,3]
Output: [1,2]
Explanation: [1,3] is also accepted.

Example 2:

1
2
Input: nums = [1,2,4,8]
Output: [1,2,4,8]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 这题要先把数组按照升序排列, 排完序之后就是求最长倍增子序列
// 动态规划 dp[i] 表示以 nums[i] 结尾的最长倍增子序列的长度
// 要求的目标为 dp 数组中的最大值
// base case 以 nums[i] 结尾的最长倍增子序列至少要包含它自己 dp[0..] = 1
// dp[i] = max(dp[i], dp[j] + 1) j 属于 [0..i) 且 nums[i] % nums[j] == 0
// 就是在前面找到可以被 nums[i] 整除的子序列, 把 nums[i] 接到后面, 长度加 1 即可
// 可能有很多个满足条件的接法, 选择长度最大的接法
// 但是由于这题不是让我们输出最长倍增子序列的个数, 而是把它们作为数组输出
// 所以 dp 数组里存的不止是最大个数, 还有它上一个数的索引
// 这样最后通过 dp 数组中的最大值, 找到前一个数的索引, 然后反推前面所有的数
vector<int> largestDivisibleSubset(vector<int>& nums)
{
int n = nums.size();
sort(nums.begin(), nums.end());
// {最长倍增子序列个数, 上一个数的索引}
vector<pair<int, int>> dp(n, {1, -1});
for(int i = 1; i < n; i++)
for(int j = 0; j < i; j++)
if(nums[i] % nums[j] == 0 && dp[i].first < dp[j].first + 1)
dp[i] = {dp[j].first + 1, j};

auto it = max_element(dp.begin(), dp.end());
vector<int> res;
res.push_back(nums[distance(dp.begin(), it)]);
int index = it->second;
while(index != -1)
{
res.push_back(nums[index]);
index = dp[index].second;
}
return res;
}
121. Best Time to Buy and Sell Stock

给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。

你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0

Example 1:

1
2
3
4
Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.

Example 2:

1
2
3
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transactions are done and the max profit = 0.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 直接贪心算法
// 要在价格最低点买, 价格最高点卖掉
// 从前往后遍历, 记录史最低点
// 今天卖掉的利润等于今天的价格减去历史最低点的价格
// 每一天都考虑是否卖掉, 如果今天卖掉比之前卖掉得到的利润大就卖掉
int maxProfit(vector<int>& prices)
{
int n = prices.size();
int minPrice = INT_MAX;
int maxProfit = 0;
for(int i = 0; i < n; i++)
// 如果今天的价格比历史最低点价格还低, 更新最低价格
if(prices[i] < minPrice)
minPrice = prices[i];
// 否则, 如果今天卖出的话比之前卖出得到的利润大, 就卖出
else if(prices[i] - minPrice > maxProfit)
maxProfit = prices[i] - minPrice;
return maxProfit;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 动态规划解法
// 状态有两个 [天数] 和 [是否持有股票]
// dp[i][0] 表示当前在第 i 天没有持有股票时获得的全部利润
// dp[i][1] 表示当前在第 i 天持有股票时获得的全部利润
// 如果在第 i 天没有持有股票, 有两种可能
// 1. 前一天持有股票, 当天将股票卖出了
// 2. 前一天没有持有股票, 当天也没买进
// dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0])
// 如果在第 i 天持有股票, 有两种可能
// 1. 前一天持有股票, 当天没有卖出
// 2. 前一天没有持有股票, 当天买进股票
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1])
// 但是这种情况下, 【由于只能交易一次】, 你当天才买进的话, 之前的利润肯定是 0, 即 dp[i - 1][0] = 0
// 所以, dp[i][1] = max(dp[i - 1][1], - prices[i - 1])
// 我们的目标是求 dp[n][0]
// 当 i == 0, dp[0][0] = 0, dp[0][1] = INT_MIN (表示不可能)
int maxProfit(vector<int>& prices)
{
int n = prices.size();
vector<vector<int>> dp(n + 1, vector<int>(2));
dp[0][1] = INT_MIN;
for(int i = 1; i <= n; i++)
{
dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][1], 0 - prices[i - 1]);
}
return dp[n][0];
}
122. Best Time to Buy and Sell Stock II

给定一个数组prices,其中prices[i]是一支给定股票第i天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

Example 1:

1
2
3
4
Input: prices = [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.

Example 2:

1
2
3
4
Input: prices = [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.

Example 3:

1
2
3
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e., max profit = 0.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 这题交易次数不限
// 状态有两个 [天数] 和 [是否持有股票]
// dp[i][0] 表示当前在第 i 天没有持有股票时获得的全部利润
// dp[i][1] 表示当前在第 i 天持有股票时获得的全部利润
// 如果在第 i 天没有持有股票, 有两种可能
// 1. 前一天持有股票, 当天将股票卖出了
// 2. 前一天没有持有股票, 当天也没买进
// dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0])
// 如果在第 i 天持有股票, 有两种可能
// 1. 前一天持有股票, 当天没有卖出
// 2. 前一天没有持有股票, 当天买进股票
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1])
// 我们的目标是求 dp[n][0]
// 当 i == 0 时, dp[0][0] = 0, dp[0][1] = INT_MIN (表示不可能)
int maxProfit(vector<int>& prices)
{
int n = prices.size();
vector<vector<int>> dp(n + 1, vector<int>(2));
dp[0][1] = INT_MIN;
for(int i = 1; i <= n; i++)
{
dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
}
return dp[n][0];
}
123. Best Time to Buy and Sell Stock III

给定一个数组,它的第i个元素是一支给定的股票在第i天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

Example 1:

1
2
3
4
Input: prices = [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.

Example 2:

1
2
3
4
Input: prices = [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.

Example 3:

1
2
3
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

Example 4:

1
2
Input: prices = [1]
Output: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这题就是下一题 k = 2 的情形
int maxProfit(vector<int>& prices)
{
int k = 2;
int n = prices.size();
vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(k + 1, vector<int>(2)));
for(int i = 1; i <= n; i++)
dp[i][0][1] = INT_MIN;
for(int j = 1; j <= k; j++)
dp[0][j][1] = INT_MIN;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= k; j++)
{
dp[i][j][0] = max(dp[i - 1][j][1] + prices[i - 1], dp[i - 1][j][0]);
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
}
}
return dp[n][k][0];
}
188. Best Time to Buy and Sell Stock IV

给定一个整数数组prices,它的第i个元素prices[i]是一支给定的股票在第i天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成k笔交易

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

Example 1:

1
2
3
Input: k = 2, prices = [2,4,1]
Output: 2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.

Example 2:

1
2
3
Input: k = 2, prices = [3,2,6,5,0,3]
Output: 7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4. Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 这题我们最多可以完成 k 笔交易
// 所以有三个状态 [天数], [交易次数] 和 [是否持有股票]
// dp[i][j][0] 表示第 i 天没有持有股票, 交易次数为 j 时获得的全部利润
// dp[i][j][1] 表示第 i 天持有股票, 交易次数为 j 时获得的全部利润
// 如果在第 i 天没有持有股票, 有两种可能
// 1. 前一天持有股票, 当天将股票卖出了
// 2. 前一天没有持有股票, 当天也没买进
// dp[i][j][0] = max(dp[i - 1][j][1] + prices[i - 1], dp[i - 1][j][0])
// 如果在第 i 天持有股票, 有两种可能
// 1. 前一天持有股票, 当天没有卖出
// 2. 前一天没有持有股票, 当天买进股票 (买进的时候交易次数加 1)
// dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1])
// 我们的目标是求 dp[n][k][0]
// 当 i == 0 时, dp[0][j][0] = 0, dp[0][j][1] = INT_MIN (表示不可能)
// 当 j == 0 时, dp[i][0][0] = 0, dp[i][0][1] = INT_MIN (表示不可能)
int maxProfit(int k, vector<int>& prices)
{
int n = prices.size();
vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(k + 1, vector<int>(2)));
for(int i = 1; i <= n; i++)
dp[i][0][1] = INT_MIN;
for(int j = 1; j <= k; j++)
dp[0][j][1] = INT_MIN;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= k; j++)
{
dp[i][j][0] = max(dp[i - 1][j][1] + prices[i - 1], dp[i - 1][j][0]);
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
}
}
return dp[n][k][0];
}
309. Best Time to Buy and Sell Stock with Cooldown

给定一个整数数组,其中第i个元素代表了第i天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为1天)。

Example 1:

1
2
3
Input: prices = [1,2,3,0,2]
Output: 3
Explanation: transactions = [buy, sell, cooldown, buy, sell]

Example 2:

1
2
Input: prices = [1]
Output: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 这题交易次数不限, 但是有冷冻期, 因此买进的时候需要看大前天的状态
// 状态有两个 [天数] 和 [是否持有股票]
// dp[i][0] 表示当前在第 i 天没有持有股票时获得的全部利润
// dp[i][1] 表示当前在第 i 天持有股票时获得的全部利润
// 如果在第 i 天没有持有股票, 有两种可能
// 1. 前一天持有股票, 当天将股票卖出了
// 2. 前一天没有持有股票, 当天也没买进
// dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0])
// 如果在第 i 天持有股票, 有两种可能
// 1. 前一天持有股票, 当天没有卖出
// 2. 只能大前天没有持有股票且没有卖出股票, 当天才能买进股票
// dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1])
// 我们的目标是求 dp[n][0]
// 当 i == 0 时, dp[0][0] = 0, dp[0][1] = INT_MIN (表示不可能)
// 当 i == 1 时, dp[1][0] = 0, dp[1][1] = -prices[0]
int maxProfit(vector<int>& prices)
{
int n = prices.size();
vector<vector<int>> dp(n + 1, vector<int>(2));
dp[0][1] = INT_MIN;
dp[1][1] = -prices[0];
// 第 1 天的情况已经知道了, 直接从第 2 天开始
for(int i = 2; i <= n; i++)
{
dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);
}
return dp[n][0];
}
714. Best Time to Buy and Sell Stock with Transaction Fee

给定一个数组prices,其中prices[i]是一支给定股票第i天的价格。非负整数fee表示交易一次股票需要支付的费用。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 这题交易次数不限
// 状态有两个 [天数] 和 [是否持有股票]
// dp[i][0] 表示当前在第 i 天没有持有股票时获得的全部利润
// dp[i][1] 表示当前在第 i 天持有股票时获得的全部利润
// 如果在第 i 天没有持有股票, 有两种可能
// 1. 前一天持有股票, 当天将股票卖出了
// 2. 前一天没有持有股票, 当天也没买进
// dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0])
// 如果在第 i 天持有股票, 有两种可能
// 1. 前一天持有股票, 当天没有卖出
// 2. 前一天没有持有股票, 当天买进股票
// 这里选择买进股票时支付手续费, 相当于买进价格升高了呗
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1] - fee)
// 我们的目标是求 dp[n][0]
// 当 i == 0 时, dp[0][0] = 0, dp[0][1] = INT_MIN (表示不可能)
int maxProfit(vector<int>& prices, int fee)
{
int n = prices.size();
vector<vector<int>> dp(n + 1, vector<int>(2));
dp[0][1] = INT_MIN;
for(int i = 1; i <= n; i++)
{
dp[i][0] = max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1] - fee);
}
return dp[n][0];
}
377. Combination Sum IV

给你一个由不同整数组成的数组nums,和一个目标整数target。请你从nums中找出并返回总和为target的元素组合的个数。

题目数据保证答案符合32位整数范围。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。(这特么就是排列呀!!!)

示例 2:

1
2
输入:nums = [9], target = 3
输出:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vector<int> memo;
int combinationSum4(vector<int>& nums, int target)
{
memo.resize(target + 1, -1);
return dfs(nums, target);
}

// 函数的定义为凑够和为 target 的组合数
int dfs(vector<int>& nums, int target)
{
if(target < 0) return 0;
if(target == 0) return 1;
if(memo[target] != -1) return memo[target];
int res = 0;
for(int num : nums)
res += dfs(nums, target - num);
memo[target] = res;
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 这题其实并不是组合问题, 而是排列问题
// 组合不关注元素顺序, 排列才关注元素顺序, 而这题关注元素的顺序!
// 状态有 [排列和 target]
// dp[i] 表示组合的和等于 i 时的不同组合个数
// 我们的目标是求 dp[target]
// 如果我们考虑将 nums[j] 加入组合中去, 并且放在最后的位置
// [[也就是关注排列中的最后一个数是多少]]
// 那么此时 dp[i] 就等于所有的 dp[i - nums[j]] (j = 0, ...) 之和
// dp[0] = 1 表示 1 种空组合
int combinationSum4(vector<int>& nums, int target)
{
vector<int> dp(target + 1);
dp[0] = 1;
for(int i = 0; i <= target; i++)
for(int num : nums)
// dp[i] + dp[i - num] 有可能溢出, 这里为了清晰起见没有考虑
if(i - num >= 0)
dp[i] += dp[i - num];
return dp[target];
}

// 这题还可以将其看作一个爬楼梯问题
// 楼梯的阶数一共为 target, 一次可以走的步数为 nums[i]
// 问你一共有多少种走法 (相当于登上最高阶所走步数的所有排列)
// [[只要关注最后一步走多少阶数就容易写出状态转移方程了]]

// 还可以看作是完全背包问题
// 背包的容量为 target, 有物品 nums[i] 可以无限选取
// 并且相同的物品选择, 物品选取的顺序不同算是不同的选择
// 问你把背包装满有多少种方法
// 这题为了考虑不同的顺序, 因此外循环遍历背包的容量, 内循环遍历物品
// 对于下题 [换零钱 2] 由于顺序不同也算一种选择, 就要外循环遍历物品, 内循环遍历背包的容量
322. Coin Change

给定不同面额的硬币coins和一个总金额amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1

你可以认为每种硬币的数量是无限的。

Constraints:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 10^4

Example 1:

1
2
3
Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Example 2:

1
2
Input: coins = [2], amount = 3
Output: -1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 状态只有 金额数
// 选择有若干枚硬币
// 定义 dp[i] 表示凑够金额 i 元的最少硬币数
// dp[i] = min(dp[i - k] + 1) k 为每种硬币的面值
// 目标是求 dp[amount]
// 显然 dp[0] = 0 就是不用任何面值的硬币就凑够了
int coinChange(vector<int>& coins, int amount)
{
// 数组中每个值初始化为 amount + 1 表示不可能
// 因为只使用 1 元硬币最多也就需要 amount 个硬币
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for(int i = 1; i <= amount; i++)
for(int coin : coins)
if(i >= coin)
dp[i] = min(dp[i], dp[i - coin] + 1);
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 也可以看成是一个完全背包问题
// 状态有 [使用的硬币] 和 [凑成的总金额]
// 定义 dp[i][j] 表示只使用前 i 个硬币凑成金额为 j 时的最少硬币数
// 对于第 i 枚硬币有使用和不使用两种选择
// 如果不使用
// dp[i][j] = dp[i - 1][j] 继承之前的硬币数
// 如果使用
// dp[i][j] = dp[i][j - coins[i - 1]] + 1
// 表示只使用前 i 种硬币凑够 j - coins[i - 1] 的硬币数, 再加上使用的这枚硬币
// 因为要求最少的硬币数, 这两种选择的结果取小值
// 所以 dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
// 显然 j == 0 时 dp[i][0] = 0 就是不凑就够了, 数量为 0
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
// 都初始化为 amount + 1 表示不可能的结果, min 时会排除掉
vector<vector<int>> dp(n + 1, vector<int>(amount + 1, amount + 1));
for(int i = 0; i <= n; i++)
dp[i][0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= amount; j++)
{
if(j >= coins[i - 1])
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n][amount] == amount + 1 ? -1 : dp[n][amount];
}
518. Coin Change 2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

Constraints:

  • 1 <= coins.size() <= 300
  • 1 <= coins[i] <= 5000
  • 0 <= amount <= 5000

Example 1:

1
2
3
4
5
6
7
Input: amount = 5, coins = [1,2,5]
Output: 4
Explanation: there are four ways to make up the amount:
5 = 5
5 = 2 + 2 + 1
5 = 2 + 1 + 1 + 1
5 = 1 + 1 + 1 + 1 + 1

Example 2:

1
2
3
Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 完全背包问题
// 状态有 [使用的硬币] 和 [凑成的总金额]
// 定义 dp[i][j] 表示只使用前 i 个硬币凑成金额为 j 时的组合数
// 对于第 i 枚硬币有使用和不使用两种选择
// 如果不使用
// dp[i][j] = dp[i - 1][j] 继承之前的组合数
// 如果使用
// dp[i][j] = dp[i][j - coins[i - 1]] 表示使用前 i 种硬币凑够 j - coins[i - 1] 的组合数
// 因为要求总的组合数, 这两种选择的结果相加
// 所以 dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
// 显然 i == 0 时 dp[0][j] = 0 不使用任何硬币
// j == 0 时 dp[i][0] = 1 就是不凑就够了
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
for(int i = 0; i <= n; i++)
dp[i][0] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= amount; j++)
{
if(j >= coins[i - 1])
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n][amount];
}
474. Ones and Zeroes

给你一个二进制字符串数组strs和两个整数mn

请你找出并返回strs的最大子集的大小,该子集中最多有m0n1

如果x的所有元素也是y的元素,集合x是集合y的子集 。

Example 1:

1
2
3
4
5
Input: strs = ["10","0001","111001","1","0"], m = 5, n = 3
Output: 4
Explanation: The largest subset with at most 5 0's and 3 1's is {"10", "0001", "1", "0"}, so the answer is 4.
Other valid but smaller subsets include {"0001", "1"} and {"10", "1", "0"}.
{"111001"} is an invalid subset because it contains 4 1's, greater than the maximum of 3.

Example 2:

1
2
3
Input: strs = ["10","0","1"], m = 1, n = 1
Output: 2
Explanation: The largest subset is {"0", "1"}, so the answer is 2.

Constraints:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] consists only of digits '0' and '1'.
  • 1 <= m, n <= 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 01背包问题
// 这里有两个背包, 一个装 0, 一个装 1
// 每个物品(串)同时消耗两个背包的容量, 每个物品的价值为 1
// 状态有 [可选择的物品], [背包 0 的容量] 和 [背包 1 的容量]
// 定义 dp[i][j][k] 表示只装前 i 个物品, 背包 0 和 1 的容量分别为 j 和 k 时所能装的最大价值
// 对于第 i 个物品 strs[i - 1] 你的选择就是装与不装
// 1. 不装
// dp[i][j][k] = dp[i - 1][j][k]
// 2. 装
// dp[i][j][k] = dp[i - 1][j - cost_0(strs[i - 1])][k - cost_1(strs[i - 1])]
// 因为要求的是最大价值, 那就比较两种选择孰大孰小
// dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - cost_0(strs[i - 1])][k - cost_1(strs[i - 1])])
// 显然当 i == 0 时, dp[0][j][k] = 0, 没有物品可以装, 价值只能是 0
// j == 0 || k == 0 时, dp[0][j][k] = 0, 背包没有容量了价值也是 0
// 我们的目标是求 dp[sz][m][n]
int findMaxForm(vector<string>& strs, int m, int n)
{
int sz = strs.size();
vector<vector<vector<int>>> dp(sz + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));
for(int i = 1; i <= sz; i++)
{
auto [zeros, ones] = count_01(strs[i - 1]);
// 注意 j 和 k 的起始索引, 有可能物品只消耗其中一种背包的容量
for(int j = 0; j <= m; j++)
{
for(int k = 0; k <= n; k++)
{
if(j < zeros || k < ones)
dp[i][j][k] = dp[i - 1][j][k];
else
dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - zeros][k - ones] + 1);
}
}
}
return dp[sz][m][n];
}

pair<int, int> count_01(const string& str)
{
int cnt = 0, n = str.size();
for(char c : str) if(c == '0') cnt++;
return {cnt, n - cnt};
}
139. Word Break

给定一个非空字符串s和一个包含非空单词的列表wordDict,判定s是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

  • 拆分时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。

Example 1:

1
2
3
Input: s = "leetcode", wordDict = ["leet","code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".

Example 2:

1
2
3
4
Input: s = "applepenapple", wordDict = ["apple","pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
Note that you are allowed to reuse a dictionary word.

Example 3:

1
2
Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
Output: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 和背包问题有点类似, 但有不同之处
// 状态有 [可选择的字符]
// 定义 dp[i] 表示对于前 i 个字符是否有满足条件的划分
// 和背包之处不同在于, 对于第 i 个字符 s[i - 1] 只有一个选择, 就是必须选择
// 这时我们需要将前 i 个字符用索引 j 划分为两部分
// 对于前半部分 s[0..j-1] 可以利用已经算出来的 dp[j]
// 后半部分 s[j..i-1] 需要单独判断是否在单词集合中
// 所以 dp[i] = dp[i] || (dp[j] && (s[j..i-1] in wordDict)) j 从 0 到 i - 1
// 我们的目标是求 dp[n]
bool wordBreak(string s, vector<string>& wordDict)
{
unordered_set<string> setting;
setting.insert(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n + 1);
// 为了需要, 我们定义空串是有效的
dp[0] = true;
for(int i = 1; i <= n; i++)
for(int j = 0; j < i; j++)
dp[i] = dp[i] || (dp[j] && setting.count(s.substr(j, i - j)));

return dp[n];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 记忆化递归
unordered_map<string, bool> memo;
bool wordBreak(string s, vector<string>& wordDict)
{
unordered_set<string> setting;
setting.insert(wordDict.begin(), wordDict.end());
return dfs(s, setting);
}

bool dfs(string s, unordered_set<string>& setting)
{
if(s.empty()) return true;
if(memo.count(s)) return memo[s];
int n = s.size();
for(int i = 1; i <= n; i++)
if(setting.count(s.substr(0, i)) && dfs(s.substr(i), setting))
return memo[s] = true;
return memo[s] = false;
}
140. Word Break II

给定一个非空字符串s和一个包含非空单词列表的字典wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。

说明:

  • 分隔时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。
  • 1 <= s.size() <= 20

Example 1:

1
2
Input: s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
Output: ["cats and dog","cat sand dog"]

Example 2:

1
2
3
Input: s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"]
Output: ["pine apple pen apple","pineapple pen apple","pine applepen apple"]
Explanation: Note that you are allowed to reuse a dictionary word.

Example 3:

1
2
Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
Output: []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 回溯
vector<string> sentences;
vector<string> wordBreak(string s, vector<string>& wordDict)
{
unordered_set<string> setting;
setting.insert(wordDict.begin(), wordDict.end());
dfs(s, "", setting);
return sentences;
}

void dfs(string s, string sentence, unordered_set<string>& setting)
{
if(s.empty())
{
sentence.pop_back();
sentences.push_back(sentence);
return;
}
int n = s.size();
for(int i = 1; i <= n; i++)
{
if(!setting.count(s.substr(0, i)))
continue;
dfs(s.substr(i), sentence + s.substr(0, i) + " ", setting);
}
}
416. Partition Equal Subset Sum

Given a non-empty array num containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Example 1:

1
2
3
Input: nums = [1,5,11,5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

1
2
3
Input: nums = [1,2,3,5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

Constraints:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 转化为背包问题进行求解
// 我们把数组分成两部分
// 1. sumA = sumB
// 2. sumA + sumB = sum
// 所以有 sumA = sum / 2
// 也就是说现在有一个背包的容量恰好是 sum / 2
// 问我们是否有一种装法恰好装满背包
// 背包问题的状态一般就两个 [可选择的物品] 和 [背包的容量]
// 所以我们定义 dp[i][j] 表示使用前 i 个物品, 背包容量为 j 时是否有一种装法给装满
// 对于第 i 个物品 nums[i - 1] 可以选择装进去和不装进去
// 1. 不装 就要看前 i - 1 个物品能不能装满容量为 j 的背包
// dp[i][j] = dp[i - 1][j]
// 2. 装 就要看前 i - 1 个物品能不能装满容量为 j - nums[i - 1] 的背包
// dp[i][j] = dp[i - 1][j - nums[i - 1]]
// 这两种任意一种选择为真的话, 就为真
// 所以 dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]
// 我们的目标是求 dp[n][sum/2]
// 显然 i == 0 时, dp[0][j] = false, 没有物品装肯定装不满
// j == 0 时, dp[i][0] = true, 容量为 0 了就是装满了啊
bool canPartition(vector<int>& nums)
{
int n = nums.size();
int sum = accumulate(nums.begin(), nums.end(), 0);
// 如果 sum 为奇数, 显然不可能
if(sum & 1) return false;
vector<vector<bool>> dp(n + 1, vector<bool>(sum/2 + 1));
for(int i = 0; i <= n; i++)
dp[i][0] = true;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= sum/2; j++)
{
if(j < nums[i - 1])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
return dp[n][sum/2];
}
698. Partition to K Equal Sum Subsets

给定一个整数数组nums和一个正整数k,找出是否有可能把这个数组分成k个非空子集,其总和都相等。

Example 1:

1
2
3
Input: nums = [4,3,2,3,5,2,1], k = 4
Output: true
Explanation: It's possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.

Example 2:

1
2
Input: nums = [1,2,3,4], k = 3
Output: false

Constraints:

  • 1 <= k <= nums.length <= 16
  • 0 <= nums[i] <= 104
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 其实上题 416(k = 2) 以及 473(k = 4) 都是这题的特例
// 这题很容易超时, 为了尽可能地剪枝我额外做的工作
// 1. 降序排序, 到大于的时候直接 continue
// 2. 去重,相邻元素必须从左往右被使用
// 3. 索引从 start 开始
int targetSum;
vector<bool> used;
bool canPartitionKSubsets(vector<int>& nums, int k)
{
int total = accumulate(nums.begin(), nums.end(), 0);
if(total % k > 0) return false;
used.resize(nums.size());
targetSum = total / k;
sort(nums.begin(), nums.end(), greater<int>());
return canPartitionKSubsets(nums, k, 0, 0);
}

bool canPartitionKSubsets(vector<int>& nums, int k, int start, int curSum)
{
if(k == 0) return true;
if(curSum == targetSum) return canPartitionKSubsets(nums, k - 1, 0, 0);
for(int i = start; i < nums.size(); i++)
{
// 剪枝 1
if(curSum + nums[i] > targetSum)
continue;
// 剪枝 2
if(used[i] || (i > 0 && nums[i - 1] == nums[i] && !used[i - 1]))
continue;
used[i] = true;
if(canPartitionKSubsets(nums, k, start + 1, curSum + nums[i])) return true;
used[i] = false;
}
return false;
}
55. Jump Game

给定一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

Example 1:

1
2
3
Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2:

1
2
3
Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

Constraints:

  • 1 <= nums.length <= 3 * 104
  • 0 <= nums[i] <= 105
1
2
3
4
5
6
7
8
9
10
11
12
// 也就是问你最多能跳多远
bool canJump(vector<int>& nums)
{
int n = nums.size();
int farthest = 0;
for(int i = 0; i < n - 1; i++)
{
farthest = max(farthest, i + nums[i]);
if(i == farthest) return false;
}
return true;
}
45. Jump Game II

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。

Example 1:

1
2
3
Input: nums = [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2. Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2:

1
2
Input: nums = [2,3,0,1,4]
Output: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 动态规划
// 定义 dp[i] 表示从索引 i 开始跳, 跳到最后需要跳的最少次数
// 显然 dp[n - 1] = 0, 我们要求的是 dp[0]
// dp[i] = 1 + min(dp[j]) j = i .. (i + nums[i])
int jump(vector<int>& nums)
{
int n = nums.size();
vector<int> dp(n, n);
dp[n - 1] = 0;
for(int i = n - 2; i >= 0; i--)
for(int j = 1; j <= nums[i] && i + j < n; j++)
dp[i] = min(dp[i], 1 + dp[i + j]);
return dp[0];
}
索引 2 的覆盖范围最远 就以其最远索引作为下次起跳的位置
image-20210428181037785 image-20210428181149058
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 贪心, 优先跳到下次能跳最远的索引
int jump(vector<int>& nums)
{
int n = nums.size();
int farthest = 0, end = 0;
int jumps = 0;
for(int i = 0; i < n - 1; i++)
{
farthest = max(farthest, i + nums[i]);
if(i == end)
{
jumps++;
end = farthest;
}
}
return jumps;
}
1306. Jump Game III

这里有一个非负整数数组arr,你最开始位于该数组的起始下标start处。当你位于下标i处时,你可以跳到i + arr[i]或者i - arr[i]。请你判断自己是否能够跳到对应元素值为0的任一下标处。注意,不管是什么情况下,你都无法跳到数组之外。

Example 1:

1
2
3
4
5
6
Input: arr = [4,2,3,0,3,1,2], start = 5
Output: true
Explanation:
All possible ways to reach at index 3 with value 0 are:
index 5 -> index 4 -> index 1 -> index 3
index 5 -> index 6 -> index 4 -> index 1 -> index 3

Example 2:

1
2
3
4
5
Input: arr = [4,2,3,0,3,1,2], start = 0
Output: true
Explanation:
One possible way to reach at index 3 with value 0 is:
index 0 -> index 4 -> index 1 -> index 3

Example 3:

1
2
3
Input: arr = [3,0,2,1,2], start = 2
Output: false
Explanation: There is no way to reach at index 1 with value 0.

Constraints:

  • 0 <= start < arr.length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bfs
bool canReach(vector<int>& arr, int start)
{
int n = arr.size();
vector<bool> visited(n);
queue<int> q;
q.push(start);
while(!q.empty())
{
int pos = q.front(); q.pop();
if(visited[pos]) continue;
visited[pos] = true;
if(arr[pos] == 0) return true;
if(pos + arr[pos] < n)
q.push(pos + arr[pos]);
if(pos - arr[pos] >= 0)
q.push(pos - arr[pos]);
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// dfs
bool canReach(vector<int>& arr, int start)
{
vector<bool> visited(arr.size());
return canReach(arr, start, visited);
}

bool canReach(vector<int>& arr, int start, vector<bool>& visited)
{
if(start < 0 || start >= arr.size() || visited[start])
return false;
if(arr[start] == 0) return true;
visited[start] = true;
if(canReach(arr, start - arr[start], visited))
return true;
if(canReach(arr, start + arr[start], visited))
return true;
visited[start] = false;
return false;
}
1345. Jump Game IV

给你一个整数数组arr,你一开始在数组的第一个元素处(下标为0)。每一步,你可以从下标i跳到下标:

  • i + 1满足:i + 1 < arr.length

  • i - 1满足:i - 1 >= 0

  • j满足:arr[i] == arr[j]i != j

    请你返回到达数组最后一个元素的下标处所需的最少操作次数

    注意:任何时候你都不能跳到数组外面。

Example 1:

1
2
3
Input: arr = [100,-23,-23,404,100,23,23,23,3,404]
Output: 3
Explanation: You need three jumps from index 0 --> 4 --> 3 --> 9. Note that index 9 is the last index of the array.

Example 2:

1
2
3
Input: arr = [7]
Output: 0
Explanation: Start index is the last index. You don't need to jump.

Example 3:

1
2
3
Input: arr = [7,6,9,6,9,6,9,7]
Output: 1
Explanation: You can jump directly from index 0 to index 7 which is last index of the array.

Example 4:

1
2
Input: arr = [6,1,9]
Output: 2

Example 5:

1
2
Input: arr = [11,22,7,7,7,7,7,7,7,22,13]
Output: 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 搜索最短路径显然用 bfs
int minJumps(vector<int>& arr)
{
int n = arr.size();
vector<bool> visited(n);
unordered_map<int, vector<int>> canJumpTo;
for(int i = 0; i < n; i++)
canJumpTo[arr[i]].push_back(i);

queue<int> q;
q.push(0);
int step = 0;
while(!q.empty())
{
int sz = q.size();
while(sz-- > 0)
{
int pos = q.front(); q.pop();
if(pos == n - 1) return step;
visited[pos] = true;
if(pos - 1 >= 0 && !visited[pos - 1])
q.push(pos - 1);

if(pos + 1 < n && !visited[pos + 1])
q.push(pos + 1);

for(auto jumpTo : canJumpTo[arr[pos]])
if(!visited[jumpTo])
q.push(jumpTo);
// 同一高度的都已经放入队列等待访问了, 下次就不用再放一次了
// 必须清空, 否则会 TLE
canJumpTo[arr[pos]] = {};
}
step++;
}
return 0;
}

// 这题遇到一个新情况, 把 unordered_map<int, vector<int>> canJumpTo;
// 里的 vector<int> 换成 unordered_set<int>
// 把清空语句 canJumpTo[arr[pos]] = {}; 变成
// canJumpTo[arr[pos]].clear(); 会超时, 但用 swap 函数就不会
// unordered_set<int> temp; canJumpTo[arr[pos]]swap(temp);
// 奇了怪了!
403. Frog Jump

一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。

给你石子的位置列表stones(用单元格序号升序表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。

开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格1跳至单元格2)。

如果青蛙上一步跳跃了k个单位,那么它接下来的跳跃距离只能选择为k - 1kk + 1个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。

Example 1:

1
2
3
Input: stones = [0,1,3,5,6,8,12,17]
Output: true
Explanation: The frog can jump to the last stone by jumping 1 unit to the 2nd stone, then 2 units to the 3rd stone, then 2 units to the 4th stone, then 3 units to the 6th stone, 4 units to the 7th stone, and 5 units to the 8th stone.

Example 2:

1
2
3
Input: stones = [0,1,2,3,4,8,9,11]
Output: false
Explanation: There is no way to jump to the last stone as the gap between the 5th and 6th stone is too large.

Constraints:

  • 2 <= stones.length <= 2000
  • 0 <= stones[i] <= 231 - 1
  • stones[0] == 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 记忆化搜索
unordered_map<string, bool> memo;
bool canCross(vector<int>& stones)
{
return dfs(stones, 0, 0);
}

bool dfs(vector<int> &stones, int pos, int lastJump)
{
if(pos + 1 == stones.size()) return true;
string key = to_string(pos) + "," + to_string(lastJump);
if(memo.count(key)) return memo[key];

for(int i = pos + 1; i < stones.size(); i++)
{
// k 是下一次必须跳的步数
int k = stones[i] - stones[pos];
if(k < lastJump - 1) continue;
if(k > lastJump + 1)
return memo[key] = false;
if(dfs(stones, i, k))
return memo[key] = true;
}
return memo[key] = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 动态规划
// dp[i][j] 表示是否可以用 j 步 跳到 第 i 块石头上
bool canCross(vector<int>& stones)
{
int n = stones.size();
vector<vector<bool>> dp(n, vector<bool>(n));
dp[0][0] = true;
for(int i = 1; i < n; i++)
{
for(int j = i - 1; j >= 0; j--)
{
int k = stones[i] - stones[j];
// k 是每次跳的步数, 每跳 1 次下次最多加 1 步
// 但是升序数组的索引 j 每次至少会加 1 所以 k <= j
// 在第 j 块石头时,再次起跳距离最大为 j + 1
// 如果和后一块石头的距离大于最大起跳距离, 就不可能跳过去
if(k > j + 1) break;

dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1];
if(dp[n - 1][k]) return true;
}
}
return false;
}
10. Regular Expression Matching

给你一个字符串s和一个模式串p,请你来实现一个支持'.''*'的正则表达式匹配。

  • '.'匹配任意单个字符

  • '*'匹配零个或多个前面的那一个元素

    所谓匹配,是要涵盖整个字符串s的,而不是部分字符串。

Example 1:

1
2
3
Input: s = "aa", p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".

Example 2:

1
2
3
Input: s = "aa", p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:

1
2
3
Input: s = "ab", p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".

Example 4:

1
2
3
Input: s = "aab", p = "c*a*b"
Output: true
Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore, it matches "aab".

Example 5:

1
2
Input: s = "mississippi", p = "mis*is*p*."
Output: false

Constraints:

  • 0 <= s.length <= 20
  • 0 <= p.length <= 30
  • s contains only lowercase English letters.
  • p contains only lowercase English letters, '.', and '*'.
  • It is guaranteed for each appearance of the character '*', there will be a previous valid character to match.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 动态规划
// dp[i][j] 表示 s 的前 i 个字符是否和 p 的前 j 个字符相匹配
// 显然我们的目标是求 dp[n][m]
// 1. 当 p 中的第 j 个字符 p[j - 1] != '*' 时
// p[j - 1] 只能选择和 s[i - 1] 进行匹配
// 所以 dp[i][j] = dp[i - 1][j - 1] if s[i - 1] == p[j - 1] or p[j - 1] == '.'
// dp[i][j] = false if s[i - 1] != p[j - 1] and p[j - 1] != '.'
// 2. 当 p 中的第 j 个字符 p[j - 1] == '*' 时
// p[j - 2] 和 '*' 就组合在一起看
// 2.1 匹配 0 次
// dp[i][j] = dp[i][j - 2] 不管 p[j - 2] 是否匹配 s[i - 1]
// 2.1 匹配多次
// dp[i][j] = dp[i - 1][j] if s[i - 1] == p[j - 2] or p[j - 2] == '.'
// 两个空串默认可以匹配 dp[0][0] = true
// 但是这题要特别注意的是, 当 s 为空串时, p 为空串的情况可以是
// "", "c*", "c*c*", ... 它们都应该为 true
bool isMatch(string s, string p)
{
int n = s.size(), m = p.size();
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1));
dp[0][0] = true;
// 特别处理 p 为 c*c*... 的情况
for(int j = 1; j <= m; j++)
if(p[j - 1] == '*')
dp[0][j] = dp[0][j - 2];

for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
// 1. 当 p 中的第 j 个字符 p[j - 1] != '*' 时
if(p[j - 1] != '*')
{
if(s[i - 1] == p[j - 1] || p[j - 1] == '.')
dp[i][j] = dp[i - 1][j - 1];
// else
// dp[i][j] = false;
}
// 2. 当 p 中的第 j 个字符 p[j - 1] == '*' 时
else
{
// 2.1 匹配 0 次
dp[i][j] = dp[i][j - 2];
// 2.1 匹配多次
if(s[i - 1] == p[j - 2] || p[j - 2] == '.')
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
return dp[n][m];
}
312. Burst Balloons

n个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组nums中。

现在要求你戳破所有的气球。戳破第i个气球,你可以获得nums[i - 1] * nums[i] * nums[i + 1]枚硬币。这里的i - 1i + 1代表和i相邻的两个气球的序号。如果i - 1i + 1超出了数组的边界,那么就当它是一个数字为1的气球。

求所能获得硬币的最大数量

Example 1:

1
2
3
4
5
Input: nums = [3,1,5,8]
Output: 167
Explanation:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

Example 2:

1
2
Input: nums = [1,5]
Output: 10

Constraints:

  • n == nums.length
  • 1 <= n <= 500
  • 0 <= nums[i] <= 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 动态规划
// 定义 dp[i][j] 表示将第 i 个气球和第 j 个气球(左右开区间)之间的气球戳破最多得到的硬币数
// 显然我们是要求 dp[0][n+1]
// 对于第 i 个气球和第 j 个气球之间的所有气球, 我们考虑最后戳破的那一个气球 k
// 于是可得 dp[i][j] = dp[i][k] + dp[k][j] + nums[i - 1] * nums[k - 1] * nums[j - 1]
// 因为我们要求最大得到的硬币数量, 就对每一个 k 分别计算, 取最大呗
// 当 (i, j) 区间内没有气球时得分显然是 0, 即当 i >= j 时 dp[i][j] = 0
int maxCoins(vector<int>& nums)
{
int n = nums.size();
// 考虑到边界条件, 在最左最右各放两个虚拟气球
nums.insert(nums.begin(), 1);
nums.insert(nums.end(), 1);
vector<vector<int>> dp(n + 2, vector<int>(n + 2));
for(int i = n; i >= 0; i--)
for(int j = i + 1; j < n + 2; j++)
for(int k = i + 1; k < j; k++)
dp[i][j] = max(dp[i][j],
dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]);
return dp[0][n+1];
}
887. Super Egg Drop

给你k枚相同的鸡蛋,并可以使用一栋从第1层到第n层共有n层楼的建筑。

已知存在楼层f,满足0 <= f <= n,任何从高于f的楼层落下的鸡蛋都会碎,从f楼层或比它低的楼层落下的鸡蛋都不会破。

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层x扔下(满足1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中重复使用这枚鸡蛋。

请你计算并返回要确定f确切的值的最坏情况下的最小尝试次数

Example 1:

1
2
3
4
5
6
7
Input: k = 1, n = 2
Output: 2
Explanation:
Drop the egg from floor 1. If it breaks, we know that f = 0.
Otherwise, drop the egg from floor 2. If it breaks, we know that f = 1.
If it does not break, then we know f = 2.
Hence, we need at minimum 2 moves to determine with certainty what the value of f is.

Example 2:

1
2
Input: k = 2, n = 6
Output: 3

Example 3:

1
2
Input: k = 3, n = 14
Output: 4

Constraints:

  • 1 <= k <= 100
  • 1 <= n <= 104
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 记忆化递归 TLE!
// 定义 dp(k, n) 表示有 k 个鸡蛋和 n 层楼时的最坏情况下的最少尝试次数
// 对于从第 1 层到第 n 层中间的某一层 i, 我们扔完之后有两种情况
// 1. 鸡蛋没碎
// 这时我们要寻找的楼层 f 显然在第 i + 1 层到第 n 层之间的共 n - i 层楼
// 于是问题规模缩小至 dp(k, n - i)
// 2. 鸡蛋碎了
// 这时我们要寻找的楼层 f 显然在第 1 层到第 i - 1 层之间的共 i - 1 层楼
// 于是问题规模缩小至 dp(k - 1, i - 1)
// 因为我们要求最坏情况下的最少尝试次数, 什么叫最坏情况下呢?
// 就是说我们并不知道扔完之后鸡蛋碎不碎, 我们要考虑碎或者不碎时的最多尝试次数
// 什么是最少尝试次数呢?就是说我们从第 1 层, 第 2 层...第 n 层依次试一遍, 取最小值
// 所以 dp(k, n) = min(max(dp(k, n - i), dp(k - 1, i - 1))) for x in 1...n
// 当 n == 0 时, 不用扔就可以确定, dp(k, 1) = 0
// 当 n == 1 时, 显然至多至少都是要扔 1 次, dp(k, 1) = 1
// 当 k == 1 时, 显然需要从 1 到 n 一层一层尝试 dp(1, n) = n
int superEggDrop(int k, int n) {
vector<vector<int>> memo(k + 1, vector<int>(n + 1, -1));
return dp(k, n, memo);
}

int dp(int k, int n, vector<vector<int>>& memo) {
if(k == 1 || n == 0) {
return n;
}
if(memo[k][n] != -1) {
return memo[k][n];
}
int res = INT_MAX;
for(int i = 1; i <= n; i++) {
res = min(res, 1 + max(dp(k, n - i, memo), dp(k - 1, i - 1, memo)));
}
return memo[k][n] = res;
}
img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 记忆化递归 + 二分搜索优化 AC!
int superEggDrop(int k, int n) {
vector<vector<int>> memo(k + 1, vector<int>(n + 1, -1));
return dp(k, n, memo);
}

// 对于 dp(k, n) 我们观察到 这是一个关于 n 的单调递增函数
// 也就是说在鸡蛋数固定的情况下, 楼层 n 越多, 需要的尝试次数一定不会变少
// 对于我们得出的这两个子问题 dp(k, n - i) 和 dp(k - 1, i - 1)
// 前者随着 i 的增加而减少, 后者随着 i 的增加而增加
// 将其想象成两条直线, 一个单调递增一个单调递减, 我们要求得一个位置 i 使得它们的最大值最小
// 交点处!没错在它们的交点处能满足
// 我们可以利用二分来查找它们的交点, 看上面的图形就明白如何应用二分了
int dp(int k, int n, vector<vector<int>>& memo) {
if(k == 1 || n == 0) {
return n;
}
if(memo[k][n] != -1) {
return memo[k][n];
}
int res = INT_MAX;
int lo = 1, hi = n;
while(lo <= hi) {
int mi = lo + ((hi - lo) >> 1);
int notBroken = dp(k, n - mi, memo);
int broken = dp(k - 1, mi - 1, memo);
if(notBroken >= broken) {
lo = mi + 1;
res = min(res, notBroken + 1);
}
else {
hi = mi - 1;
res = min(res, broken + 1);
}
}
return memo[k][n] = res;
}
198. House Robber

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

Example 1:

1
2
3
4
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.

Example 2:

1
2
3
4
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.

Constraints:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 解法一
// 定义 dp[i] 表示从第 i 间 (i 从 0 算起) 房屋开始打劫最多能抢到的钱
// 对于第 i 间房屋我们有 2 个选择
// 1. 不抢
// dp[i] = dp[i + 1]
// 2. 抢
// dp[i] = nums[i] + dp[i + 2]
// 要求最多能抢多少钱, 两种选择取大值
// 即 dp[i] = max(dp[i + 1], nums[i] + dp[i + 2])
// 我们要求的目标是 dp[0]
// 当 i == n 时, 没房子抢了, dp[n] = 0
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n + 2);
for(int i = n - 1; i >= 0; i--) {
dp[i] = max(dp[i + 1], nums[i] + dp[i + 2]);
}
return dp[0];
}

// 当前状态只依赖前两个状态, 可进行空间优化
int rob(vector<int>& nums) {
int n = nums.size();
int dp_1 = 0, dp_2 = 0;
int dp_0 = 0;
for(int i = n - 1; i >= 0; i--) {
dp_0 = max(dp_1, nums[i] + dp_2);
dp_2 = dp_1;
dp_1 = dp_0;
}
return dp_0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 解法二: 重新定义 dp 数组
// dp[i][0] 表示不抢第 i 间房屋时, 经过了前 i 间所获得的金钱
// dp[i][1] 表示抢第 i 间房屋, 经过了前 i 间所获得的金钱
// 那么 dp[i][0] = max(dp[i - 1][0], dp[i - 1][1])
// dp[i][1] = dp[i - 1][0] + nums[i - 1]
int rob(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n + 1, vector<int>(2));
for(int i = 1; i <= n; i++) {
// 此家不抢, 上家可抢可不抢, 看哪个收益大
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
// 此家抢, 上家不能抢
dp[i][1] = dp[i - 1][0] + nums[i - 1];
}
return max(dp[n][0], dp[n][1]);
}
213. House Robber II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。

Example 1:

1
2
3
Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.

Example 2:

1
2
3
4
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.

Example 3:

1
2
Input: nums = [0]
Output: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在题 I 的基础上, 只需考虑三种情况
// 1. 第 0 间和第 n - 1 间都不抢
// 2. 第 0 间抢那么第 n - 1 间就不能抢
// 3. 第 n - 1 间抢那么第 0 间就不能抢
// 比较这三种情况, 取大值即可
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) {
return nums[0];
}
return max({rob(nums, 1, n - 2), rob(nums, 0, n - 2), rob(nums, 1, n - 1)});
}

int rob(vector<int>& nums, int start, int end) {
int dp_1 = 0, dp_2 = 0;
int dp_0 = 0;
for(int i = end; i >= start; i--) {
dp_0 = max(dp_1, nums[i] + dp_2);
dp_2 = dp_1;
dp_1 = dp_0;
}
return dp_0;
}
337. House Robber III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

Example 1:

img
1
2
3
Input: root = [3,2,3,null,3,null,1]
Output: 7
Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.

Example 2:

img
1
2
3
Input: root = [3,4,5,1,3,null,1]
Output: 9
Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在 I 题方法 2 的思路下
int rob(TreeNode* root) {
// <不抢 root 最多得到的钱, 抢 root 最多得到的钱>
auto dfs = [](auto&& dfs, TreeNode* root) {
if(root == nullptr) {
return make_pair(0, 0);
}
auto [l_norob, l_rob] = dfs(dfs, root->left);
auto [r_norob, r_rob] = dfs(dfs, root->right);
// 不抢, 左右两家家可抢可不抢, 看哪个收益大
auto norob = max(l_norob, l_rob) + max(r_norob, r_rob);
// 抢, 左右两家不能抢
auto rob = l_norob + root->val + r_norob;
return make_pair(norob, rob);
};
auto [norob, rob] = dfs(dfs, root);
return max(norob, rob);
}
740. Delete and Earn

给你一个整数数组nums,你可以对它进行一些操作。

每次操作中,选择任意一个nums[i],删除它并获得nums[i]的点数。之后,你必须删除每个等于nums[i] - 1nums[i] + 1的元素。

开始你拥有0个点数。返回你能通过这些操作获得的最大点数

Example 1:

1
2
3
4
5
Input: nums = [3,4,2]
Output: 6
Explanation: Delete 4 to earn 4 points, consequently 3 is also deleted.
Then, delete 2 to earn 2 points.
6 total points are earned.

Example 2:

1
2
3
4
5
Input: nums = [2,2,3,3,3,4]
Output: 9
Explanation: Delete 3 to earn 3 points, deleting both 2's and the 4.
Then, delete 3 again to earn 3 points, and 3 again to earn 3 points.
9 total points are earned.

Constraints:

  • 1 <= nums.length <= 2 * 104
  • 1 <= nums[i] <= 104
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在选择了元素 x 后, x, x - 1, x + 1 都会被删除
// 并且我们可以一下选择剩余的所有 x 以尽可能多的获得点数
// 如果我们定义 dp[i] 表示元素 i 开始选择可以获得的点数
// 选择元素 i 获得的点数等于 i * (i 出现的次数)
// 那么这题就和上面的打家劫舍题 I 相同了
// 也即是说抢劫了第 i 间房屋, i - 1 和 i + 1 都不能抢了
int deleteAndEarn(vector<int>& nums)
{
int maxVal = *max_element(nums.begin(), nums.end());
vector<int> counts(maxVal + 1);
for(int num : nums)
counts[num]++;
vector<int> dp(maxVal + 3);
for(int i = maxVal; i >= 0; i--)
dp[i] = max(dp[i + 1], i * counts[i] + dp[i + 2]);
return dp[0];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 预处理一下, 可以直接调用打家劫舍的 rob 函数
int deleteAndEarn(vector<int>& nums)
{
int maxVal = *max_element(nums.begin(), nums.end());
// sum[x] 等于所有 x 之和
vector<int> sum(maxVal + 1);
for(int num : nums)
sum[num] += num;
return rob(sum);
}

int rob(vector<int>& nums)
{
int n = nums.size();
vector<int> dp(n + 2);
for(int i = n - 1; i >= 0; i--)
dp[i] = max(dp[i + 1], nums[i] + dp[i + 2]);
return dp[0];
}
53. Maximum Subarray

给定一个整数数组nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

Example 1:

1
2
3
Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Example 2:

1
2
Input: nums = [1]
Output: 1

Example 3:

1
2
Input: nums = [5,4,-1,7,8]
Output: 23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 暴力 超时
// 时间复杂度 O(n2) 空间复杂度 O(1)
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int res = INT_MIN;
for(int i = 0; i < n; i++) {
int sum = 0;
for(int j = i; j < n; j++) {
sum += nums[j];
res = max(res, sum);
}
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 动态规划 dp[i] 表示以 nums[i] 结尾的连续子数组的最大和
// 时间复杂度和空间复杂度均为 O(n)
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n == 0) {
return 0;
}
vector<int> dp(n);
dp[0] = nums[0];
int res = dp[0];
for(int i = 1; i < n; i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
res = max(res, dp[i]);
}
return res;
}

// 由于 dp[i] 只与上一个状态 dp[i - 1] 和 nums[i] 有关
// 通过一个变量记录上一个状态值将空间复杂度压缩到 O(1)
// 并且同时使用一个变量更新状态中的最大值
// 时间复杂度 O(n) 空间复杂度 O(1)
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int preState = 0, curState = 0;
int res = INT_MIN;
for(int i = 0; i < n; i++) {
curState = nums[i];
if(preState > 0) {
curState += preState;
}
res = max(res, curState);
preState = curState;
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 二分法 (还没搞懂)
// 时间复杂度 O(nlogn) 空间复杂度 O(1)
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n == 0) {
return 0;
}
return binary_search(nums, 0, n - 1);
}

int binary_search(vector<int>&nums, int left, int right) {
if(left >= right) {
return nums[left];
}
int mid = (left + right) >> 1;
int leftMax = binary_search(nums, left, mid - 1);
int rightMax = binary_search(nums, mid + 1, right);
int midMax = nums[mid], t = midMax;
// 开始计算左边的最大值
for(int i = mid - 1; i >= left; i--) {
t += nums[i];
midMax = max(midMax, t);
}
t = midMax;
for(int i = mid + 1; i <= right; i++) {
t += nums[i];
midMax = max(midMax, t);
}
return max(midMax, max(leftMax, rightMax));
}
152. Maximum Product Subarray

给你一个整数数组nums,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

Example 1:

1
2
3
Input: nums = [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.

Example 2:

1
2
3
Input: nums = [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

Constraints:

  • 1 <= nums.length <= 2 * 104
  • -10 <= nums[i] <= 10
  • The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 如果还是定义 dp[i] 表示以 nums[i] 结尾的连续子数组的最大乘积
// 继续以 53 题的思路解决这道题是错误的做法
// 因为数组中负数的出现会使得正的最大值乘以负数而可能变成最小值
// 而负的最小值乘以一个负数而可能会变成最大值
// 也就是说乘积的最大值和最小值会由于当前的数的正负而发生转化
// 所以这题要根据 nums[i] 的正负来分别考虑以 nums[i] 结尾的子数组的最大值和最小值两种情况
// 定义 dp[0][i] 和 dp[1][i] 分别表示以 nums[i] 结尾的子数组的乘积的最小值和最大值
// 当 nums[i] > 0 时
// dp[0][i] = min(nums[i] * dp[0][i - 1], nums[i])
// dp[1][i] = max(nums[i] * dp[1][i - 1], numa[i])
// 当 nums[i] < 0 时
// dp[0][i] = min(nums[i] * dp[1][i - 1], nums[i])
// dp[1][i] = max(nums[i] * dp[0][i - 1], numa[i])
// 这题也可以问你连续子数组的最小乘积是多少
int maxProduct(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(2));
dp[0][0] = dp[0][1] = nums[0];
int res = nums[0];
for(int i = 1; i < n; i++) {
if(nums[i] > 0) {
dp[i][0] = min(dp[i - 1][0] * nums[i], nums[i]);
dp[i][1] = max(dp[i - 1][1] * nums[i], nums[i]);
}
else {
dp[i][0] = min(dp[i - 1][1] * nums[i], nums[i]);
dp[i][1] = max(dp[i - 1][0] * nums[i], nums[i]);
}
res = max(res, dp[i][1]);
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 当前状态只依赖前一个状态, 故可以使用滚动变量进行空间优化
int maxProduct(vector<int>& nums) {
int n = nums.size();
int dp_0_0 = nums[0];
int dp_1_0 = nums[0];
int res = dp_1_0;
// 滚动变量
int dp_0_1, dp_1_1;
for(int i = 1; i < n; i++) {
if(nums[i] > 0) {
dp_0_1 = min(nums[i] * dp_0_0, nums[i]);
dp_1_1 = max(nums[i] * dp_1_0, nums[i]);
}
else {
dp_0_1 = min(nums[i] * dp_1_0, nums[i]);
dp_1_1 = max(nums[i] * dp_0_0, nums[i]);
}
dp_0_0 = dp_0_1;
dp_1_0 = dp_1_1;
res = max(res, dp_1_1);
}
return res;
}
1262. Greatest Sum Divisible by Three

给你一个整数数组nums,请你找出并返回能被3整除的元素最大和

Example 1:

1
2
3
Input: nums = [3,6,5,1,8]
Output: 18
Explanation: Pick numbers 3, 6, 1 and 8 their sum is 18 (maximum sum divisible by 3).

Example 2:

1
2
3
Input: nums = [4]
Output: 0
Explanation: Since 4 is not divisible by 3, do not pick any number.

Example 3:

1
2
3
Input: nums = [1,2,3,4,4]
Output: 12
Explanation: Pick numbers 1, 3, 4 and 4 their sum is 12 (maximum sum divisible by 3).

Constraints:

  • 1 <= nums.length <= 4 * 10^4
  • 1 <= nums[i] <= 10^4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// dp[i][j] 表示对于前 i 个数, 选取的数字和对 3 取余为 j 的最大和 (j == 0, 1, 2)
// 我们的目标是要求 dp[n][0]
// 那么如果进行状态转移呢?
// 需要看第 i 个数 nums[i - 1] 对 3 取余的情况
// 1. 余数为 0
// 加到哪一个最大和身上对 3 取余的结果不变
// dp[i][0] = max(dp[i - 1][0], dp[i - 1][0] + nums[i - 1])
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][1] + nums[i - 1])
// dp[i][2] = max(dp[i - 1][2], dp[i - 1][2] + nums[i - 1])
// 2. 余数为 1
// 加到原来余 2 的最大和身上, 余数变成 0
// 所以 dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] + nums[i - 1])
// 加到原来余 0 的最大和身上, 余数变成 1
// 所以 dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + nums[i - 1])
// 加到原来余 1 的最大和身上, 余数变成 2
// 所以 dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i - 1])
// 3. 余数为 2
// 加到原来余 1 的最大和身上, 余数变成 0
// 所以 dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + nums[i - 1])
// 加到原来余 2 的最大和身上, 余数变成 1
// 所以 dp[i][1] = max(dp[i - 1][1], dp[i - 1][2] + nums[i - 1])
// 加到原来余 0 的最大和身上, 余数变成 2
// 所以 dp[i][2] = max(dp[i - 1][2], dp[i - 1][0] + nums[i - 1])
int maxSumDivThree(vector<int> &nums) {
// k 为任意数的一般情况
int k = 3;
int n = nums.size();
vector<vector<int>> dp(n + 1, vector<int>(k));
// 这个初始化有点不太好想到
for(int j = 1; j < k; j++) {
dp[0][j] = INT_MIN;
}
for(int i = 1; i <= n; i++) {
for(int j = 0; j < k; j++) {
int mod = nums[i - 1] % k;
dp[i][j] = max(dp[i - 1][j], dp[i - 1][(k - mod + j) % k] + nums[i - 1]);
}
}
return dp[n][0];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在理解了上一种解法的基础上, 下面这种思路更好
// dp[i] 表示选择的数字累加和模 3 == i 的数字和
// 对于 nums[i], 如果 nums[i] % 3 == 1, 那么将其加到之前模 3 == 2 的最大和上面
// 就变成了模 3 == 0 了, 所以 dp[0] = max(dp[0], dp[2] + nums[i])
// 依次类推, 对于每一个 nums[i] 都要更新 dp 数组的值
int maxSumDivThree(vector<int> &nums) {
// k 为任意数的一般情况
int k = 3;
vector<int> dp(k);
for(int num : nums) {
// 备份上一个状态
// 因为要用上一个状态的值来更新当前状态
auto temp = dp;
for(int maxSum : temp) {
int mod = (maxSum + num) % k;
dp[mod] = max(dp[mod], maxSum + num);
}
}
return dp[0];
}
486. Predict the Winner

给定一个表示分数的非负整数数组。 玩家A从数组任意一端拿取一个分数,随后玩家B继续从剩余数组任意一端拿取分数,然后玩家A拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。

给定一个表示分数的数组,预测作为先手的玩家A是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

  • 如果最终两个玩家的分数相等,那么玩家A仍为赢家。

示例:

1
2
3
4
5
6
7
8
输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 定义 dp[i][j][0] 表示先手在面对 nums[i..j] 的石头堆时能拿到的最大分数
// dp[i][j][1] 表示后手在面对 nums[i..j] 的石头堆时能拿到的最大分数
// 1. 对于石头堆 nums[i..j]
// 先手需要判断拿左边堆分数高还是拿右边堆分数高
// 如果先手拿了左边堆, 先手面对 nums[i+1..j] 时就变成了后手
// 此时的得分是 dp[i + 1][j][1] + nums[i]
// 如果先手拿了右边堆, 先手面对 nums[i..j-1] 时就变成了后手
// 此时的得分是 dp[i][j - 1][1] + nums[j]
// 肯定取较高的情况
// dp[i][j][0] = max(dp[i + 1][j][1] + nums[i], dp[i][j - 1][1] + nums[j])
// 2. 先手的选择对后手会产生影响, 如果先手选择了左边堆
// 后手在面对 nums[i+1..j] 时就变成了先手
// dp[i][j][1] = dp[i + 1][j][0]
// 如果先手选择了右边堆
// 后手在面对 nums[i..j-1] 时就变成了先手
// dp[i][j][1] = dp[i][j - 1][0]
// 当 i > j 时, 没有石头堆了 dp[i][j][0,1] = 0
// 当 i == j 时, dp[i][j][0] = nums[i], dp[i][j][1] = 0
bool PredictTheWinner(vector<int>& nums)
{
int n = nums.size();
vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(2)));
for(int i = 0; i < n; i++)
{
dp[i][i][0] = nums[i];
dp[i][i][1] = 0;
}
for(int i = n - 2; i >= 0; i--)
{
for(int j = i + 1; j < n; j++)
{
// 比较拿左边堆之后和拿右边堆之后的分数
int left = dp[i + 1][j][1] + nums[i];
int right = dp[i][j - 1][1] + nums[j];
if(left > right)
{
dp[i][j][0] = left;
dp[i][j][1] = dp[i + 1][j][0];
}
else
{
dp[i][j][0] = right;
dp[i][j][1] = dp[i][j - 1][0];
}
// 不可以写成这样, 不能仅仅比较当前左右两堆石头的大小
// 比如 [1, 5, 233, 7] 的情况
// if(nums[i] > nums[j])
// {
// dp[i][j][0] = dp[i + 1][j][1] + nums[i];
// dp[i][j][1] = dp[i + 1][j][0];
// }
// else
// {
// dp[i][j][0] = dp[i][j - 1][1] + nums[j];
// dp[i][j][1] = dp[i][j - 1][0];
// }
}
}
return dp[0][n - 1][0] >= dp[0][n - 1][1];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 动态规划的另一种思路
// 甲乙比赛, 甲先手面对区间 [i...j] 时, dp[i][j] 表示甲对乙的净胜分
// 最终求的就是, 甲先手面对区间 [0...n-1] 时, 甲对乙的净胜分 dp[0][n-1] 是否 >= 0
// 甲先手面对区间[i...j]时
// 1. 如果甲拿 nums[i], 那么变成乙先手面对区间 [i+1...j], 这段区间内乙对甲的净胜分为 dp[i+1][j]
// 那么甲对乙的净胜分就应该是 nums[i] - dp[i+1][j]
// 如果甲拿 nums[j], 同理可得甲对乙的净胜分为是 nums[j] - dp[i][j-1]
// 以上两种情况二者取大即可
// 状态转移方程 dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
bool PredictTheWinner(vector<int>& nums)
{
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(n));
// 当 i == j 时先手对后手的净胜分就是 nums[i]
for(int i = 0; i < n; i++)
dp[i][i] = nums[i];

for(int i = n - 2; i >= 0; i--)
for (int j = i + 1; j < n; j++)
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);

return dp[0][n - 1] >= 0;
}
fig1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 记忆化递归
unordered_map<string, int> memo;
bool PredictTheWinner(vector<int>& nums)
{
return totalScores(nums, 0, nums.size() - 1, 1) >= 0;
}
// 返回拿走 [i, j] 的石头堆后的的得分
// flag = 1 表示当前是先手, flag = -1 表示当前是后手
// flag 可用于控制先手的得分均为正值, 后手的得分均为负值
// 交替选择的过程中将他们的分数加起来
// 显然, 最后总得分为正时先后获胜, 为负时后手获胜
int totalScores(vector<int>& nums, int i, int j, int flag)
{
if(i == j) {
return flag * nums[i];
}
string key = to_string(i) + "," + to_string(j) + "," + to_string(flag);
if(memo.count(key)) {
return memo[key];
}
int selectLeft = flag * nums[i] + totalScores(nums, i + 1, j, -flag);
int selectRight = flag * nums[j] + totalScores(nums, i, j - 1, -flag);
// 无论当前是先后还是后手, 其选择左或右后, 目前得分的总和可能为正也可能为负
// 所以需要去掉正负号后再选出较大的值, 方法就是 得分*flag 就可以
// 也就是说先手会尽量的把数变正变大
// 后手会尽量把数变负变小
return memo[key] = flag * max(flag * selectLeft, flag * selectRight);
}
664. Strange Printer

有台奇怪的打印机有以下两个特殊要求:

  • 打印机每次只能打印由 同一个字符 组成的序列。
  • 每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。

给你一个字符串s,你的任务是计算这个打印机打印它需要的最少打印次数。

Example 1:

1
2
3
>Input: s = "aaabbb"
>Output: 2
>Explanation: Print "aaa" first and then print "bbb".

Example 2:

1
2
3
>Input: s = "aba"
>Output: 2
>Explanation: Print "aaa" first and then print "b" from the second place of the string, which will cover the existing character 'a'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 定义 dp[i][j] 表示打印出字符串区间 s[i..j] 中的字符所需要的最少次数
// 对于 s[i] == s[j]
// 我们在第一次打印出 s[i] 的同时可以顺便打印出 s[j]
// 因此只需要关注如何更快的打印出 s[i..j-1] 中的字符即可
// dp[i][j] = dp[i][j - 1]
// 对于 s[i] != s[j]
// 我们要分别完成以 s[i] 开头的左半部分字符和以 s[j] 结尾的右半部分字符的打印
// 具体的需要在 i 到 j 之间找一个分界点 k
// 找到最小的 dp[i][k] + dp[k + 1][j]
// dp[i][j] = min(dp[i][k] + dp[k + 1][j]) for k in [i, j)
// 我们的目标是求 dp[0][n - 1]
// 当 i == j 时 dp[i][j] = 1 只需要一次打印即可
int strangePrinter(string s)
{
// 可能想到的优化点
// 注意到本题中连续的相同字符等价于单个字符
// 所以可以先进行一个相邻元素去重处理
// auto last = unique(s.begin(), s.end());
// s.resize(distance(s.begin(), last));

int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
for(int i = 0; i < n; i++) {
dp[i][i] = 1;
}
// 由于 dp[i][j] 依赖于 dp[i + k][j]
// 所以画出二维矩阵图之后知道我们要对 i 从下往上, j 从左往右遍历
for(int i = n - 2; i >= 0; i--) {
for(int j = i + 1; j < n; j++) {
if(s[i] == s[j]) {
dp[i][j] = dp[i][j - 1];
}
else {
for(int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
}
return dp[0][n - 1];
}
546. Remove Boxes

给出一些不同颜色的盒子,盒子的颜色由数字表示,即不同的数字表示不同的颜色。

你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续k个盒子(k >= 1),这样一轮之后你将得到k * k个积分。

当你将所有盒子都去掉之后,求你能获得的最大积分和

Example:

1
2
3
4
5
6
7
8
Input: boxes = [1,3,2,2,2,3,4,3,1]
Output: 23
Explanation:
[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (3*3=9 points)
----> [1, 3, 3, 3, 1] (1*1=1 points)
----> [1, 1] (3*3=9 points)
----> [] (2*2=4 points)
1
2
3
4
5
// 太特么难了
int removeBoxes(vector<int>& boxes)
{

}
338. Counting Bits

给定一个非负整数num。对于0 ≤ i ≤ num范围中的每个数字i,计算其二进制数中的1的数目并将它们作为数组返回。

进阶:给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?

Example:

1
2
3
4
5
6
7
8
9
Input: n = 5
Output: [0, 1, 1, 2, 1, 2]
Explanation:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<int> countBits(int n) {
vector<int> bits(n + 1);
for(int i = 0; i <= n; i++) {
bits[i] = countOnes(i);
}
return bits;
}

int countOnes(int x) {
int ones = 0;
while(x > 0) {
x = x & (x - 1);
ones++;
}
return ones;
}
1
2
3
4
5
6
7
8
9
10
11
12
// 计算当前状态的值时通过最高位的 1 转移到之前的状态
vector<int> countBits(int n) {
vector<int> bits(n + 1);
int highBit = 0;
for(int i = 1; i <= n; i++) {
if((i & (i - 1)) == 0) {
highBit = i;
}
bits[i] = bits[i - highBit] + 1;
}
return bits;
}
1
2
3
4
5
6
7
8
9
10
11
12
// 计算当前状态的值时通过最低位的 1 转移到之前的状态
// 实际上就是通过把值变小, 使当前值中的 1 的个数和更小的值中 1 的个数具有数量关系
vector<int> countBits(int n) {
vector<int> bits(n + 1);
for(int i = 1; i <= n; i++) {
// 方式一: 右移一位, 最右边的位可能为 0 或 1
bits[i] = bits[i >> 1] + (i & 1);
// 方式二: 去掉最低位 1
bits[i] = bits[i & (i - 1)] + 1;
}
return bits;
}
1866. Number of Ways to Rearrange Sticks With K Sticks Visible

n根长度互不相同的木棍,长度为从1n的整数。请你将这些木棍排成一排,并满足从左侧可以看到恰好k根木棍。

从左侧可以看到木棍的前提是这个木棍的左侧不存在比它更长的木棍。例如,如果木棍排列为[1,3,2,5,4],那么从左侧可以看到的就是长度分别为1、3 、5的木棍。

给你nk,返回符合题目要求的排列数目 。由于答案可能很大,请返回对10^9 + 7取余的结果。

Example 1:

1
2
3
4
Input: n = 3, k = 2
Output: 3
Explanation: [1,3,2], [2,3,1], and [2,1,3] are the only arrangements such that exactly 2 sticks are visible.
The visible sticks are underlined.

Example 2:

1
2
3
4
Input: n = 5, k = 5
Output: 1
Explanation: [1,2,3,4,5] is the only arrangement such that all 5 sticks are visible.
The visible sticks are underlined.

Example 3:

1
2
3
Input: n = 20, k = 11
Output: 647427950
Explanation: There are 647427950 (mod 10^9 + 7) ways to rearrange the sticks such that exactly 11 sticks are visible.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义 dp[i][j] 表示对于高度为 [1..i] 的所有柱子进行排列, 从左侧能看到 j 根的排列数
// 我们考虑最后一根柱子能不能被看见
// 1. 如果最后一根柱子能被看见, 那么它的高度一定为 i
// 这样对于左侧 [1..i-1] 高度的柱子看到 j-1 根柱子的排列数就是之前的状态
// dp[i][j] = dp[i - 1][ j - 1]
// 2. 如果最后一根柱子不能被看见, 那么它的高度可以为 [1..i-1] 中的任意一个
// 由于一根木棍能否被看到只与它和它左侧木棍的「相对高度关系」有关,而与「绝对高度关系」无关
// 因此, 此时最后一根柱子的高度有 [1..i-1] 中 i-1 种选择
// 固定每种选择后, 对于左侧的柱子看到 j 根柱子的排列数就是之前状态
// dp[i][j] = (i - 1) * dp[i - 1][j]
// 综合两种情况 dp[i][j] = dp[i - 1][ j - 1] + (i - 1) * dp[i - 1][j]
// 我们的目标是求 dp[n][k]
// 当 i == 0 && j == 0 时 dp[0][0] = 1, 其它情况都初始化为 0
int rearrangeSticks(int n, int k) {
int mod = 1e9+7;
vector<vector<long>> dp(n + 1, vector<long>(k + 1));
dp[0][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= k; j++) {
dp[i][j] = dp[i - 1][j - 1] + (i - 1) * dp[i - 1][j];
dp[i][j] %= mod;
}
}
return dp[n][k];
}
410. Split Array Largest Sum

给定一个非负整数数组nums和一个整数m,你需要将这个数组分成m个非空的连续子数组。设计一个算法使得这m个子数组各自和的最大值最小。

Example:

1
2
3
4
5
6
Input: nums = [7,2,5,10,8], m = 2
Output: 18
Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [7,2,5] and [10,8],
where the largest sum among the two subarrays is only 18.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义 dp[i][j] 表示将前 i 个数划分成 j 个连续的子数组, 各自和的最大值
int splitArray(vector<int>& nums, int m) {
int n = nums.size();
vector<int> prefix(n + 1);
for(int i = 1; i <= n; i++) {
prefix[i] = prefix[i - 1] + nums[i - 1];
}
vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MAX));
dp[0][0] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
// 枚举将前 k 个数划分为 j - 1 个子数组
// 因为子数组非空, 所以 k in [j - 1, i - 1]
for(int k = j - 1; k < i; k++) {
dp[i][j] = min(dp[i][j], max(dp[k][j - 1], prefix[i] - prefix[k]));
}
}
}
return dp[n][m];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 二分 + 贪心 check
int splitArray(vector<int>& nums, int m) {
auto check = [&](int maxVal) {
int cnt = 1, sum = 0;
for(int num : nums) {
if(sum + num <= maxVal) {
sum += num;
}
else {
cnt++;
sum = num;
}
}
return cnt <= m;
};
int lo = 0, hi = 0;
for(int num : nums) {
hi += num;
if(lo < num) {
lo = num;
}
}
while(lo <= hi) {
int mi = lo + (hi - lo) / 2;
if(check(mi)) {
hi = mi - 1;
}
else {
lo = mi + 1;
}
}
return lo;
}
1049. 最后一块石头的重量 II

有一堆石头,用整数数组stones表示。其中stones[i]表示第i块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为xy,且x <= y。那么粉碎的可能结果如下:

  • 如果x == y,那么两块石头都会被完全粉碎;

  • 如果x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y - x

    最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回0

Example 1:

1
2
3
4
5
6
7
Input: stones = [2,7,4,1,8,1]
Output: 1
Explanation:
We can combine 2 and 4 to get 2, so the array converts to [2,7,1,8,1] then,
we can combine 7 and 8 to get 1, so the array converts to [2,1,1,1] then,
we can combine 2 and 1 to get 1, so the array converts to [1,1,1] then,
we can combine 1 and 1 to get 0, so the array converts to [1], then that's the optimal value.

Example 2:

1
2
Input: stones = [31,26,33,21,40]
Output: 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 每一回合的操作相当于在较大的数前面放置一个 '+' 号, 在较小的数前面放置一个 '-' 号
// 经过若干回合之后, 所有的数前面都被放置了相应的正负号
// 最后形成的表达式求值就是最终剩下的石头的重量
// 我们假设前面被放正号的数之和是 sumP, 前面放负号的数之和为 sumN
// 所有石头总和为 sum, 最后剩下的石头重量为 w
// 则 w = sumP - sumN = sum - sumN - sumN = sum - 2*sumN
// 为了保证 w 最小, 那么我们就是要保证 sum - 2*sumN 为非负值的情况下
// 也就是在 sumN <= sum/2 时, 找到尽可能大的 sumN
// 此时问题就转换为了一个 01 背包问题
// 有一个容量为 sum/2 的背包和一堆重量已知的石头
// 问我们如何装能够装的石头总重量最大
// 定义 dp[i][j] 表示对于前 i 堆石头, 背包容量为 j 时能装的最大重量
// 对于第 i 块石头有两个选择
// 1. 不装 dp[i][j] = dp[i - 1][j]
// 2. 装 dp[i][j] = dp[i - 1][j - stones[i - 1]] + stones[i - 1]
// 我们要求最大可能的重量, 所以两者取最大值即可
// 对于初始条件, 当 i == 0 时, 即没有石头可装, dp[i][j] = 0
// 当 j == 0, 即背包容量为 0, dp[i][j] = 0
int lastStoneWeightII(vector<int>& stones) {
int n = stones.size();
int sum = accumulate(stones.begin(), stones.end(), 0);
vector<vector<int>> dp(n + 1, vector<int>(sum/2 + 1));
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= sum/2; j++) {
dp[i][j] = dp[i - 1][j];
if(j >= stones[i - 1]) {
dp[i][j] = max(dp[i][j] , dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
}
}
return sum - 2*dp[n][sum/2];
}
1
2
3
4
5
6
7
8
9
10
11
12
// 由于 i 的当前状态只与前一个状态有关, 使用滚动数组优化
int lastStoneWeightII(vector<int>& stones) {
int n = stones.size();
int sum = accumulate(stones.begin(), stones.end(), 0);
vector<int> dp(sum/2 + 1);
for(int i = 0; i < n; i++) {
for(int j = sum/2; j >= stones[i]; j--) {
dp[j] = max(dp[j] , dp[j - stones[i]] + stones[i]);
}
}
return sum - 2*dp[sum/2];
}
879. 盈利计划

集团里有n名员工,他们可以完成各种各样的工作创造利润。第i种工作会产生profit[i]的利润,它要求group[i]名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生minProfit利润的子集称为盈利计划。并且工作的成员总数最多为n

有多少种计划可以选择?因为答案可能很大,所以返回结果模10^9 + 7的值。

示例 1

1
2
3
4
输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。

示例 2

1
2
3
4
输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

提示:

  • 1 <= n <= 100,0 <= minProfit <= 100,1 <= group.length <= 100
  • 1 <= group[i] <= 100,profit.length == group.length,0 <= profit[i] <= 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 状态有工作, 员工数以及当前利润
// 我们定义 dp[i][j][k] 表示对于前 i 种工作, 参与的员工数为 j 时, 在利润 [至少] 为 k 的情况下的计划数
// 对于第 i 种工作我们有选择和不选两种选择
// 1. 不选 显然有
// dp[i][j][k] = dp[i - 1][j][k]
// 2. 选
// dp[i][j][k] = dp[i - 1][j - group[i - 1]][k - profit[i - 1]]
// 注意到对于状态 k 我们定义的是至少获得的利润
// 因此, 当 k - profit[i - 1] 为负时, 我们都取 0
// 也就是说所有大于 k 的利润都归为等于 k, 这就是至少的定义嘛
// 当 i == 0 时, 无论你用了多少个员工, 至少获得的利润 k 显然都为 0, 这属于 1 种计划
// dp[0][j][0] = 1 for j in [1..n]
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int m = group.size();
int mod = 1e9 + 7;
vector<vector<vector<int>>> dp(m + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
for(int j = 0; j <= n; j++) {
dp[0][j][0] = 1;
}
for(int i = 1; i <= m; i++) {
for(int j = 0; j <= n; j++) {
for(int k = 0; k <= minProfit; k++) {
dp[i][j][k] = dp[i - 1][j][k];
if(j >= group[i - 1]) {
dp[i][j][k] += dp[i - 1][j - group[i - 1]][max(0, k - profit[i - 1])];
}
dp[i][j][k] = dp[i][j][k] % mod;
}
}
}
return dp[m][n][minProfit];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 滚动数组优化空间复杂度
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int m = group.size();
int mod = 1e9 + 7;
vector<vector<int>> dp(n + 1, vector<int>(minProfit + 1));
for(int j = 0; j <= n; j++) {
dp[j][0] = 1;
}
for(int i = 1; i <= m; i++) {
for(int j = n; j >= group[i - 1]; j--) {
for(int k = 0; k <= minProfit; k++) {
dp[j][k] += dp[j - group[i - 1]][max(0, k - profit[i - 1])];
dp[j][k] = dp[j][k] % mod;
}
}
}
return dp[n][minProfit];
}
1155. 掷骰子的N种方法

这里有d个一样的骰子,每个骰子上都有f个面,分别标号为1, 2, ..., f

我们约定:掷骰子的得到总点数为各骰子面朝上的数字的总和。

如果需要掷出的总点数为target,请你计算出有多少种不同的组合情况(所有的组合情况总共有f^d种),模1e9+7后返回。

提示:

  • 1 <= d, f <= 30
  • 1 <= target <= 1000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 01 背包问题
int numRollsToTarget(int d, int f, int target) {
vector<vector<int>> dp(d + 1, vector<int>(target + 1));
dp[0][0] = 1;
for(int i = 1; i <= d; i++) {
for(int j = 1; j <= target; j++) {
for(int k = 1; k <= f; k++) {
if(k <= j) {
dp[i][j] += dp[i - 1][j - k];
dp[i][j] = dp[i][j] % int(1e9+7);
}
}
}
}
return dp[d][target];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 滚动数组优化空间
int numRollsToTarget(int d, int f, int target) {
int mod = 1e9+7;
vector<int> dp(target + 1);
dp[0] = 1;
for(int i = 1; i <= d; i++) {
for(int j = target; j >= 0; j--) {
// 这个不太容易想到
// 它表示降维前, dp[1..d][0] = 0 的情况
// 即, 使用了多于一个骰子的话, 和为 0 的情况不可能出现
// 因此, 每次迭代时要将这种情况的组合数设为 0
dp[j] = 0;
for(int k = 1; k <= f && k <= j; k++) {
dp[j] += dp[j - k];
dp[j] = dp[j] % mod;
}
}
}
return dp[target];
}
801. 使序列递增的最小交换次数

我们有两个长度相等且不为空的整型数组AB。我们可以交换A[i]B[i]的元素。注意这两个元素在各自的序列中应该处于相同的位置。在交换过一些元素之后,数组AB都应该是严格递增的。

给定数组AB,请返回使得两个数组均保持严格递增状态的最小交换次数。假设给定的输入总是有效的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 对于第 i 个元素, 有交换和不交换两种选择
// 定义 dp[i][0] 表示第 i 个元素交换时, 将前 i 个元素交换操作之后严格递增的最小操作次数
// 定义 dp[i][1] 表示第 i 个元素不交换时, 将前 i 个元素交换操作之后严格递增的最小操作次数
// 那么在考虑第 i 个元素时, 需要连同第 i - 1 个元素一起考虑
// 也就是 A[i - 1], A[i], B[i - 1] 和 B[i] 这四个元素
// 1. A[i - 1] < A[i] && B[i - 1] < B[i]
// 此时若 A[i - 1] < B[i] && B[i - 1] < A[i]
// 前一个元素可以交换也可以不交换
// 1.1 当前元素不交换 dp[i][0] = min(dp[i - 1][0], dp[i - 1][1])
// 1.2 当前元素交换 dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + 1
// 此时若 A[i - 1] >= B[i] || B[i - 1] >= A[i]
// 当前元素和前一个元素必须同时交换或不交换
// 1.3 当前元素不交换 dp[i][0] = dp[i - 1][0]
// 1.4 当前元素交换 dp[i][1] = dp[i - 1][1] + 1
// 2. A[i - 1] >= A[i] || B[i - 1] >= B[i]
// 当前元素若不交换则前一元素需要交换, 当前元素若交换则前一元素不能交换
// dp[i][0] = d[i - 1][1]
// dp[i][1] = d[i - 1][0] + 1
// 作为 base case, 第 0 个元素可以交换也可以不交换
// 若不交换 dp[0][0] = 0
// 若交换 dp[0][1] = 1
int minSwap(vector<int>& A, vector<int>& B) {
int n = A.size();
vector<vector<int>> dp(n, vector<int>(2));
dp[0][0] = 0;
dp[0][1] = 1;
for(int i = 1; i < n; i++) {
if(A[i - 1] < A[i] && B[i - 1] < B[i]) {
if(A[i - 1] < B[i] && B[i - 1] < A[i]) {
dp[i][0] = min(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + 1;
}
else {
dp[i][0] = dp[i - 1][0];
dp[i][1] = dp[i - 1][1] + 1;
}
}
else {
dp[i][0] = dp[i - 1][1];
dp[i][1] = dp[i - 1][0] + 1;
}
}
return min(dp[n - 1][0], dp[n - 1][1]);
}
LCP 07. 传递信息

小朋友A在和ta的小伙伴们玩传信息游戏,游戏规则如下:

  1. n名玩家,所有玩家编号分别为0~n-1,其中小朋友A的编号为0

  2. 每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的关系是单向的(比如A可以向B传信息,但B不能向A传信息)。

  3. 每轮信息必须需要传递给另一个人,且信息可重复经过同一个人

    给定总玩家数n,以及按[玩家编号,对应可传递玩家编号]关系组成的二维数组relation。返回信息从小A (编号0) 经过k轮传递到编号为n-1的小伙伴处的方案数;若不能到达,返回0

示例 1

1
2
3
输入:n = 5, relation = [[0,2],[2,1],[3,4],[2,3],[1,4],[2,0],[0,4]], k = 3
输出:3
解释:信息从小 A 编号 0 处开始,经 3 轮传递,到达编号 4。共有 3 种方案,分别是 0->2->0->4, 0->2->1->4, 0->2->3->4

示例 2

1
2
3
输入:n = 3, relation = [[0,2],[2,1]], k = 2
输出:0
解释:信息不能从小 A 处经过 2 轮传递到编号 2

限制:

  • 2 <= n <= 10
  • 1 <= k <= 5
  • 1 <=relation.length <= 90, 且 relation[i].length == 2
  • 0 <= relation[i][0], relation[i][1]< n 且 relation[i][0] != relation[i][1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 比较容易想到的是 dfs
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> edges(n);
for(auto&& r : relation) {
edges[r[0]].push_back(r[1]);
}
int res = 0;
auto dfs = [&](auto&& dfs, int idx, int step) {
if(step == k) {
if(idx == n - 1) {
res++;
}
return;
}
for(auto&& to : edges[idx]) {
dfs(dfs, to, step + 1);
}
};
dfs(dfs, 0, 0);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 动态规划解法
// 假设当前我们已经走了 i 步, 所在位置为 j, 那么剩余 k - j 步, 能否到达位置 n - 1
// 仅取决于 [剩余步数 k - i] 和 [边权关系 relation], 与如何到达位置 i 无关
// 对于方案数而言, 如果已经走了 i 步, 所在位置为 j, 到达位置 n - 1 的方案数仅取决于
// [剩余步数 k - i], [边权关系 relation] 和 [花费 i 步到达位置 j 的方案数]
// 以上分析归纳到边界 [走了 0 步, 所在位置 0] 同样成立
// 这就是动态规划中需要满足的 [无后效性]
// 定义 dp[i][j] 表示走了 i 步, 到达位置 j 的方案数
// 最终要求 dp[k][n - 1], 初始时 dp[0][0] = 1
// dp[i][j] = dp[i - 1][p] for [p, j] in relation
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> dp(k + 1, vector<int>(n));
dp[0][0] = 1;
for(int i = 1; i <= k; i++) {
for(auto&& edge : relation) {
int src = edge[0], dst = edge[1];
dp[i][dst] += dp[i - 1][src];
}
}
return dp[k][n - 1];
}
313. 超级丑数

编写一段程序来查找第n个超级丑数。超级丑数是指其所有质因数都是长度为k的质数列表primes中的正整数。

示例

1
2
3
4
输入: n = 12, primes = [2,7,13,19]
输出: 32
解释: 给定长度为 4 的质数列表 primes = [2,7,13,19],
前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32]

说明

  • 1是任何给定primes的超级丑数;
  • 给定primes中的数字以升序排列。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 利用 set 的去重和有序特性
int nthSuperUglyNumber(int n, vector<int>& primes) {
set<long> uglys;
long res = 1;
for(int i = 1; i < n; i++) {
for(int prime : primes) {
uglys.insert(prime * res);
}
res = *uglys.begin();
uglys.erase(res);
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 动态规划: 多指针
int nthSuperUglyNumber(int n, vector<int>& primes) {
int k = primes.size();
vector<int> p(k);
vector<int> su(n);
su[0] = 1;
for(int i = 1; i < n; i++) {
su[i] = INT_MAX;
for(int j = 0; j < k; j++) {
su[i] = min(su[i], primes[j] * su[p[j]]);
}
for(int j = 0; j < k; j++) {
if(su[i] == primes[j] * su[p[j]]) {
p[j]++;
}
}
}
return su[n - 1];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 263. 丑数
//https://leetcode-cn.com/problems/ugly-number/
bool isUgly(int n) {
if(n <= 0) {
return false;
}
for(int factor : {2, 3, 5}) {
while(n % factor == 0) {
n /= factor;
}
}
return n == 1;
}


// 264. 丑数II
// https://leetcode-cn.com/problems/ugly-number-ii/

// 使用 set 的自动排序与降重功能
// 时间复杂度为外围的 n 次循环 O(n)
// 内部 set 的插入和删除操作都是 O(logn)
// 所以时间复杂度总共是 O(nlogn)
// 空间复杂度总共为 set 使用的 O(n)
int nthUglyNumber(int n) {
set<long> setting;
long res = 1;
while(--n > 0) {
setting.insert(res * 2);
setting.insert(res * 3);
setting.insert(res * 5);
// 每次取出最小的丑数用于下一次循环
res = *setting.begin();
// 取出来之后它就没用了, 及时剔除
setting.erase(res);
}
return res;
}

// 使用动态规划: 三指针
// p2 代表的是第几个数的 2 倍
// p3 代表的是第几个数的 3 倍
// p5 代表的是第几个数的 5 倍
// 时间复杂度就只有外层的 n 次循环 O(n)
// 空间复杂度为 O(n)
int nthUglyNumber(int n) {
vector<int> u(n);
u[0] = 1;
int p2 = 0, p3 = 0, p5 = 0;
for(int i = 1; i < n; i++) {
u[i] = min({2 * u[p2], 3 * u[p3], 5 * u[p5]});
if(u[i] == 2 * u[p2]) {
p2++;
}
if(u[i] == 3 * u[p3]) {
p3++;
}
if(u[i] == 5 * u[p5]) {
p5++;
}
}
return u[n - 1];
}

// 助你理解一:
/*
这段代码两个关键点.
1. 每次对计算出三个丑数并取最小,这里需要计算三个丑数,一定有两个丑数是在上一次中就已经被计算并比较过的, 因为较大所以被筛掉了两个(这两个进入下次比较中), 留下较小的那个, 并自增一次最小值的下标 i, 使得参与下次计算时能稍微增大, 并同该次比较中淘汰的两个稍大数比较, 经过这样的不断比较 + 迭代, 能保证结果集是按从小到大的

2. 自增每次的最小丑数值的下标, 这么做一是可以迭代避免重复计算, 从而避免出现重复值, 二是避免遗漏. 因为按照最直接的理解与解法, 每个数都需分别 *2、*3、*5 加入数组, 历经n次,最后再排序,但通过设置三个从 0 开始的下标, 使其对应的值分别只与 2 或 3 或 5 相乘, 而每个下标都有机会迭代, 这样可以保证数组中的每个数实际都是分别与 2、3、5 乘一次, 数组中的前四个数是 1,2,3,5, 后面的数都是由前面的数分别与 2、3、5 相乘计算出来的, 这样也满足了丑数的定义, 并且每次计算的数都是先排序再加入的, 如此可以避免不漏且排序. 而不重是通过三个 if 判断解决的, 因为假如一个数既可通过 *2 得到又可以通过 *3 得到, 此时必然会重复计算并重复加入到数组, 因此此时需要既迭代 2 对应的下标 p2, 也迭代 3 对应的下标 p3, 所以 if 如果换成 if-else, 必然会有大量重复值出现.
*/

// 助你理解二:
/*
动态规划方法对于三指针的解释令人费解, 实际上这三个指针是用于对三个子序列归并排序的.
1. 令 nums 为升序的全部丑数序列, nums[n - 1] 即为所求, 初始只有一个元素 1, 即 nums[0] 为 1.

2. 根据丑数的定义可知, 除 1 外其他丑数都可以通过更小的丑数乘以 2、3、5 得到, 因此由 nums 可生成三个有序丑数子序列 nums_j, j 为 2、3、5. nums_j 元素生成规则为 nums[p_j] * j, p_j 为指向 nums 元素的指针,
对这三个有序丑数子序列进行归并排序可以求得 nums 中的其他元素.

3. nums 和三个子序列 nums_j 的元素都是动态生成的, 且生成 nums_j 的规则 nums[p_j] * j 中 j 是固定的, 不必预先创建出三个完整的子序列,只需维护三个指向 nums 的指针 p_j 用于归并排序即可.

4. 归并排序每生成一个最小丑数 min 时,将 min 追加到 nums 中, 然后生成 min 所在子序列 nums_j 的下一个元素, 即将 p_j 右移,pj++, 注意 min 可能同时存在于多个子序列,需要同时右移 p_j, 避免产生重复元素.

5. nums 每追加一个丑数后, pj 只可能右移 1 个位置, 所以不会发生越界的情况.
*/