01揹包、有依賴的揹包
P1048 [NOIP2005 普及組] 採藥
01揹包(模版)
給定一個正數 t,表示揹包的容量
有 m 個貨物,每個貨物可以選擇一次
每個貨物有自己的體積 costs[i] 和價值 values[i]
返回在不超過總容量的情況下,怎麼挑選貨物能達到價值最大
返回最大的價值
- 二維 dp 陣列
#include <iostream>
#include <vector>
using namespace std;
int bag(vector<int> &cost, vector<int> &value, int t, int n) {
// dp[i][j] 表示在前 i 種物品中選擇,總代價不超過 j 的情況下,能獲得的最大價值
vector<vector<int>> dp(n + 1, vector<int>(t + 1));
// 第一行為 0
fill(dp[0].begin(), dp[0].end(), 0);
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= t; ++j) {
// 不選 i 號物品,則最大價值和在前 i - 1 個物品中選,總代價不超過 j 的情況下能獲得的最大價值一樣
dp[i][j] = dp[i - 1][j];
// 選 i 號物品,價值就是 i 號物品的價值加上在前 i - 1 個物品中選,總代價不超過 j - cost[i] 的情況下能獲得的最大價值
if (j - cost[i] >= 0)
dp[i][j] = max(dp[i][j], dp[i - 1][j - cost[i]] + value[i]);
}
}
// 返回在 n 種物品中選擇,總代價不超過 t 的情況下,能獲得的最大價值
return dp[n][t];
}
int main() {
// t 為揹包容量,n 為物品種數
int t, n;
cin >> t >> n;
// 物品從 1 編號
vector<int> cost(n + 1);
vector<int> value(n + 1);
for (int i = 1; i <= n; ++i)
cin >> cost[i] >> value[i];
cout << bag(cost, value, t, n);
}
- 空間壓縮
#include <iostream>
#include <vector>
using namespace std;
int bag(vector<int> &cost, vector<int> &value, int t, int n) {
// 原矩陣的每一行,逐行往下,第一行為 0
vector<int> dp(t + 1, 0);
for (int i = 1; i <= n; ++i)
// 如果從左往右的話就需要兩個一維陣列了,因為從左往右的過程中會覆蓋掉 dp[j - cost[i]]
// 從右往左可以避免這個問題
// dp[j] 繼承自上一行的 dp[j] 不變,表示不選 i 號物品
// 或者選 i 號物品,價值就是 i 號物品的價值加上在前 i - 1 個物品中選,總代價不超過 j - cost[i] 的情況下能獲得的最大價值
// dp[j - cost[i]] 也來自上一行
for (int j = t; j - cost[i] >= 0; j--)
dp[j] = max(dp[j], dp[j - cost[i]] + value[i]);
// 返回在 n 種物品中選擇,總代價不超過 t 的情況下,能獲得的最大價值
return dp[t];
}
int main() {
// t 為揹包容量,n 為物品種數
int t, n;
cin >> t >> n;
// 物品從 1 編號
vector<int> cost(n + 1);
vector<int> value(n + 1);
for (int i = 1; i <= n; ++i)
cin >> cost[i] >> value[i];
cout << bag(cost, value, t, n);
}
bytedance-006. 夏季特惠
#include <iostream>
#include <vector>
using namespace std;
#define ll long long
// 返回在 n 種物品中選擇,總代價不超過 x 的情況下,能獲得的最大價值
ll bag(vector<int> &cost, vector<ll> &value, int x) {
int n = cost.size() - 1;
vector<ll> dp(x + 1, 0);
for (int i = 1; i <= n; ++i)
for (int j = x; j - cost[i] >= 0; j--)
dp[j] = max(dp[j], dp[j - cost[i]] + value[i]);
return dp[x];
}
int main() {
// x 為預算
int n, x;
cin >> n >> x;
// 待考慮的商品,下標從 1 開始
vector<int> cost(1);
// 快樂值
vector<ll> value(1);
// 獲得的快樂值
ll res = 0;
int before, now;
ll happy;
for (int i = 1; i <= n; ++i) {
cin >> before >> now >> happy;
int val = before - now - now;
if (val > 0) {
// 優惠的錢比購買價格還高,一定購買,會使心裡預算增加
x += val;
res += happy;
} else {
cost.emplace_back(-val);
value.emplace_back(happy);
}
}
cout << res + bag(cost, value, x);
}
494. 目標和
- 暴力遞迴
#include <vector>
using namespace std;
class Solution {
public:
int res;
// 暴力遞迴
void recursive(vector<int> &nums, int target, int curIndex, int sum) {
if (curIndex == nums.size()) {
if (sum == target) res++;
return;
}
recursive(nums, target, curIndex + 1, sum + nums[curIndex]);
recursive(nums, target, curIndex + 1, sum - nums[curIndex]);
}
int findTargetSumWays(vector<int> &nums, int target) {
res = 0;
recursive(nums, target, 0, 0);
return res;
}
};
- 帶返回值的暴力遞迴
#include <vector>
using namespace std;
class Solution {
public:
int recursive(vector<int> &nums, int target, int curIndex, int sum) {
if (curIndex == nums.size())
return sum == target ? 1 : 0;
return recursive(nums, target, curIndex + 1, sum + nums[curIndex])
+ recursive(nums, target, curIndex + 1, sum - nums[curIndex]);
}
int findTargetSumWays(vector<int> &nums, int target) {
return recursive(nums, target, 0, 0);
}
};
- 記憶化搜尋
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
unordered_map<int, unordered_map<int, int>> dp;
// 記憶化搜尋版
// 本來使用 dp[curIndex][sum] 記錄,但是 sum 可能是負數
// 所以用二級雜湊表模擬
int recursive(vector<int> &nums, int target, int curIndex, int sum) {
if (curIndex == nums.size())
return sum == target ? 1 : 0;
if (dp.find(curIndex) != dp.end() && dp[curIndex].find(sum) != dp[curIndex].end())
return dp[curIndex][sum];
int res = recursive(nums, target, curIndex + 1, sum + nums[curIndex])
+ recursive(nums, target, curIndex + 1, sum - nums[curIndex]);
dp[curIndex].emplace(sum, res);
return res;
}
int findTargetSumWays(vector<int> &nums, int target) {
return recursive(nums, target, 0, 0);
}
};
- 嚴格位置依賴的動態規劃
#include <vector>
using namespace std;
class Solution {
public:
// todo
int findTargetSumWays(vector<int> &nums, int target) {
int s = 0;
for (const auto &item: nums)
s += item;
// 不在範圍內,湊不出來
if (target < -s || target > s) return 0;
int n = nums.size();
int m = 2 * s + 1;
// 原本的 dp[i][j] 含義:
// nums[0, i-1] 範圍上,已經形成的累加和是 sum
// 為了避免 sum 為負數的情況,dp[i][j] 平移為 dp[i][j + s]
vector<vector<int>> dp(n + 1, vector<int>(m));
dp[n][target + s] = 1;
for (int i = n - 1; i >= 0; i--) {
for (int j = -s; j <= s; j++) {
if (j + nums[i] + s < m)
dp[i][j + s] = dp[i + 1][j + nums[i] + s];
if (j - nums[i] + s >= 0)
dp[i][j + s] += dp[i + 1][j - nums[i] + s];
}
}
return dp[0][s];
}
};
- 01 揹包
#include <vector>
using namespace std;
class Solution {
public:
// 求非負陣列 nums 有多少個子序列累加和是 t
// 01 揹包問題(子集累加和嚴格是 t) + 空間壓縮
// dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
int subsets(vector<int> &nums, int t) {
if (t < 0) return 0;
vector<int> dp(t + 1, 0);
dp[0] = 1;
for (int num: nums)
for (int j = t; j - num >= 0; j--)
dp[j] += dp[j - num];
return dp[t];
}
// 比如說給定一個陣列, nums = [1, 2, 3, 4, 5] 並且 target = 3
// 其中一個方案是 : +1 -2 +3 -4 +5 = 3
// 該方案中取了正的集合為A = {1,3,5}
// 該方案中取了負的集合為B = {2,4}
// 所以任何一種方案,都一定有 sum(A) - sum(B) = target
// 現在我們來處理一下這個等式,把左右兩邊都加上sum(A) + sum(B),那麼就會變成如下:
// sum(A) - sum(B) + sum(A) + sum(B) = target + sum(A) + sum(B)
// 2 * sum(A) = target + 陣列所有數的累加和
// sum(A) = (target + 陣列所有數的累加和) / 2
// 也就是說,任何一個集合,只要累加和是(target + 陣列所有數的累加和) / 2
// 那麼就一定對應一種target的方式
// 比如非負陣列nums,target = 1, nums所有數累加和是11
// 求有多少方法組成1,其實就是求,有多少種子集累加和達到6的方法,(1+11)/2=6
// 因為,子集累加和6 - 另一半的子集累加和5 = 1(target)
// 所以有多少個累加和為6的不同集合,就代表有多少個target==1的表示式數量
// 至此已經轉化為01揹包問題了
int findTargetSumWays(vector<int> &nums, int target) {
int sum = 0;
for (int num: nums) sum += num;
// 範圍外湊不出 target,奇偶性不一致也湊不出
if (target < -sum || sum < target || ((target & 1) ^ (sum & 1)) == 1) return 0;
return subsets(nums, (target + sum) >> 1);
}
};
1049. 最後一塊石頭的重量 II
#include <vector>
using namespace std;
class Solution {
public:
// 非負陣列 nums 中,子序列累加和不超過 t,但是最接近 t 的累加和是多少
// 01 揹包問題(子集累加和儘量接近 t) + 空間壓縮
int getNear(vector<int> &nums, int t) {
vector<int> dp(t + 1);
for (int num: nums)
for (int j = t; j - num >= 0; j--)
// dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])
dp[j] = max(dp[j], dp[j - num] + num);
return dp[t];
}
int lastStoneWeightII(vector<int> &stones) {
int sum = 0;
for (int num: stones)
sum += num;
// nums 中隨意選擇數字,累加和一定要 <= sum / 2,又儘量接近
int near = getNear(stones, sum / 2);
return sum - near - near;
}
};
P1064 [NOIP2006 提高組] 金明的預算方案
有依賴的揹包(模版)
#include <vector>
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 代價
vector<int> cost(m + 1);
// 收益
vector<int> value(m + 1);
// 是否是主商品
vector<bool> king(m + 1);
// 主商品的附屬商品
vector<vector<int>> follows(m + 1);
// 編號從 1 開始
for (int i = 1, v, p, q; i <= m; ++i) {
cin >> v >> p >> q;
cost[i] = v;
value[i] = v * p;
king[i] = q == 0;
if (q != 0) follows[q].emplace_back(i);
}
// dp[i][j] 表示前 i 個商品中,只關心主商品,並且進行展開,花費不超過 j 的情況下,獲得的最大收益
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// 上次展開的主商品編號
int pre = 0;
for (int i = 1, fan1, fan2; i <= m; ++i) {
// 跳過附屬商品
if (!king[i]) continue;
for (int j = 0; j <= n; ++j) {
// 可能性1: 不考慮當前主商品
dp[i][j] = dp[pre][j];
// 可能性2: 考慮當前主商品,只要主
if (j - cost[i] >= 0)
dp[i][j] = max(dp[i][j],
dp[pre][j - cost[i]] + value[i]);
// fan1: 如果有附 1 商品,編號給 fan1,如果沒有,fan1 == -1
// fan2: 如果有附 2 商品,編號給 fan2,如果沒有,fan2 == -1
fan1 = follows[i].size() >= 1 ? follows[i][0] : -1;
fan2 = follows[i].size() >= 2 ? follows[i][1] : -1;
// 可能性3: 主 + 附1
if (fan1 != -1 && j - cost[i] - cost[fan1] >= 0)
dp[i][j] = max(dp[i][j],
dp[pre][j - cost[i] - cost[fan1]] + value[i] + value[fan1]);
// 可能性4: 主 + 附2
if (fan2 != -1 && j - cost[i] - cost[fan2] >= 0)
dp[i][j] = max(dp[i][j],
dp[pre][j - cost[i] - cost[fan2]] + value[i] + value[fan2]);
// 可能性5: 主 + 附1 + 附2
if (fan1 != -1 && fan2 != -1 && j - cost[i] - cost[fan1] - cost[fan2] >= 0)
dp[i][j] = max(dp[i][j],
dp[pre][j - cost[i] - cost[fan1] - cost[fan2]] + value[i] + value[fan1] + value[fan2]);
}
pre = i;
}
cout << dp[pre][n];
}
- 空間壓縮
#include <vector>
#include <iostream>
using namespace std;
int main() {
// m 種商品,總金額 n
int n, m;
cin >> n >> m;
// 代價
vector<int> cost(m + 1);
// 收益
vector<int> value(m + 1);
// 是否是主商品
vector<bool> king(m + 1);
// 主商品的附屬商品
vector<vector<int>> follows(m + 1);
// 編號從 1 開始
for (int i = 1, v, p, q; i <= m; ++i) {
cin >> v >> p >> q;
cost[i] = v;
value[i] = v * p;
king[i] = q == 0;
if (q != 0) follows[q].emplace_back(i);
}
// dp[i][j] 表示前 i 個商品中,只關心主商品,並且進行展開,花費不超過 j 的情況下,獲得的最大收益
// 首行為 0
vector<int> dp(n + 1, 0);
for (int i = 1, fan1, fan2; i <= m; ++i) {
// 跳過附屬商品
if (!king[i]) continue;
// 從右往左
for (int j = n; j - cost[i] >= 0; j--) {
// 可能性1: 不考慮當前主商品
// 可能性2: 考慮當前主商品,只要主
dp[j] = max(dp[j], dp[j - cost[i]] + value[i]);
fan1 = follows[i].size() >= 1 ? follows[i][0] : -1;
fan2 = follows[i].size() >= 2 ? follows[i][1] : -1;
// 可能性3: 主 + 附1
if (fan1 != -1 && j - cost[i] - cost[fan1] >= 0)
dp[j] = max(dp[j], dp[j - cost[i] - cost[fan1]] + value[i] + value[fan1]);
// 可能性4: 主 + 附2
if (fan2 != -1 && j - cost[i] - cost[fan2] >= 0)
dp[j] = max(dp[j], dp[j - cost[i] - cost[fan2]] + value[i] + value[fan2]);
// 可能性5: 主 + 附1 + 附2
if (fan1 != -1 && fan2 != -1 && j - cost[i] - cost[fan1] - cost[fan2] >= 0)
dp[j] = max(dp[j], dp[j - cost[i] - cost[fan1] - cost[fan2]] + value[i] + value[fan1] + value[fan2]);
}
}
cout << dp[n];
}
非負陣列前k個最小的子序列累加和
非負陣列前k個最小的子序列累加和
給定一個陣列 nums,含有 n 個數字,都是非負數
給定一個正數 k,返回所有子序列中累加和最小的前 k 個累加和
子序列是包含空集的
1 <= n <= 10^5
1 <= nums[i] <= 10^6
1 <= k <= 10^5
注意這個資料量,用 01 揹包的解法是不行的,時間複雜度太高了
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <random>
using namespace std;
// 非負陣列前k個最小的子序列累加和
// 給定一個陣列nums,含有n個數字,都是非負數
// 給定一個正數k,返回所有子序列中累加和最小的前k個累加和
// 子序列是包含空集的
// 1 <= n <= 10^5
// 1 <= nums[i] <= 10^6
// 1 <= k <= 10^5
class Solution {
public:
// 暴力方法
vector<int> topKSum1(vector<int> &nums, int k) {
// 所有子序列的和
vector<int> allSubsequences;
recursive(nums, 0, 0, allSubsequences);
sort(allSubsequences.begin(), allSubsequences.end());
vector<int> res(k);
// 取前 k 個
for (int i = 0; i < k; i++)
res[i] = allSubsequences[i];
return res;
}
// 得到所有子序列的和
void recursive(vector<int> &nums, int curIndex, int sum, vector<int> &res) {
if (curIndex == nums.size()) {
res.push_back(sum);
return;
}
// 不要當前
recursive(nums, curIndex + 1, sum, res);
// 要當前
recursive(nums, curIndex + 1, sum + nums[curIndex], res);
}
// 01 揹包來實現,時間複雜度太差,因為 n 很大,數值也很大,那麼可能的累加和就更大
vector<int> topKSum2(vector<int> &nums, int k) {
int sum = 0;
for (int num: nums) sum += num;
vector<int> dp(sum + 1, 0);
dp[0] = 1;
for (int num: nums)
// 從右往左
for (int j = sum; j - num >= 0; j--)
dp[j] += dp[j - num];
vector<int> res(k);
int index = 0;
for (int j = 0; j <= sum && index < k; j++)
for (int i = 0; i < dp[j] && index < k; i++)
res[index++] = j;
return res;
}
struct cmp {
bool operator()(pair<int, int> &p1, pair<int, int> &p2) {
return p1.second > p2.second;
}
};
// 正式方法
// 用堆來做是最優解,時間複雜度 O(n * log n) + O(k * log k)
vector<int> topKSum3(vector<int> &nums, int k) {
sort(nums.begin(), nums.end());
// <子序列的最右下標,子序列的累加和>,根據累加和遞增
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> heap;
heap.push({0, nums[0]});
vector<int> res(k);
res[0] = 0;
for (int i = 1; i < k; i++) {
pair<int, int> cur = heap.top();
heap.pop();
int right = cur.first;
int sum = cur.second;
// 收集彈出的最小累加和
res[i] = sum;
if (right + 1 < nums.size()) {
// 去掉末尾,加上下個數
heap.push({right + 1, sum - nums[right] + nums[right + 1]});
// 不去掉末尾,加上下個數
heap.push({right + 1, sum + nums[right + 1]});
}
}
return res;
}
// 為了測試
vector<int> randomArray(int len, int value) {
vector<int> ans(len);
random_device rd;
mt19937 gen(rd());
uniform_int_distribution<> dis(0, value);
for (int i = 0; i < len; i++)
ans[i] = dis(gen);
return ans;
}
// 為了測試
bool equals(const vector<int> &ans1, const vector<int> &ans2) {
if (ans1.size() != ans2.size()) return false;
for (int i = 0; i < ans1.size(); i++)
if (ans1[i] != ans2[i])
return false;
return true;
}
};
int main() {
Solution solution;
int n = 15;
int v = 40;
int testTime = 5000;
cout << "測試開始" << endl;
for (int i = 0; i < testTime; i++) {
int len = rand() % n + 1;
vector<int> nums = solution.randomArray(len, v);
int k = rand() % ((1 << len) - 1) + 1;
vector<int> ans1 = solution.topKSum1(nums, k);
vector<int> ans2 = solution.topKSum2(nums, k);
vector<int> ans3 = solution.topKSum3(nums, k);
if (!solution.equals(ans1, ans2) || !solution.equals(ans1, ans3))
cout << "出錯了!" << endl;
}
cout << "測試結束" << endl;
}