分組揹包、完全揹包
分組揹包:多個物品分組,每組只能取1件。每一組的物品都可能性展開就可以了。時間複雜度 O(物品數量 * 揹包容量)
完全揹包:與 01 揹包的區別僅在於每種商品可以選取無限次。時間複雜度 O(物品數量 * 揹包容量)
P1757 通天之分組揹包
- 嚴格位置依賴的動態規劃
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 時間複雜度 O(m * n)
int main() {
// n 件物品,揹包容量 m
int m, n;
cin >> m >> n;
vector<vector<int>> arr(n + 1, vector<int>(3));
for (int i = 1; i <= n; ++i)
cin >> arr[i][0] >> arr[i][1] >> arr[i][2];
// 根據組號排序
sort(begin(arr), end(arr),
[](vector<int> &v1, vector<int> &v2) {
return v1[2] < v2[2];
});
// 組數
int teams = 1;
for (int i = 2; i <= n; ++i)
if (arr[i][2] != arr[i - 1][2]) teams++;
// dp[i][j] 表示從前面 i 個組中選取(每個組最多選一個),總重量不超過 j 時能獲得的最大收益
vector<vector<int>> dp(teams + 1, vector<int>(m + 1));
// 首行為 0
fill(begin(dp[0]), end(dp[0]), 0);
for (int l = 1, r = 2, i = 1; l <= n; ++i) {
// r 移動到下一組的開頭
while (r <= n && arr[l][2] == arr[r][2]) r++;
// [l, r-1] 為當前組的物品
for (int j = 0; j <= m; ++j) {
// 一個都不選
dp[i][j] = dp[i - 1][j];
// 嘗試選則組內的每一個
for (int k = l; k < r; ++k)
if (j - arr[k][0] >= 0)
dp[i][j] = max(dp[i][j], dp[i - 1][j - arr[k][0]] + arr[k][1]);
}
// 處理下一組
l = r++;
}
cout << dp[teams][m];
}
- 空間最佳化
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 時間複雜度 O(m * n)
int main() {
// n 件物品,揹包容量 m
int m, n;
cin >> m >> n;
vector<vector<int>> arr(n + 1, vector<int>(3));
for (int i = 1; i <= n; ++i)
cin >> arr[i][0] >> arr[i][1] >> arr[i][2];
// 根據組號排序
sort(begin(arr), end(arr),
[](vector<int> &v1, vector<int> &v2) {
return v1[2] < v2[2];
});
// 組數
int teams = 1;
for (int i = 2; i <= n; ++i)
if (arr[i][2] != arr[i - 1][2]) teams++;
// dp[i][j] 表示從前面 i 個組中選取(每個組最多選一個),總重量不超過 j 時能獲得的最大收益
// 首行為 0
vector<int> dp(m + 1, 0);
for (int l = 1, r = 2, i = 1; l <= n; ++i) {
// r 移動到下一組的開頭,[l, r-1] 為當前組的物品
while (r <= n && arr[l][2] == arr[r][2]) r++;
// 從右往左
for (int j = m; j >= 0; --j) {
// 一個都不選,或者嘗試選則組內的每一個
for (int k = l; k < r; ++k)
if (j - arr[k][0] >= 0)
dp[j] = max(dp[j], dp[j - arr[k][0]] + arr[k][1]);
}
// 處理下一組
l = r++;
}
cout << dp[m];
}
2218. 從棧中取出 K 個硬幣的最大面值和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxValueOfCoins(vector<vector<int>> &piles, int m) {
// 組數
int n = piles.size();
// dp[i][j] 表示前 i 組上,一共拿走 j 個硬幣的情況下,獲得的最大價值
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
// 組號從 1 開始
for (int i = 1; i <= n; ++i) {
// 當前組
vector<int> team = piles[i - 1];
// 計算字首和
int t = min((int) team.size(), m);
vector<int> preSum(t + 1);
for (int j = 1; j <= t; ++j)
preSum[j] = preSum[j - 1] + team[j - 1];
// 更新動態規劃表
for (int j = 0; j <= m; ++j) {
// 當前組一個硬幣也不拿
dp[i][j] = dp[i - 1][j];
// i 組裡拿走前 k 個硬幣的方案
for (int k = 1; k <= min(t, j); ++k)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + preSum[k]);
}
}
return dp[n][m];
}
};
- 空間最佳化
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxValueOfCoins(vector<vector<int>> &piles, int m) {
// 組數
int n = piles.size();
// dp[i][j] 表示前 i 組上,一共拿走 j 個硬幣的情況下,獲得的最大價值
// 首行全 0
vector<int> dp(m + 1, 0);
// 組號從 1 開始
for (int i = 1; i <= n; ++i) {
// 當前組
vector<int> team = piles[i - 1];
// 計算字首和
int t = min((int) team.size(), m);
vector<int> preSum(t + 1);
for (int j = 1; j <= t; ++j)
preSum[j] = preSum[j - 1] + team[j - 1];
// 更新動態規劃表
for (int j = m; j >= 0; j--) {
// 當前組一個硬幣也不拿
// i 組裡拿走前 k 個硬幣的方案
for (int k = 1; k <= min(t, j); ++k)
dp[j] = max(dp[j], dp[j - k] + preSum[k]);
}
}
return dp[m];
}
};
P1616 瘋狂的採藥
完全揹包(模版)
給定一個正數 t,表示揹包的容量
有 m 種貨物,每種貨物可以選擇任意個
每種貨物都有體積 costs[i] 和價值 values[i]
返回在不超過總容量的情況下,怎麼挑選貨物能達到價值最大
返回最大的價值
- 嚴格位置依賴的動態規劃
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
int main() {
int t, m;
cin >> t >> m;
vector<int> cost(m + 1);
vector<int> value(m + 1);
for (int i = 1; i <= m; ++i)
cin >> cost[i] >> value[i];
// dp[i][j] 表示前 i 號物品,每種可以無限拿,總代價不超過 j 的情況下,能獲得的最大價值
vector<vector<ll>> dp(m + 1, vector<ll>(t + 1));
for (int i = 1; i <= m; ++i) {
for (int j = 0; j <= t; ++j) {
// 當前物品一個都不拿
dp[i][j] = dp[i - 1][j];
// 當前物品拿若干個
if (j - cost[i] >= 0)
dp[i][j] = max(dp[i][j], dp[i][j - cost[i]] + value[i]);
}
}
cout << dp[m][t];
}
- 空間壓縮
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
int main() {
int t, m;
cin >> t >> m;
vector<int> cost(m + 1);
vector<int> value(m + 1);
for (int i = 1; i <= m; ++i)
cin >> cost[i] >> value[i];
// dp[i][j] 表示前 i 號物品,每種可以無限拿,總代價不超過 j 的情況下,能獲得的最大價值
// 首行全 0
vector<ll> dp(t + 1, 0);
for (int i = 1; i <= m; ++i) {
// 從左往右
for (int j = cost[i]; j <= t; ++j) {
// 當前物品一個都不拿
// 當前物品拿若干個
dp[j] = max(dp[j], dp[j - cost[i]] + value[i]);
}
}
cout << dp[t] << endl;
}
10. 正規表示式匹配
- 暴力遞迴
#include <iostream>
using namespace std;
class Solution {
public:
// s 從 i 下標開始能不能被 p 從 j 下標開始完全匹配出來
bool recursive(string s, string p, int i, int j) {
// 1. s 沒字尾
if (i == s.length()) {
// 同時 p 也沒字尾
if (j == p.length()) return true;
// p 還剩下一些字尾
// 如果 p[j+1] 是 *,那麼 p[j, j+1]可以消掉,然後看看再剩下的是不是都能消掉
return j + 1 < p.length() && p[j + 1] == '*' && recursive(s, p, i, j + 2);
}
// 2. s 有字尾,p 沒字尾
if (j == p.length()) return false;
// 3. 都有字尾
// 3.1 j+1 位置不是 *
if (j + 1 == p.length() || p[j + 1] != '*')
return (s[i] == p[j] || p[j] == '.') && recursive(s, p, i + 1, j + 1);
// 3.2 j+1 位置是 *
// 完全揹包
// 選擇1: 當前 p[j, j+1] 變成空,用 p 的 j+2 位置開始和 s 的 i 位置匹配
bool p1 = recursive(s, p, i, j + 2);
// 選擇2: (s[i] == p[j] || p[j] == '.') 時,當前 p[j..j+1] 消去 s[i]
bool p2 = (s[i] == p[j] || p[j] == '.') && recursive(s, p, i + 1, j);
return p1 || p2;
}
bool isMatch(string s, string p) {
return recursive(s, p, 0, 0);
}
};
- 記憶化搜尋
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
// s 從 i 下標開始能不能被 p 從 j 下標開始完全匹配出來
bool recursive(string s, string p, int i, int j, vector<vector<int>> &dp) {
if (dp[i][j] != 0) return dp[i][j] == 1;
bool res;
if (i == s.length()) {
// 1. s 沒字尾
// 同時 p 也沒字尾
if (j == p.length()) {
res = true;
} else {
// p 還剩下一些字尾
// 如果 p[j+1] 是 *,那麼 p[j, j+1]可以消掉,然後看看再剩下的是不是都能消掉
res = j + 1 < p.length() && p[j + 1] == '*' && recursive(s, p, i, j + 2, dp);
}
} else if (j == p.length()) {
// 2. s 有字尾,p 沒字尾
res = false;
} else {
if (j + 1 == p.length() || p[j + 1] != '*') {
// 3. 都有字尾
// 3.1 j+1 位置不是 *
return (s[i] == p[j] || p[j] == '.') && recursive(s, p, i + 1, j + 1, dp);
} else {
// 3.2 j+1 位置是 *
// 完全揹包
// 選擇1: 當前 p[j, j+1] 變成空,用 p 的 j+2 位置開始和 s 的 i 位置匹配
bool p1 = recursive(s, p, i, j + 2, dp);
// 選擇2: (s[i] == p[j] || p[j] == '.') 時,當前 p[j..j+1] 消去 s[i]
bool p2 = (s[i] == p[j] || p[j] == '.') && recursive(s, p, i + 1, j, dp);
res = p1 || p2;
}
}
dp[i][j] = res ? 1 : 2;
return res;
}
bool isMatch(string s, string p) {
// 記憶化搜尋
// dp[i][j] == 0,表示沒算過
// dp[i][j] == 1,表示算過,答案是 true
// dp[i][j] == 2,表示算過,答案是 false
vector<vector<int>> dp(s.size() + 1, vector<int>(p.size() + 1, 0));
return recursive(s, p, 0, 0, dp);
}
};
- 嚴格位置依賴的動態規劃
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
// 根據遞迴改寫
bool isMatch(string s, string p) {
int n = s.length();
int m = p.length();
vector<vector<bool>> dp(s.size() + 1, vector<bool>(p.size() + 1));
dp[n][m] = true;
for (int j = m - 1; j >= 0; j--)
dp[n][j] = j + 1 < m && p[j + 1] == '*' && dp[n][j + 2];
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
if (j + 1 == m || p[j + 1] != '*') {
dp[i][j] = (s[i] == p[j] || p[j] == '.') && dp[i + 1][j + 1];
} else {
dp[i][j] = dp[i][j + 2] || ((s[i] == p[j] || p[j] == '.') && dp[i + 1][j]);
}
}
}
return dp[0][0];
}
};
44. 萬用字元匹配
- 暴力遞迴
#include <iostream>
#include <vector>
using namespace std;
// '?' 表示可以變成任意字元,數量 1 個
// '*' 表示可以匹配任何字串
class Solution {
public:
bool recursive(string s, string p, int i, int j) {
// 1. s 沒字尾
if (i == s.length()) {
// 同時 p 也沒字尾
if (j == p.length()) return true;
// p 還剩下一些字尾
// 如果 p[j] 是 *,那麼 p[j]可以消掉,然後看看再剩下的是不是都能消掉
return p[j] == '*' && recursive(s, p, i, j + 1);
}
// 2. s 有字尾,p 沒字尾
if (j == p.length()) return false;
// 3. 都有字尾
// 3.1 j 位置不是 *,那麼當前的字元必須能匹配
if (p[j] != '*') return (s[i] == p[j] || p[j] == '?') && recursive(s, p, i + 1, j + 1);
// 3.2 j 位置是 *,可以選擇消掉或者不消掉 s[i]
return recursive(s, p, i + 1, j) || recursive(s, p, i, j + 1);
}
bool isMatch(string s, string p) {
return recursive(s, p, 0, 0);
}
};
- 記憶化搜尋
#include <iostream>
#include <vector>
using namespace std;
// '?' 表示可以變成任意字元,數量 1 個
// '*' 表示可以匹配任何字串
class Solution {
public:
bool recursive(string s, string p, int i, int j, vector<vector<int>> &dp) {
if (dp[i][j] != 0) return dp[i][j] == 1;
bool res;
if (i == s.length()) {
if (j == p.length()) {
res = true;
} else {
res = p[j] == '*' && recursive(s, p, i, j + 1, dp);
}
} else if (j == p.length()) {
res = false;
} else {
if (p[j] != '*') {
res = (s[i] == p[j] || p[j] == '?') && recursive(s, p, i + 1, j + 1, dp);
} else {
res = recursive(s, p, i + 1, j, dp) || recursive(s, p, i, j + 1, dp);
}
}
dp[i][j] = res ? 1 : 2;
return res;
}
bool isMatch(string s, string p) {
vector<vector<int>> dp(s.length() + 1, vector<int>(p.length() + 1));
return recursive(s, p, 0, 0, dp);
}
};
- 嚴格位置依賴
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.length();
int m = p.length();
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1));
dp[n][m] = true;
for (int j = m - 1; j >= 0 && p[j] == '*'; j--)
dp[n][j] = true;
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
if (p[j] != '*') {
dp[i][j] = (s[i] == p[j] || p[j] == '?') && dp[i + 1][j + 1];
} else {
dp[i][j] = dp[i + 1][j] || dp[i][j + 1];
}
}
}
return dp[0][0];
}
};
P2918 [USACO08NOV] Buying Hay S
#include <iostream>
#include <vector>
using namespace std;
// 購買足量乾草的最小花費
// 有 n 個提供乾草的公司,每個公司都有兩個資訊
// cost[i] 代表購買 1 次產品需要花的錢
// val[i] 代表購買 1 次產品所獲得的乾草數量
// 每個公司的產品都可以購買任意次
// 你一定要至少購買 h 數量的乾草,返回最少要花多少錢
int main() {
int n, h;
cin >> n >> h;
vector<int> cost(n + 1);
vector<int> value(n + 1);
int _max = 0;
for (int i = 1; i <= n; ++i) {
cin >> value[i] >> cost[i];
_max = max(_max, value[i]);
}
// 往外擴充
int m = h + _max;
// dp[i][j] 表示前 i 個公司裡挑公司,購買嚴格 j 磅乾草,需要的最少花費
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
// 首行為最大值表示沒有解
fill(dp[0].begin() + 1, dp[0].end(), 0x7fffffff);
// 除了 dp[0][0] 是 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
dp[i][j] = dp[i - 1][j];
if (j - value[i] >= 0 && dp[i][j - value[i]] != 0x7fffffff)
dp[i][j] = min(dp[i][j], dp[i][j - value[i]] + cost[i]);
}
}
int res = 0x7fffffff;
// 至少購買 h 數量的乾草,返回最少要花多少錢
for (int j = h; j <= m; ++j)
res = min(res, dp[n][j]);
cout << res << endl;
}
- 空間最佳化
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, h;
cin >> n >> h;
vector<int> cost(n + 1);
vector<int> value(n + 1);
int _max = 0;
for (int i = 1; i <= n; ++i) {
cin >> value[i] >> cost[i];
_max = max(_max, value[i]);
}
// 往外擴充
int m = h + _max;
// dp[i][j] 表示前 i 個公司裡挑公司,購買嚴格 j 磅乾草,需要的最少花費
vector<int> dp(m + 1, 0x7fffffff);
// 首行除了 dp[0][0] 是 0,其他都為最大值表示沒有解
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = value[i]; j <= m; ++j) {
if (dp[j - value[i]] != 0x7fffffff)
dp[j] = min(dp[j], dp[j - value[i]] + cost[i]);
}
}
int res = 0x7fffffff;
// 至少購買 h 數量的乾草,返回最少要花多少錢
for (int j = h; j <= m; ++j)
res = min(res, dp[j]);
cout << res << endl;
}