01揹包、有依賴的揹包

n1ce2cv發表於2024-10-20

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;
}

相關文章