【DP学习总结】背包例题


大佬的博客!!!推荐!!!

1. Steadily Growing Steam

题目链接:2021上海区域赛 I题

题意:
N N N个物品,每个物品的体积为 t i t_i ti,价值为 v i v_i vi。在选择物品之前,你可以选择 0 ∼ k 0 \sim k 0k个物品的体积翻倍,然后需要在这 N N N个物品中选出若干个物品分成 A A A, B B B两堆,使得这 A A A B B B的体积相同,并且价值之和最大。求这个价值的最大值。
1 ≤ n ≤ 100 , t i ≤ 13 , 0 ≤ k ≤ n , ∣ v i ∣ ≤ 1 0 9 1 \le n \le 100,t_i \le 13,0 \le k \le n, |v_i| \le 10^9 1n100ti130knvi109

solution:
01背包稍微变化:
背包的体积为设为 N ∗ 26 N * 26 N26
如果放入 A A A堆里,体积为负数;如果放入 B B B堆里,体积为正数。
答案就是求体积为 N ∗ 13 N * 13 N13的背包的最大价值

  • 状态定义: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]:表示选了前 i i i个物品,有 j j j个物品体积翻倍,背包体积为 k k k的所有方案价值的最大值
  • 状态计算:对于第 i i i个物品:
    1.不放, f [ i ] [ j ] [ k ] = f [ i − 1 ] [ j ] [ k ] f[i][j][k] = f[i - 1][j][k] f[i][j][k]=f[i1][j][k]
    2.放入 A A A f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ j ] [ k ] , f [ i − 1 ] [ j ] [ k − v i ] + w i ) f[i][j][k] = max(f[i][j][k], f[i - 1][j][k - v_i] + w_i) f[i][j][k]=max(f[i][j][k],f[i1][j][kvi]+wi)
    3.放入 B B B f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ j ] [ k ] , f [ i − 1 ] [ j ] [ k + v i ] + w i ) f[i][j][k] = max(f[i][j][k], f[i - 1][j][k + v_i] + w_i) f[i][j][k]=max(f[i][j][k],f[i1][j][k+vi]+wi)
    4.翻倍放入 A A A f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ j ] [ k ] , f [ i − 1 ] [ j − 1 ] [ k − 2 v i ] + w i ) f[i][j][k] = max(f[i][j][k], f[i - 1][j - 1][k -2 v_i] + w_i) f[i][j][k]=max(f[i][j][k],f[i1][j1][k2vi]+wi)
    5.翻倍放入 B B B f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ j ] [ k ] , f [ i − 1 ] [ j − 1 ] [ k + 2 v i ] + w i ) f[i][j][k] = max(f[i][j][k], f[i - 1][j - 1][k + 2v_i] + w_i) f[i][j][k]=max(f[i][j][k],f[i1][j1][k+2vi]+wi)

另外,可以利用滚动数组进行优化空间,但好像此题空间刚刚好,不优化也行

Code:

#include <bits/stdc++.h>
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define x first
#define y second
using namespace std;
const int N = 110, M = 50 * 110;
pair<int, int> a[N];
int f[N][N][M];
int n, m;

void Max(int &x, int y) {
    if(x < y) x = y;
}
signed main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; ++i) cin >> a[i].x >> a[i].y;
    memset(f, -0x3f, sizeof f);
    f[0][0][n * 13] = 0;

    for(int i = 1; i <= n; ++ i) {
        for(int j = 0; j <= m; ++ j) {
            for(int k = 0; k <= n * 26; ++ k) {

                Max(f[i][j][k], f[i - 1][j][k]);
                if(k - a[i].y>= 0)
                    Max(f[i][j][k], f[i - 1][j][k - a[i].y] + a[i].x);
                if(k + a[i].y <= n * 26)
                    Max(f[i][j][k], f[i - 1][j][k + a[i].y] + a[i].x);
                if(j > 0) {
                    if(k - 2 * a[i].y >= 0)
                        Max(f[i][j][k], f[i - 1][j - 1][k - 2 * a[i].y] + a[i].x);
                    if(k + 2 * a[i].y <= n * 26)
                        Max(f[i][j][k], f[i - 1][j - 1][k + 2 * a[i].y] + a[i].x);
                }
            }
        }
    }
    int res = 0;
    for(int i = 0; i <= m; ++ i) res = max(res, f[n][i][n * 13]);
    cout << res << "\n";
}

2. 选数

题目链接:Acwing 27周赛C

题意:
n n n个数,现在需要选出 k k k个。让这 k k k个数相乘后末尾零的个数最多

solution:
考虑背包dp:

  • 状态表示:
    d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示前 i i i个物品,选了 j j j个,且 5 5 5的因子数量为 k k k的所有方案中因子 2 2 2的最大值
  • 状态计算:
    不选第 i i i个: d p [ i ] [ j ] [ k ] = d p [ i − 1 ] [ j ] [ k ] dp[i][j][k] = dp[i - 1][j][k] dp[i][j][k]=dp[i1][j][k]
    选第 i i i个:可以看作一个体积为 c n t 5 cnt_5 cnt5,价值为 c n t 2 cnt_2 cnt2的物品放入背包。也就是: d p [ i ] [ j ] [ k ] = m a x ( d p [ i ] [ j ] [ k ] , d p [ i − 1 ] [ j − 1 ] [ k − c n t 5 [ i ] ] + c n t 2 [ i ] ) dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - 1][k - cnt_5[i]] + cnt_2[i]) dp[i][j][k]=max(dp[i][j][k],dp[i1][j1][kcnt5[i]]+cnt2[i])

那么根据dp的定义,最后的结果就是 1 ≤ m ≤ c n t , m i n ( d p [ n ] [ k ] [ m ] , m ) 1 \le m \le cnt, \quad min(dp[n][k][m], m) 1mcnt,min(dp[n][k][m],m)

然后对一维进行滚动数组优化
预处理一下第 i i i个数 2 , 5 2,5 2,5的因子个数

Code:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 210, M = 6010;
int dp[2][N][M];
int cnt_2[N], cnt_5[N], cnt;
int n, m;
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    
    cin >> n >> m;
    for(int i = 1; i <= n; ++ i) {
        int x; cin >> x;
        while(x % 2 == 0) x /= 2, cnt_2[i] ++;
        while(x % 5 == 0) x /= 5, cnt_5[i] ++;
        cnt += cnt_5[i];
    }
    
    memset(dp, -1, sizeof dp);
    dp[0][0][0] = dp[1][0][0] = 0;
    
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= m; ++ j) {
            for(int k = cnt; k >= 0; -- k)
                dp[i & 1][j][k] = dp[i - 1 & 1][j][k];
                
            for(int k = cnt; k >= cnt_5[i]; -- k)
                    if(dp[i - 1 & 1][j - 1][k - cnt_5[i]] >= 0)
                        dp[i & 1][j][k] = max(dp[i & 1][j][k], dp[i - 1 & 1][j - 1][k - cnt_5[i]] + cnt_2[i]);
        }
    }
    
    int res = 0;
    for(int i = 0; i <= cnt; ++ i)
        res = max(res, min(dp[n & 1][m][i], i));
    
    cout << res << "\n";
}

3. 宠物小精灵之收服

题目链接:Acwing 1022

题意:
小智有 N N N个精灵球,皮卡丘有 M M M的体力。现在有 K K K个精灵,每个精灵有两个属性:需要的精灵球和对皮卡丘造成的伤害。
小智的主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。
特别的,皮卡丘体力为0,就不能继续收服精灵了,并且使皮卡丘体力变为0的也不会收服。
求出,最多收服精灵的数量,以及皮卡丘能留下的最大体力

solution:
01背包的性质可以相互转化
例如: f [ i ] f[i] f[i] 可以表示容量为 i i i的最大价值,也可以表示价值为 i i i的最小容量
对于此题:

  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示 皮卡丘受到 i i i伤害,收了 j j j个精灵的所有方案的最小精灵球数
  • 状态计算就是一个二维费用背包的模型了

code:

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 550, S = 105;
int f[M][S]; 
int n, m, k;
int num, harm;
int main()
{
    memset(f, 0x3f, sizeof f);
    cin >> n >> m >> k;
    f[0][0] = 0;
    for(int i = 1; i <= k; ++ i) {
        cin >> num >> harm;
        for(int j = m; j >= harm; -- j) {
            for(int s = k; s >= 1; -- s) {
                if(f[j - harm][s - 1] + num <= n) {
                    f[j][s] = min(f[j][s], f[j - harm][s - 1] + num);
                }
            }
        }
    }
    // find Answer
    for(int s = k; s != -1; --s) {
        int p = 0x3f3f3f3f;
        for(int j = 0; j < m; ++ j) {// 不能等于m,因为使得皮卡丘体力小于等于0的野生小精灵也不会被小智收服。
            if(f[j][s] != 0x3f3f3f3f && j < p) {
                p = j;
            }
        }
        if(p != 0x3f3f3f3f) {
            cout << s << " " << m - p << "\n";
            return 0;
        }
    }
    return 0;
}

4. 数字组合 & 买书

4.1 数字组合
给定 N N N 个正整数 A 1 , A 2 , ⋯   , A N A_1,A_2,\cdots,A_N A1,A2,,AN,从中选出若干个数,使它们的和为 M M M,求有多少种选择方案。
01背包

  • 将总和 M M M看作背包容量, f [ i ] f[i] f[i]表示和为 i i i的方案数;
  • 将每个数 A i A_i Ai看作体积 A i A_i Ai的物品
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int f[N];
int n, m, x;
int main()
{
    cin >> n >> m;
    f[0] = 1;
    for(int i = 1; i <= n; ++i) {
        cin >> x;
        for(int j = m; j >= x; -- j) {
            f[j] += f[j - x];
        }
    }
    cout << f[m] << "\n";
}

4.2 买书
小明手里有 n n n元钱全部用来买书,书的价格为10元,20元,50元,100元。
问小明有多少种买书方案?(每种书可购买多本)
完全背包:

  • f [ i ] f[i] f[i]表示买书恰好 i i i元的方案数
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N];
int a[5] = {0, 10, 20, 50, 100};
int n;
int main()
{
    cin >> n;
    f[0] = 1;
    for(int i = 1; i <= 4; ++ i) {
        for(int j = a[i]; j <= n; ++ j)
            f[j] += f[j - a[i]];
    }
    cout << f[n] << "\n";
}

5. 货币系统

题目链接:Acwing 532

题意:
定义一个货币系统 ( n , a ) (n, a) (n,a):有 n n n种货币分别为 a i a_i ai,每种货币能用无限次,进行组合。
两个货币系统是等价的:对于任意一个数 x x x,要么都可以被这两个货币系统表示出来,要么都不可以表示出来。
现在给出一个货币系统: ( n , a ) (n, a) (n,a),需要找到一个最小的 m m m,货币系统 ( m , b ) (m,b) (m,b)与之等价

solution:

结论:答案中的系统方案一定是由原先的系统方案去掉若干种货币得到
证明:
阿巴阿巴
然后只需找出 ( n , a ) (n,a) (n,a)系统中哪些是多余的就行。比如说系统中有一个2,那么2的倍数就没有存在的必要了

  • 定义: f [ i ] f[i] f[i]表示 i i i有没有必要存在。
  • 计算:类似埃筛,把有必要存在的倍数”筛去“

code:

#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 25010;
int v[N];
bool f[M];
int main()
{
    int t;
    cin >> t;
    while(t -- ) {
        int n;
        cin >> n;
        for(int i = 1; i <= n; ++i) cin >> v[i];
        sort(v + 1,v + 1 + n);
        int m = v[n], res = 0;
        memset(f, false, sizeof f);
        f[0] = true;
        for(int i = 1; i <= n; ++i) {
            if(f[v[i]]) continue;
            res ++;
            for(int j = v[i]; j <= m; ++ j)
                f[j] |= f[j - v[i]];
        }
        cout << res << "\n";
    }
}

6. 机器分配

题目链接:Acwing 1013

题意:
M M M台设备,有 N N N个公司。对于第 i i i个公司,分配 j j j个设备的价值为 w i , j w_{i,j} wi,j。问要如何搭配能获得最大价值。
输出最大价值及分配情况

solution:
解法一:背包dp
求最大价值:
每个公司可以看成一个物品组

  • f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i个物品组,选了 j j j台设备的所有方案的最大价值
  • f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − k ] + w i , k ) ( 0 ≤ k ≤ j ) f[i][j] = max(f[i][j], f[i -1][j-k]+w_{i,k})(0 \le k \le j) f[i][j]=max(f[i][j],f[i1][jk]+wi,k)(0kj)

输出路径:
采用了图论的角度:大佬的题解

code

#include <bits/stdc++.h>
using namespace std;

const int N = 20;

int w[N][N], f[N][N];
int n, m;
int path[N], cnt;

void dfs(int i, int j) 
{
    if(!i) return ;
    //寻找当前状态f[i][j]是从上述哪一个f[i-1][k]状态转移过来的
    for(int a = 0; a <= j; ++ a) {
        if(f[i - 1][j - a] + w[i][a] == f[i][j]) {
            path[cnt++] = a;
            dfs(i - 1, j - a);
            return ;
        }
    }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= m; ++ j) {
            cin >> w[i][j];
        }
    }
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= m; ++ j) {
            for(int k = 0; k <= j; ++ k) {
                f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
            }
        }
    }
    cout << f[n][m] << "\n";
    dfs(n, m);
    for (int i = cnt - 1, j = 1; i >= 0; -- i, j ++) 
        cout << j << " " << path[i] << "\n";
}

解法二:dfs
数据范围小,直接爆搜

#include <bits/stdc++.h>
using namespace std;

const int N = 20;

int n, m;
int a[N][N];
int ans_cnt[N], cnt[N], res;

void dfs(int u, int ans, int k) { // 第u个公司,当前价值ans,还有k个设备
    if(u == n) {// 到最后一个直接全部给他
        ans += a[n][k];
        cnt[n] = k;
        if(ans > res) { // 更新答案
            res = ans;
            for(int i = 1; i <= n; ++ i ) ans_cnt[i] = cnt[i];
        }
        return ;
    }
    for(int i = 0; i <= k; ++ i) {
        cnt[u] = i;
        dfs(u + 1, ans + a[u][i], k - i);
    }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= m; ++ j) {
            cin >> a[i][j];
        }
    }
    dfs(1, 0, m);
    cout << res << "\n";
    for(int i = 1; i <= n; ++ i) cout << i << " " << ans_cnt[i] << "\n";
}

7. 金明的预算方案

题目链接:Acwing 487

题意:
M M M个物品,每个物物品有价格,重要度,主件还是附件三个属性。要买附件就必须先买附件对应的主件。
问:现有 N N N元钱,能买到物品的价格乘重要度的最大值。

solution:
解法一:
有依赖的背包 + 分组背包
对于模板,我们是按照子树的体积来划分的。对于此题,显然是不行的(数据范围来到了 3.2 × 1 0 4 3.2×10^4 3.2×104)
但此题的附件最多只有2个,所以我们直接二进制枚举附件,然后就变成了,分组背包,每组为主件+若干个附件,若干个由二进制枚举得到
时间复杂度: O ( N ∗ 2 2 ∗ M ) O(N*2^2*M) O(N22M)

#include <bits/stdc++.h>
using namespace std;
const int N = 32010, M = 66;
int v[M], w[M];
bool good[M];
vector<int> all[M];
int f[N];
int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; ++ i) {
        int p;
        cin >> v[i] >> w[i] >> p;
        w[i] *= v[i];
        if(p) all[p].push_back(i); // i的主件为p
        else good[i] = true; 
    }
    for(int i = 1; i <= m; ++i) { // 找出主件
        if(good[i]) {
            for(int j = n; j >= 0; -- j) { // 优化成1维逆序dp
                int sz = all[i].size(); // 二进制枚举
                for(int state = 0; state < 1 << sz; ++ state) {
                    int v_ = v[i], w_ = w[i];
                    for(int k = 0; k < sz; ++ k) {
                        if(state >> k & 1) {
                            v_ += v[all[i][k]];
                            w_ += w[all[i][k]];
                        }
                    }
                    if(j - v_ >= 0) f[j] = max(f[j], f[j - v_] + w_);
                }
            }
        }
    }
    cout << f[n] << "\n";
}

解法二:
因为题目规定了附件的数量至多为2,但是不止2的话。上面的二进制枚举的话可能就会TLE。
但是我们知道分组背包的实质,就是在每组里面进行01背包。
仍然定义: f [ i ] [ j ] f[i][j] f[i][j]:考虑前 i i i个,物品体积不超过 j j j的所有方案中的最大值。
计算:如果选择主件,一定要先对主件这个物品进行一次01背包,然后对于他的所有附件,按照体积划分状态,状态一定>0,做一次类似的01背包

时间复杂度: O ( N ∗ M ∗ M ) O(N*M*M) O(NMM)

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 32010, M = 65;

int n, m;
PII good[M];
vector<PII> all[M];
int f[M][N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; ++ i) {
        int v, p, q;
        cin >> v >> p >> q;
        if(q) all[q].push_back({v, v*p});
        else good[i] = {v, v * p};
    }
    
    for(int i = 1; i <= m; ++ i) {
        if(good[i].first) { // 选择主件,
            for(int j = good[i].first; j <= n; ++ j) 
                f[i][j] = max(f[i][j], f[i - 1][j - good[i].first] + good[i].second);
			// 然后在这组里进行01背包,因为以及选了主件,注意状态一定是>0,
            for(int k = 0; k < all[i].size(); ++ k) {
                for(int j = n; j >= all[i][k].first; -- j)
                    if(f[i][j - all[i][k].first])
                        f[i][j] = max(f[i][j], f[i][j - all[i][k].first] + all[i][k].second);
            }
        }
        // 没选主件的时候,记得把状态往下传
         for(int j = n; j >= 0; -- j) f[i][j] = max(f[i][j], f[i - 1][j]);
            
    }
    cout << f[m][n] << "\n";
}

8. 能量石

题目链接:Acwing 734

题意:

N N N块能量石,
每个能量石有 E i E_i Ei的能量,吃掉需要 S i S_i Si的时间,每秒损耗 L i L_i Li的能量
如何吃能得到最大的能量

solution:

贪心+01背包
去掉损失的话,是一个01背包模板题
存在损失的话,就需要思考一下吃的顺序,也就是01背包的顺序。
贪心的一般思路:
考虑两个相邻的能量石, i j ij ij ,易知对于这两个能量石前面以及后面的能量获得与 i j ij ij的顺序是无关的
我们要证明 − − − i j − − − --- ij--- ij的顺序比 − − − j i − − − ---ji--- ji的顺序要更优,也就是: E i + E j − S i ∗ L j > E j + E i − S j ∗ L i E_i + E_j - S_i*L_j > E_j + E_i - S_j * L_i Ei+EjSiLj>Ej+EiSjLi
也就是 S i ∗ L j < S j ∗ L i S_i*L_j < S_j*L_i SiLj<SjLi
故按照 S i ∗ L j < S j ∗ L i S_i*L_j < S_j*L_i SiLj<SjLi对能量石排序,然后进行01背包

code

#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 1e4 + 10;
struct Node{
    int s, w, l;
    bool operator < (const Node &b) const{
        return s * b.l < b.s * l;
    }
}a[N];
int f[M];
// f[j] 表示 时间为j的所有方案的最大能量 
int main()
{
    int T; cin >> T;
    for(int t = 1; t <= T; ++ t) {
        int n, m; cin >> n;
        memset(f, -0x3f, sizeof f);
        f[0] = m = 0;
        for(int i = 1; i <= n; ++ i) cin >> a[i].s >> a[i].w >> a[i].l, m += a[i].s;
        sort(a + 1, a + 1 + n);
        for(int i = 1; i <= n; ++ i) {
            for(int j = m; j >= a[i].s; -- j) {
                int pre = j - a[i].s;
                f[j] = max(f[j], f[j - a[i].s] + a[i].w - pre * a[i].l);
            }
        }
        cout << "Case #" << t << ": " << *max_element(f, f + 1 + m) << "\n";
    }
}

pending~~~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

W⁡angduoyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值