背包模型

背包模型

二维费用的背包问题

\(N\) 件物品和一个容量是 \(V\) 的背包,背包能承受的最大重量是 \(M\)

每件物品只能用一次。体积是 \(v_i\),重量是 \(m_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

题解:

状态表示:\(f[i][j][k]\):代表只从前\(i\)个物品中取,且背包容量不超过\(j\),背包重量不超过\(k\)所能获得的最大价值

状态属性:\(MAX\)

状态转移:

  1. 不选择第\(i\)个物品:\(f[i-1][j][k]\)
  2. 选择第\(i\)个物品:\(f[i-1][j-v_i][k-m_i]+w_i\)

\[f[i][j][k] = max(f[i-1][j][k],f[i-1][j-v_i][k-m_i]+w_i) \]

状态优化:线性DP,滚动数组优化掉第一维即可

const int N = 1e3 + 10, M = 4e5 + 10;

int n, m1, m2;
int v1[N], v2[N], w[N];
int f[N][N];

void solve()
{
    cin >> n >> m1 >> m2;
    for (int i = 1; i <= n; ++i)
        cin >> v1[i] >> v2[i] >> w[i];
    for (int i = 1; i <= n; ++i)
        for (int j = m1; j >= v1[i]; --j)
            for (int k = m2; k >= v2[i]; --k)
                f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + w[i]);
    cout << f[m1][m2] << endl;
}

潜水员

潜水员为了潜水要使用特殊的装备。

他有一个带2种气体的气缸:一个为氧气,一个为氮气。

让潜水员下潜的深度需要各种数量的氧和氮。

潜水员有一定数量的气缸。

每个气缸都有重量和气体容量。

潜水员为了完成他的工作需要特定数量的氧和氮。

他完成工作所需气缸的总重的最低限度的是多少?

题解:二维费用的背包问题

  • 状态表示:\(f[i][j][k]\):代表只从前i个气缸中选,使得选出的气缸氧气容量不少于\(j\)升,且氮气容量不少于\(k\)升的最小气缸重量

  • 状态属性:\(MIN\)

  • 状态计算:

  1. 不选第\(i\)个气缸:\(f[i-1][j][k]\)

  2. 选第\(i\)个气缸:\(f[i][max(0,j - v_{1i})][max(0,k-v_{2i})]\)

const int N = 1e3 + 10, M = 4e5 + 10;

int n, m, q;
int v1[N], v2[N], w[N];
int f[N][N];

void solve()
{
    cin >> n >> m >> q;
    for (int i = 1; i <= q; ++i)
        cin >> v1[i] >> v2[i] >> w[i];
    for (int i = 0; i <= n; ++i)
        for (int j = 0; j <= m; ++j)
            f[i][j] = INF;
    f[0][0] = 0;
    for (int i = 1; i <= q; ++i)
        for (int j = n; j >= 0; --j)
            for (int k = m; k >= 0; --k)
                f[j][k] = min(f[j][k], f[max(j - v1[i], 0LL)][max(k - v2[i], 0LL)] + w[i]);
    cout << f[n][m] << endl;
}

数字组合(硬币问题)

给定 \(N\) 个正整数 \(A_1,A_2,…,A_N\),从中选出若干个数,使它们的和为 \(M\),求有多少种选择方案。

题解:

状态表示:\(f[i][j]\):只从前\(i\)个数中选择数,使得这些选出的数之和为\(j\)的方案数

状态属性:数量

状态计算:

  1. 不选第i个数:\(f[i-1][j]\)
  2. 选第i个数: \(f[i-1][j-a[i]]\)

状态优化 : 滚动数组优化: \(f[j] += f[j - a[i]]\)
状态初始:\(f[0] = 1\)

const int N = 1e2 + 10, M = 1e4 + 10;

int n, m;
int a[N];
int f[M];

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    f[0] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = m; j >= 1; --j)
            if (j - a[i] >= 0)
                f[j] += f[j - a[i]];
    cout << f[m] << endl;
}

[NOIP2018 提高组] 货币系统

在网友的国度中共有 \(n\) 种不同面额的货币,第 \(i\) 种货币的面额为 \(a[i]\),你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为 \(n\)、面额数组为 \(a[1..n]\) 的货币系统记作 \((n,a)\)

在一个完善的货币系统中,每一个非负整数的金额 \(x\) 都应该可以被表示出,即对每一个非负整数 \(x\),都存在 \(n\) 个非负整数 \(t[i]\) 满足 \(a[i] \times t[i]\) 的和为 \(x\)。然而, 在网友的国度中,货币系统可能是不完善的,即可能存在金额 \(x\) 不能被该货币系统表示出。例如在货币系统 \(n=3\), \(a=[2,5,9]\) 中,金额 \(1,3\) 就无法被表示出来。

两个货币系统 \((n,a)\)\((m,b)\) 是等价的,当且仅当对于任意非负整数 \(x\),它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。

现在网友们打算简化一下货币系统。他们希望找到一个货币系统 \((m,b)\),满足 \((m,b)\) 与原来的货币系统 \((n,a)\) 等价,且 \(m\) 尽可能的小。他们希望你来协助完成这个艰巨的任务:找到最小的 \(m\)

题解:完全背包解决极大线性无关组问题

  • 容易发现我们需要找到一个最小的货币系统,且这个货币系统能够线性表示原来货币系统中所有的面值,所以实际上就是让我们求原来货币系统的极大线性无关组的大小
  • 所以我们利用完全背包求出每种金额的货币的方案数,方案数为\(1\)的一定是极大线性无关组中的一员
  • 即求出多少个货币的方案数为\(1\)即可
const int N = 2e5 + 10, M = 4e5 + 10;

int n;
int a[N];
int f[N];

void solve()
{
    cin >> n;
    int mx = -INF;
    for (int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        mx = max(mx, a[i]);
    }
    for (int i = 0; i <= mx; ++i)
        f[i] = 0;
    f[0] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = a[i]; j <= mx; ++j)
            f[j] += f[j - a[i]];
    int ans = 0;
    for (int i = 1; i <= n; ++i)
        if (f[a[i]] == 1)
            ans++;
    cout << ans << endl;
}

多重背包问题——单调队列优化

题解:\(O(n^2)\)

image-20230617212201879 image-20230617212229592 image-20230617212246328 image-20230617212312959
const int N = 1e3 + 10, M = 2e4 + 10;

int n, m;
int f[2][M];
int v[N], w[N], s[N];
int q[M], hh, tt;

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> v[i] >> w[i] >> s[i];
    for (int i = 1; i <= n; ++i)
    {
        for (int r = 0; r < v[i]; ++r)
        {
            hh = 0, tt = -1;
            for (int j = r; j <= m; j += v[i])
            {
                while (hh <= tt && j - s[i] * v[i] > q[hh])
                    hh++;
                while (hh <= tt && f[i & 1 ^ 1][q[tt]] + (j - q[tt]) / v[i] * w[i] < f[i & 1 ^ 1][j])
                    tt--;
                q[++tt] = j;
                f[i & 1][j] = f[i & 1 ^ 1][q[hh]] + (j - q[hh]) / v[i] * w[i];
            }
        }
    }
    cout << f[n & 1][m] << endl;
}

混合背包问题

\(N\) 种物品和一个容量是 \(V\) 的背包。

物品一共有三类:

  • 第一类物品只能用\(1\)次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 \(s_i\) 次(多重背包);

每种体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

题解:

  • 对于该类问题我们直接对每种背包问题逐个\(dp\)即可
  • 我们不妨先利用二进制优化将多重背包变成01背包问题
  • 然后完全背包就按完全背包的状态转移走,01背包就按01背包的状态转移走
const int N = 1e4 + 10, M = 4e5 + 10;

int n, m;
int f[N];
int v[N], w[N];
int idx;

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
    {
        int a, b, c;
        cin >> a >> b >> c;
        if (c == -1)
        {
            v[++idx] = a;
            w[idx] = b;
        }
        else if (c > 0)
        {
            int k = 1;
            while (k <= c)
            {
                v[++idx] = a * k;
                w[idx] = b * k;
                c -= k;
                k <<= 1;
            }
            if (c)
            {
                v[++idx] = a * c;
                w[idx] = b * c;
            }
        }
        else
        {
            for (int j = a; j <= m; ++j)
                f[j] = max(f[j], f[j - a] + b);
        }
    }
    for (int i = 1; i <= idx; ++i)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
}

[NOIP2006 提高组] 金明的预算方案

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 \(n\) 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件 附件
电脑 打印机,扫描仪
书柜 图书
书桌 台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 \(0\) 个、\(1\) 个或 \(2\) 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 \(n\) 元。于是,他把每件物品规定了一个重要度,分为 \(5\) 等:用整数 \(1 \sim 5\) 表示,第 \(5\) 等最重要。他还从因特网上查到了每件物品的价格(都是 \(10\) 元的整数倍)。他希望在不超过 \(n\) 元的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 \(j\) 件物品的价格为 \(v_j\),重要度为\(w_j\),共选中了 \(k\) 件物品,编号依次为 \(j_1,j_2,\dots,j_k\),则所求的总和为:

\(v_{j_1} \times w_{j_1}+v_{j_2} \times w_{j_2}+ \dots +v_{j_k} \times w_{j_k}\)

请你帮助金明设计一个满足要求的购物单。

题解:有依赖的背包问题——利用分组背包求解

  • 因为每个主件的附件比较少,且附件没有附件,所以我们不妨将选择一个主件和其某些附件当成某组的一个方案,利用分组背包求解即可
  • 状态表示:\(f[i][j]\)代表只在前\(i\)个组中选择决策,使得选出的物品的价格不超过\(j\)的最大价值
  • 状态属性:\(MAX\)
  • 状态转移:
    显然我们不能按照分配给附件多少体积来划分,因为体积太大了,\(N*V*V\)显然会超时,但是我们发现每个主件最多只有2个附件,那么如果我们将每个主件及其附件看成一个物品组的话,最多只有4种情况,所以我们可以利用二进制枚举这四种组合,那么一个物品组中最多只会有4个物品,时间复杂度为\(O(4*N*V)\)
const int N = 60 + 10, M = 4e4 + 10;

int n, m;
int f[M];
int idx;
int V[N], W[N];
vector<pii> g[N];

void solve()
{
    cin >> m >> n;
    for (int i = 1; i <= n; ++i)
    {
        int a, b, c;
        cin >> a >> b >> c;
        if (c == 0)
            V[i] = a, W[i] = b;
        else
            g[c].push_back({a, b});
    }
    for (int i = 1; i <= n; ++i) // 遍历物品组
    {
        if (!V[i]) // 如果不是主件直接continue,因为有了主件才有物品组
            continue;
        for (int j = m; j >= 0; j--) // 枚举体积
        {
            for (int k = 0; k < (1 << g[i].size()); ++k) // 枚举物品组中的物品(决策)
            {
                int v = V[i], w = V[i] * W[i];        // 主件必须选,因为依赖关系
                for (int q = 0; q < g[i].size(); ++q) // 二进制枚举所有组合
                {
                    if (k >> q & 1)
                    {
                        v += g[i][q].first;
                        w += g[i][q].first * g[i][q].second;
                    }
                }
                if (j - v >= 0)
                    f[j] = max(f[j], f[j - v] + w);
            }
        }
    }
    cout << f[m] << endl;
}

有依赖的背包问题

image-20230618001917697

题解:树上背包 \(O(n\times v^2)\)

  • 状态表示:\(f[u][i][j]\)代表只从以\(u\)为根的前\(i\)个子树中选,且选出的物品体积不超过\(j\)的最大价值

  • 状态属性:\(MAX\)

  • 状态计算:\(O(v ^ 2)\)
    首先因为从\(u\)的子树中选,由于依赖关系,\(u\)必须选,所以剩余的可用体积为\(j-v[u]\),所以我们按照每个子树用了多少体积来划分
    我们可以将每个子树\(v\)看成一个物品组,每一个物品(决策)就是分配\(k\)个体积给该子树,那么每个物品的体积为\(k\),价值为\(f[v][v_{son}][k],v_{son}\)是节点\(v\)的子节点个数

\[f[u][i][j] = max(f[u][i][j],f[u][i-1][j-k] + f[v][v_{son}][k]) \]

  • 状态优化:我们发现状态只和上一层状态有关,我们优化掉第二维,倒序遍历体积
    \(f[u][j] = max(f[u][j],f[u][j-k] + f[v][k])\)

  • 状态初始:
    \(f[u][j] = f[u][j - v[u]] + w[u]; j > v[u]\)
    \(f[u][j] = 0,j < v[u]\)

const int N = 1e2 + 10, M = 4e5 + 10;

int n, m;
int f[N][N];
int v[N], w[N];
vector<int> g[N];

void dfs(int u, int par)
{
    for (auto V : g[u]) // 遍历物品组,即遍历子节点
    {
        if (V == par)
            continue;
        dfs(V, u);
        for (int j = m - v[u]; j >= 0; j--) // 遍历预留好u后的体积
            for (int k = 0; k <= j; ++k)    // 遍历决策
                f[u][j] = max(f[u][j], f[u][j - k] + f[V][k]);
    }
    for (int j = m; j >= v[u]; j--) // 将预留好的u放入
        f[u][j] = f[u][j - v[u]] + w[u];
    for (int j = 0; j < v[u]; ++j) // 清空无法选u的所有状态
        f[u][j] = 0;
}

void solve()
{
    int rt = 1;
    cin >> n >> m;
    for (int i = 1, u; i <= n; ++i)
    {
        cin >> v[i] >> w[i] >> u;
        if (u == -1)
        {
            rt = i;
            continue;
        }
        g[u].push_back(i);
        g[i].push_back(u);
    }
    dfs(rt, -1);
    cout << f[rt][m] << endl;
}

背包问题求最优解方案数

image-20230618124142473

题解:

  • 我们不妨在状态转移的时候进行计数\(dp\)
  • 状态表示:\(g[i][j]\)代表只从前\(i\)个物品中选,且选出的物品的总体积恰好为\(j\)的最大价值的方案数
  • 状态属性:数量
  • 状态转移:
  1. \(f[i-1][j] > f[i-1][j-v_i]+w_i\)\(g[i][j] += g[i-1][j]\)
  2. \(f[i-1][j] < f[i-1][j-v_i]+w_i\)\(g[i][j] += g[i-1][j-v_i]\)
  3. \(f[i-1][j] = f[i-1][j-v_i]+w_i\)\(g[i][j] += g[i-1][j-v_i] + g[i-1][j]\)
  • 状态初始:\(g[0][0] =1\)
  • 答案呈现:\(\sum g[n][i]\ \ 且 \ \ f[n][i] = f[n][m]\)
const int N = 1e3 + 10, M = 4e5 + 10;

int n, m;
int f[N][N];
int g[N][N];
int v[N], w[N];

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> v[i] >> w[i];
    g[0][0] = 1;
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 0; j <= m; ++j)
        {
            if (j - v[i] < 0)
            {
                f[i][j] = f[i - 1][j];
                g[i][j] = (g[i][j] + g[i - 1][j]) % mod;
            }
            else
            {
                if (f[i - 1][j] > f[i - 1][j - v[i]] + w[i])
                {
                    f[i][j] = f[i - 1][j];
                    g[i][j] = (g[i][j] + g[i - 1][j]) % mod;
                }
                else if (f[i - 1][j] < f[i - 1][j - v[i]] + w[i])
                {
                    f[i][j] = f[i - 1][j - v[i]] + w[i];
                    g[i][j] = (g[i][j] + g[i - 1][j - v[i]]) % mod;
                }
                else
                {
                    f[i][j] = f[i - 1][j];
                    g[i][j] = (g[i][j] + g[i - 1][j] + g[i - 1][j - v[i]]) % mod;
                }
            }
        }
    }
    int ans = 0;
    for (int i = 0; i <= m; ++i)
    {
        if (f[n][i] == f[n][m])
            ans = (ans + g[n][i]) % mod;
    }
    cout << ans << endl;
}

背包问题求字典序最小的具体方案

image-20230618131357914

题解:

  • 我们求字典序最小的方案需要从前往后递推,但是一般我们\(dp\)输出方案的时候都是从后往前根据转移找的
  • 所以在本题我们需要先从后往前\(dp\),也就是说我们倒序遍历物品的顺序,这样的话我们就可以从第一个物品开始正序求字典序最小的方案
  • 观察倒序遍历物品后的01背包的状态转移方程:
    \(f[i][j] = max(f[i+1][j],f[i+1][j-v[i]] + w[i])\)
  • 我们贪心的思考,如果我们能选第\(i\)个物品,那我们一定要选,因为这样字典序一定是最小的
    如果我们不能选第\(i\)个物品 ,那么我们就不选
const int N = 1e3 + 10, M = 4e5 + 10;

int n, m;
int f[N][N];
int v[N], w[N];

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> v[i] >> w[i];
    for (int i = n; i >= 1; --i)
        for (int j = 0; j <= m; ++j)
            if (j - v[i] < 0)
                f[i][j] = f[i + 1][j];
            else
                f[i][j] = max(f[i + 1][j], f[i + 1][j - v[i]] + w[i]);
    vector<int> ans;
    int pre = f[1][m];
    int j = m;
    for (int i = 1; i <= n; ++i)
    {
        if (j - v[i] >= 0 && pre == f[i + 1][j - v[i]] + w[i])
        {
            pre = f[i + 1][j - v[i]];
            j = j - v[i];
            ans.push_back(i);
        }
        else
            pre = f[i + 1][j];
    }
    for (auto it : ans)
        cout << it << " ";
    cout << endl;
}
posted @ 2023-06-18 13:17  Zeoy_kkk  阅读(8)  评论(0编辑  收藏  举报