分組揹包、完全揹包

n1ce2cv發表於2024-10-21

分組揹包、完全揹包

分組揹包:多個物品分組,每組只能取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;
}

相關文章