[[TOC]]

问题引入

问题描述

给定nn种物品和一背包,++每种物品是无限的++。物品ii的重量是wiw_i,其价值为viv_i,背包的容量为CC。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

输入格式/样例

格式:第一行有两个数nn,CC,表示有nn个物品,背包的容量为CC.接下来nn行,每一行两个数w,vw,v,表示物品的重量和价值.

5 7
6 5
5 4
2 3
4 6
2 6

解法1,O(n3)O(n^3)

Q(i,j)Q(i,j)表示前ii物品在每个物品可以选多次的情况下所有合法(所选的物品的重量和小于等于jj)选法组成的集合,这一个有重集(每个元素可以重复出现多次),显然max{sum(x)xQ(i,j)}=f(i,j)\max \{sum(x) | x \in Q(i,j) \} = f(i,j),其中sum(x)sum(x)表示选法xx所选对应物品的重量和,符合每一个问题对应一个集合的规律

现在考虑最后一个物品i=(wi,vi)i = (w_i,v_i),根据是否含有ii,显然可以把集合Q(i,j)Q(i,j)分成两不重不漏的两个集合:

  1. 集合BB表示ii没有出现(一个也不选),这个时候B=Q(i1,j)B = Q(i-1,j),对应的答案就是f(i1,j)f(i-1,j).
  2. 集合CC表示ii有出现, 然后在根据物品ii出的次数,对集合CC进行分类
  • 可能出现11次,设为C1C_1,转化对应的问题为f(i1,jwi)+vif(i-1,j- w_i) + v_i
  • 可能出现22次,设为C2C_2,转化对应的问题为f(i1,j2wi)+2vif(i-1,j- 2 \cdot w_i) + 2 \cdot v_i
  • \cdots
  • 最多可能出现kk次,设为CkC_k. 显然k=jwik = \lfloor \frac{j}{w_i} \rfloor,转化对应的问题为f(i1,jkwi)+kvif(i-1,j- k \cdot w_i) + k \cdot v_i

显然,可以得出

f(i,j)=max{Q(i,j)}=max{maxB,maxC}=max{maxB,maxC1,maxC2,,maxCk}=max{f(i1,j),f(i1,jwi),f(i1,j2wi)+2vi,,f(i1,jkwi)+kvi}=max{f(i1,jkwi)+kvi},0kjwi \begin{aligned} f(i,j) &= \max\{ Q(i,j) \} \\ &= \max\{ \max B, \max C \} \\ &= \max\{ \max B, \max C_1 ,\max C_2 ,\cdots , \max C_k \} \\ &= \max\{ f(i-1,j), f(i-1,j-w_i) ,f(i-1,j- 2 \cdot w_i) + 2\cdot v_i, \cdots, f(i-1, j - k \cdot w_i) + k \cdot v_i \} \\ &= \max\{f(i-1, j - k \cdot w_i) + k \cdot v_i \} , 0 \leqslant k \leqslant \lfloor \frac{j}{w_i} \rfloor \\ \end{aligned}

于是我们得到一般的状态转移方程

f(i,j)={0i==0j==0f(i,jk×wi)+k×vi0kk×wijk个物品if(i,j)= \left\{ \begin{array}{ccc} 0& i==0 \lor j == 0 \\ f(i,j-k \times w_i) + k \times v_i & 0 \leqslant k \land k \times w_i \leqslant j & \text{选$k$个物品$i$} \\ \end{array} \right.

那么这样的话,我们可以得到一个O(n3)O(n^3)算法的

点击
//Author by [Rainboy](https://github.com/rainboylvx) 
//date: 2024-07-09 22:47:39
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
int n,m;
int w[maxn];
int v[maxn];
int f[maxn][maxn];

int main (int argc, char *argv[]) {
    std::cin >> n >> m;

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        cin >> w[i] >> v[i];
    }

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        for(int j = 1;j <= m ;++j ) // j: 1->m
        {
            //表示一件物品i也不选,可以理解成k=0
            f[i][j] = f[i-1][j];

            //选k件,
            for(int k = 1 ; k*w[i] <= j ;k++) {
                f[i][j] = max(f[i][j],f[i-1][j- k*w[i]] + k*v[i]);
            }
        }
    }
    cout << f[n][m] <<endl;

    return 0;
}

解法2, 转成01背包

很容易想到,虽然题目说:每个物品有无限个.但因为容量是有限的,所以不可以无限的去放某个物品ii,那么对于某个物品ii来说,最多选ki=jwik_i = \lfloor \frac{j}{w_i} \rfloor个. 转变一个思路,可以认有kik_i个物品ii让你来选,也就是可以认为有kik_i个不同的物品,但它们的重量和价值都是相同的,都是(wi,vi)(w_i,v_i).

于是我们成功的把问题转化成了一个01背包问题.

如果要用二维数组存储f(i,j)f(i,j),也就是需要计算每个物品的kik_i,然后具体计算ki\sum k_i,这能才能知道需要二维数组的行数.

因为01背包的一维写法不需要开二维数组,那就是不需要计算行数了,减少了计算量.所以这里使用01背包的一维写法.

得到一个O(n3)O(n^3)的代码如下:

点击
//Author by [Rainboy](https://github.com/rainboylvx) 
//date: 2024-07-09 22:47:39
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
int n,m;
int w[maxn];
int v[maxn];
int f[maxn]; //一维数组

int main (int argc, char *argv[]) {
    std::cin >> n >> m;

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        cin >> w[i] >> v[i];
    }

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        // 遍历 k个 物品,每个物品的重量都是(w_i,v_i)
        for (int k = 1; k * w[i] <= m; k++)
        {
            // 上两行都是在枚举物品

            //倒过来枚举容量
            for(int j = m;j>= w[i];j--) {
                f[j] = max(f[j],f[j-w[i]] + v[i]);
            }
        }
    }
    cout << f[m] <<endl;

    return 0;
}

解法3优化,O(n2)O(n^2)

Q(i,j)Q(i,j)集合的子集合CC里每一个元素xx都一定含有一物品ii,好,现在把每个元素都去除一个物品ii,变成了一个新的集合DD,显然DD里的每个元素yy的价值和sum(y)<=jwisum(y) <= j-w_i.

显然: 集合DD就是问题f(i,jwi)f(i,j-w_i)对应的集合

k(k1)k(k \geqslant 1)个物品ii,等价描述为,至少选11个物品ii
Q(i,jwi)Q(i,j-w_i)集合对就的问题就是f(i,jwi)f(i,j-w_i),可能选了物品ii,也可能没有选物品ii

Q(i,jwi)Q(i,j-w_i)集合的每个元素是添加一个物品ii,变成新集合Q(i,j)Q(i,j),那么就是至少选一个.

我们成功应用了集合一一映射的思想,把一个问题转成了另一个问题.

f(i,j)={0i==0j==0f(i1,j)j<w[i]max{f(i1,j)不选物品if(i,jwi)+v[i]至少选一个物品ijwif(i,j)= \left\{ \begin{array}{ccc} 0& i==0 \lor j == 0 \\ f(i-1,j) & j<w[i] \\ \max \left\{ \begin{array}{cc} f(i-1,j) &\text{不选物品$i$} \\ f(i,j-w_i) + v[i] &\text{至少选一个物品$i$} \\ \end{array} \right. & j \geqslant w_i \end{array} \right.

//Author by [Rainboy](https://github.com/rainboylvx) 
//date: 2024-07-09 22:47:39
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
int n,m;
int w[maxn];
int v[maxn];
int f[maxn][maxn];

int main (int argc, char *argv[]) {
    std::cin >> n >> m;

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        cin >> w[i] >> v[i];
    }

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        for(int j = 1;j <= m ;++j ) // j: 1->m
        {
            f[i][j] = f[i-1][j]; //一件i也不选
            //可以至少选一件
            if( w[i] <= j && f[i][j] < f[i][j-w[i]] + v[i])
                f[i][j] = f[i][j-w[i]] + v[i];
        }
    }
    cout << f[n][m] <<endl;

    return 0;
}

优化2,降维

仔细观察动画,类比[ Rbook: 01背包]的思想,根据每个点需要的转移点的位置,可以把f[i][j]f[i][j]降成f[j]f[j]

//Author by [Rainboy](https://github.com/rainboylvx) 
//date: 2024-07-09 22:47:39
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
int n,m;
int w[maxn];
int v[maxn];
int f[maxn];

int main (int argc, char *argv[]) {
    std::cin >> n >> m;

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        cin >> w[i] >> v[i];
    }

    for(int i = 1;i <= n ;++i ) // i: 1->n
    {
        for(int j = w[i];j <= m ;++j ) // j: 1->m
        {
            f[j] = max(f[j],f[j-w[i]] +v[i]);
        }
    }
    cout << f[m] <<endl;

    return 0;
}

练习题目

  • luogu-P1616 疯狂的采药 入门题目

暂无题目