Chapter 14 Dynamic Programming (2)
基本题目
416. 分割等和子集
0-1 背包问题,使用一维数组替代二维数组:从后向前更新即可
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (const int num : nums) sum += num;
if (sum % 2) return false;
sum /= 2;
vector<int> available_sums(sum + 1, 0);
available_sums[0] = 1;
for (const int num : nums) {
for (int i = sum; i >= num; i--)
available_sums[i] |= available_sums[i - num];
if (available_sums[sum]) return true;
}
return false;
}
};
1049. 最后一块石头的重量 II
本问题等价于:将石头分为两组,求它们的和之间的最小差值
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0, target;
for (const int w : stones) sum += w;
target = sum / 2;
vector<int> available_sums(target + 1, 0);
available_sums[0] = 1;
for (const int w : stones)
for (int i = target; i >= w; i--)
available_sums[i] = available_sums[i] | available_sums[i - w];
for (int i = target; i >= 0; i--)
if (available_sums[i])
return sum - 2 * i;
return 0;
}
};
正确性证明:
每次粉碎时,记重量最大的石头所处的堆为 A(若两堆最大重量相同则任选一堆),另一堆为 B。从 A 中取出重量最大的石头,B 中任取一石头,若没有完全粉碎,则将新石头重新放入 A。这一操作从每堆石头中减去了同样的重量,从而保证重量之差的绝对值在粉碎前后是不变的。
若出现一堆没有石头,而另一堆不止一块石头的情况,记有石头的那一堆为 A,另一堆为 B。要继续粉碎,则需要从 A 中取出一块石头移入 B,然后按规则粉碎。但移入操作让重量之差的绝对值变得更小,与事实(已按照最小和差划分石头)矛盾,所以不会出现这种情况。
494. 目标和
dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]];
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
if (nums.size() == 1) return target == nums[0] || target == -nums[0];
int sum = accumulate(nums.begin(), nums.end(), 0);
if (target < -sum || target > sum) return 0;
vector<vector<int>> possible_sums(nums.size(), vector<int>(2 * sum + 1, 0));
possible_sums[0][sum + nums[0]]++;
possible_sums[0][sum - nums[0]]++;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j <= 2 * sum; j++) {
if (j >= nums[i])
possible_sums[i][j] += possible_sums[i - 1][j - nums[i]];
if (j <= 2 * sum - nums[i])
possible_sums[i][j] += possible_sums[i - 1][j + nums[i]];
}
}
return possible_sums.back()[target + sum];
}
};
注意到,如果将结果中的元素分为正、负,那么目标值可以表示为正和-负和,即总和-2*负和。因此在计算完总和后,只需要统计有多少种方式选出特定的元素,其和等于负和即可
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (target < -sum || target > sum) return 0;
if ((sum - target) % 2) return 0;
sum = (sum - target) / 2;
vector<int> possible_sums(sum + 1, 0);
possible_sums[0]++;
for (int i = 0; i < nums.size(); i++) {
for (int j = sum; j >= nums[i]; j--) {
possible_sums[j] += possible_sums[j - nums[i]];
}
}
return possible_sums[sum];
}
};
474. 一和零
二维的 0-1 背包问题
class Solution {
public:
void count(const string &str, int &m, int &n) {
m = 0, n = 0;
for (const char c : str)
if (c == '0') m++;
else n++;
}
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
int cm, cn;
for (const string &str : strs) {
count(str, cm, cn);
for (int i = m; i >= cm; i--)
for (int j = n; j >= cn; j--)
dp[i][j] = max(dp[i][j], dp[i - cm][j - cn] + 1);
}
return dp[m][n];
}
};
518. 零钱兑换 II
0-n 背包问题
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (const int &coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
};
377. 组合总和 IV
0-n 背包问题的衍生问题:打乱顺序?
调整两层循环的顺序:对于不同的目标值,每次考虑当前序列的最后一个元素(求排列)。注意:该算法无法应对重复元素(去重)
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (const int &num: nums) {
if (i >= num) dp[i] += dp[i - num];
}
}
return dp[target];
}
};
补充:原来的循环顺序:对于不同的元素,每次考虑与之前的元素结合组成目标值(求组合)
70. 爬楼梯(进阶版)
同组合总和 IV
class Solution {
public:
int climbStairs(int n, int m) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (i >= j) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
322. 零钱兑换
统计最大数量的 0-n 背包问题
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX - 1);
dp[0] = 0;
for (const int &coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = min(dp[i - coin] + 1, dp[i]);
}
}
return dp[amount] < INT_MAX - 1 ? dp[amount] : -1;
}
};
279. 完全平方数
统计最大数量的 0-n 背包问题
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX - 1);
dp[0] = 0;
int sq;
for (int i = 0;; i++) {
if ((sq = i * i) > n) break;
for (int j = sq; j <= n; j++) {
dp[j] = min(dp[j], dp[j - sq] + 1);
}
}
return dp[n];
}
};
139. 单词拆分
类似于背包问题,添加前提——字符串匹配
class Solution {
public:
struct Trie {
bool terminal = false;
unordered_map<char, Trie *> children;
void addPath(const string &word) {
Trie *tmp = this;
for (const char c : word) {
if (!tmp->children.count(c)) tmp->children[c] = new Trie();
tmp = tmp->children[c];
}
tmp->terminal = true;
}
};
bool matchDict(const string &s, int i, int j, Trie *trie) {
for (int k = i; k < j; k++) {
if (!trie->children.count(s[k])) return false;
trie = trie->children[s[k]];
}
return trie->terminal;
}
bool wordBreak(string s, vector<string>& wordDict) {
Trie *trie = new Trie();
for (const string &word : wordDict) trie->addPath(word);
vector<bool> match(s.size() + 1, false); // s[0..i) can be segmented
match[0] = true;
for (int j = 1; j <= s.size(); j++) {
for (int i = 0; i < j; i++) {
match[j] = match[i] && matchDict(s, i, j, trie);
if (match[j]) break;
}
}
return match.back();
}
};
考虑到字符串匹配的特殊性(跳过大多数匹配),可以尝试直接广度优先搜索(带剪枝)
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
Trie *trie = new Trie();
for (const string &word : wordDict) trie->addPath(word);
vector<bool> checked(s.size(), false);
queue<int> matchLen;
matchLen.push(0);
while (!matchLen.empty()) {
int idx = matchLen.front();
printf("%d\n", idx);
matchLen.pop();
// Insert all matching substrings into the queue
Trie *tmp = trie;
while (idx < s.size()) {
char c = s[idx++];
if (!tmp->children.count(c)) break;
tmp = tmp->children[c];
if (tmp->terminal) {
if (idx == s.size()) return true;
if (!checked[idx]) {
matchLen.push(idx);
checked[idx] = true;
}
}
}
}
return false;
}
};