OI 中的背包问题

OI 中的背包问题

2025.3.18 🍏讲课记录

01 背包

无价值

给定 \(n\) 个物品,每个物品的体积为 \(c_i\),求能否选出一些物品,使得体积和为 \(m\)

\(c = \max c_i\)

  • bitset 优化普通背包,时间复杂度为 \(\mathcal{O}(\frac{nm}\omega)\)
  • 分治 FFT,即将每个物品看成一个多项式 \(x^{c_i}+1\),然后全部卷起来,时间复杂度 \(\mathcal{O}(nc\log^2 n)\)

以上两个做法都可以求出所有 \(m\in [0,\sum c_i]\) 时的答案。然后下面是一个经典做法:

我们先从前往后加入物品,在满足体积和 \(\le m\) 的情况下一直加,假设加了前 \(p\) 个物品,则 \(\sum\limits_{i=1}^p c_i\in [m-c,m]\)。(特判掉所有物品体积和都不超过 \(m\) 的情况)

那么对于最终的答案,一定是在现在的方案上删除一些物品,加入一些物品。那么一定存在一种加删的方式,使得物品的体积和一直保持在 \([m-c,m+c]\) 中。因为可以考虑在当前体积和 \(\le m\) 时就加入物品,否则就删除。

我们设 \(f_{i,j,x}\) 表示考虑了前 \(i\) 个物品,只能删除编号 \(>j\) 的物品(即编号 \(\in[j+1,p]\),也相当于不动前 \(j\) 个物品),能否凑出来体积和为 \(x\),其中 \(x\in[m-c,m+c]\)。我们发现 \(j\) 更小限制一定是更宽的,所以对于特定的 \(i,x\)\(f_{i,j,x}\) 一定是一段前缀为 \(1\),后缀为 \(0\),那么可以记录这个分界点,设 \(g_{i,x}\) 表示考虑了前 \(i\) 个物品,凑出体积和为 \(x\) 的所有方案中,最多能不动前多少个物品。那么有转移:

  1. 不选当前的物品:\(g_{i,x}\larr g_{i-1,x}\)
  2. 加入当前物品,显然 \(j\) 不变:\(g_{i,x+c_i}\larr g_{i,x}\)
  3. 删除前 \(j\) 个物品中的一个,\(\forall j\in [1,g_{i,x}],g_{i,x-c_j}\larr j-1\)

那么最后答案就是 \(g_{n,m}\) 是否非空。但是这样子转移的时间复杂度为 \(\mathcal{O}(n^2c)\) 的,没有优化。

我们发现,每次枚举的 \(j\) 不用这么多,因为 \(g_{i-1,x}\le g_{i,x}\),而在第 \(i\) 位枚举的 \(j\in[1,g_{i,x}]\) 中,所有 \(j\in[1,g_{i-1,x}]\) 已经在 \(i-1\) 中枚举过了,因此只需要枚举 \(j\in (g_{i-1,x},g_{i,x}]\) 中的值来做转移即可。那么对于同一个 \(x\),枚举量只有 \(\mathcal{O}(n)\),总复杂度为 \(\mathcal{O}(nc)\)

这个做法还能求出体积和不超过 \(m\) 的情况下,体积和最大是多少。

例题:Subset Sum - Problem

代码
const int N = 2e4+5;
int a[N],F[N<<1],G[N<<1],n,m,k,mx,*f,*g,ans;

int main()
{
    for(int t = rd();t--;)
    {
        n = rd();m = rd();ans = mx = 0;
        for(int i = 1;i <= n;i++)mx = max(mx,a[i] = rd());
        f = F-m+mx;g = G-m+mx;int pos = 0,sum = 0;
        memset(F,-1,sizeof F);
        for(int i = 1;i <= n;i++)
            if(sum+a[i] > m){pos = i;break;}
            else sum += a[i];
        if(!pos){printf("%d\n",sum);continue;}
        f[sum] = pos-1;
        for(int i = pos;i <= n;i++)
        {
            memcpy(G,F,sizeof F);
            for(int j = m+mx;j >= m-mx;j--)
            {
                if(j-a[i] >= m-mx)f[j] = max(f[j],g[j-a[i]]);
                for(int k = g[j]+1;k <= f[j];k++)
                    if(j-a[k] >= m-mx)f[j-a[k]] = max(f[j-a[k]],k-1);
            }
        }
        for(int i = m;i;i--)if(~f[i]){ans = i;break;}
        printf("%d\n",ans);
    }
    return 0;
}

有价值

给定 \(n\) 个物品,第 \(i\) 个物品体积为 \(w_i\),价值为 \(v_i\),你需要选出一些物品,体积和不超过 \(m\),求最大价值和。

\(w = \max w_i,v = \max v_i\)。首先有复杂度为 \(\mathcal{O}(nm)\) 的暴力。

做法一

跟无价值的做法类似,先找到一个断点 \(p\),使得 \(\sum\limits_{i=1}^p w_i\in [m-w,m]\),然后直接选前 \(p\) 个物品,并将这 \(p\) 个物品的体积和价值取反,这样还是相当于 \(n\) 个物品的背包。根据经典结论,任意一个随机的和为 \(0\) 的由 \(1,-1\) 构成的序列中,前缀最大值期望为 \(\mathcal{O}(\sqrt n)\)

于是我们先将 \(n\) 个物品随机打乱,做背包的时候只需记录体积和在 \([m-\sqrt nw,m+\sqrt nw]\) 的状态即可。为了提高正确率可以适当扩大点区间。时间复杂度为 \(\mathcal{O}(n\sqrt nw)\)

似乎第一步选物品时先按性价比排序再选,那么前缀最大值最多为 \(\mathcal{O}(w\sqrt w)\)。(尚未证明,但这样做正确率应该会高点)

做法二

将同一体积的物品一起加入,设 \(f_{i,x}\) 表示加入了体积为 \([1,i]\) 的物品,体积和 \(\le x\) 的最大价值。

如果当前体积 \(i\) 选了 \(j\) 个,那么一定选的是体积为 \(i\) 的物品中价值最大的 \(j\) 个,假设价值和为 \(g_{i,j}\),那么转移为:\(f_{i,x+ik}\larr f_{i-1,x}+g_{i,j}\)

我们将 \(f_{i-1,x}\)\(x\bmod i\) 分类,每一类内部是做一个 \(\max+\) 卷积,因为 \(g_i\) 是凸的,所以有决策单调性,那么每次做是 \(\mathcal{O}(m\log m)\),总时间复杂度为 \(\mathcal{O}(wm\log m)\)。但是这个做法非常慢,基本被第一个做法偏序。

完全背包

无价值

给定 \(n\) 类物品,第 \(i\) 类物品体积为 \(c_i\),有无限个,求是否能选出一些物品,使得体积和为 \(m\)

\(c=\max c_i,g = \gcd(c_i)\),如果 \(g\nmid m\),那么肯定不行。然后可以发现,如果 \(m\) 足够大,那么肯定可以凑出。根据裴蜀定理,这个界为 \(\mathcal{O}(w^2)\)。那么当 \(m\) 不够大时,有以下做法。另 \(z=\min c_i\),设 \(f_{i,x}\) 表示考虑了前 \(i\) 个物品,\(\bmod z = x\) 的体积中能凑出来的最小的体积和是多少。因为有无限个体积为 \(z\) 的物品,所以所有 \(\ge f_{i,x}\)\(\bmod z = x\) 的体积和都可以凑出来。

初始化为 \(f_{0,0} = 0\),当加入第 \(i\) 个物品时,有转移 \(f_{i,(x+c_i)\bmod z}\larr f_{i-1,x}+c_i\),这个时候可以看成一张 \(z\) 个点的图,然后从 \(0\) 开始跑最短路,即同余最短路。或者使用转圈:考虑在 \(f_{i-1}\rarr f_{i}\) 的转移时,按照 \(\bmod \gcd(c_i,z)\) 分类,每一类的转移是一个环,那么从一个点开始转两圈更新即可。容易发现这一定会覆盖到所有转移。

继续分析上界,你会发现这张图一共有 \(z\) 个点,每条边权长度 \(\le c\),于是到每个点的最短路 \(\le zc\),于是有上界 \(zc\)

模板题很多,比如 P2371 [国家集训队] 墨墨的等式P2662 牛场围栏 - 洛谷 等。

选取刚好 \(k\)

\(\alpha = \lfloor \frac m k\rfloor\),然后 \(m\) 减去 \(k\alpha\),并将所有物品的体积减去 \(\alpha\),这样子还是相当于原问题。因为 \(m\in[0,kc]\) 内才有意义,所以操作后 \(\alpha\in[0,c],m\in [0,k),c_i\in[-c,c]\)。根据 01 背包的结论,一定存在一种选法,使得体积和一直在 \([\min(0,m-c),m+c]\) 内.

于是跑多项式快速幂求 \((\sum x^{c_i})^k\),然后看第 \(x^m\) 的系数即可,然后每次乘出来的结果只保留 \([\min(0,m-c),m+c]\) 内的即可。时间复杂度 \(\mathcal{O}((k+c)\log(k+c)\log k)\)

有价值

给定 \(n\) 类物品,第 \(i\) 类物品体积为 \(w_i\),价值为 \(v_i\),有无限个,你需要选出一些物品,体积和不超过 \(m\),求最大价值和。

\(w = \max w_i\)

\(m\ge w^2\)

\(m\) 非常大,根据直觉来看,肯定会选很多个性价比最大的那个,再用其余物品来填剩下的体积。

设性价比最大的物品为体积和价值为 \(w_{max},v_{max}\),那么以 \(w_{max}\) 为基准跑同余最短路。考虑怎么衡量一个背包方案的优劣,我们可以用这个方案的价值和减去全部用性价比最高的物品填的价值和来表示一个背包方案的价值。设状态 \(f_{i,x}\) 表示加入了前 \(i\) 个物品,凑出 \(\bmod z_{max}=x\) 的体积和的背包方案中价值最大的是多少。

那么可以写出转移:\(f_{i,(x+w_i)\bmod w_{max}}\larr f_{i-1,x}+v_i-\lfloor\frac{x+w_i}{w_{max}}\rfloor v_{max}\)。和上面一样的用最短路或者绕圈即可。如果要恰好凑出体积和 \(m\),答案就是 \(f_{n,m\bmod w_{max}}+\lfloor\frac m {w_{max}}\rfloor v_{max}\)

例题:P9140 [THUPC 2023 初赛] 背包 - 洛谷

代码
const int N = 1e5+5;
int n,m;
ll a[N],c[N],d[N],mx;
bool vis[N];
queue<int> q;
void SPFA()
{
    for(int i = 0;i < a[mx];i++)d[i] = -1e18;
    q.push(0);d[0] = 0;vis[0] = 1;
    while(!q.empty())
    {
        int u = q.front();q.pop();vis[u] = 0;
        for(int i = 1;i <= n;i++)if(i != mx)
        {
            int v = (u+a[i])%a[mx];ll w = c[i]-(u+a[i])/a[mx]*c[mx];
            if(d[v] < d[u]+w)
            {
                d[v] = d[u]+w;
                if(!vis[v]){q.push(v);vis[v] = 1;}
            }
        }
    }
}
int main()

    n = rd();m = rd();
    for(int i = 1;i <= n;i++)
    {
        a[i] = rd();c[i] = rd();
        if(!mx||c[i]*a[mx] > c[mx]*a[i])mx = i;
    }
    SPFA();
    while(m--)
    {
        ll x = rd();
        if(d[x%a[mx]] == -1e18)puts("-1");
        else printf("%lld\n",x/a[mx]*c[mx]+d[x%a[mx]]);
    }
    return 0;
}

然后思考 \(m\) 很小会出现什么问题,发现可能对于 \(f_{n,x}\) 的背包方案的体积和已经大于 \(m\) 了,那么再用这种算法就会出错。

那么这个上界是多少呢,考虑对于任意一个背包方案,假设除了性价比最高的物品,其余物品的体积分别为 \(w_1,w_2\ldots w_k\),设前缀和为 \(s_1,s_2\ldots s_k\)。如果有任意两个 \(i,j\) 满足 \(i<j,s_i\equiv s_j\pmod{w_{max}}\),那么区间 \([i+1,j]\) 内的物品体积和为 \(w_{max}\) 的倍数,那么可以把这段区间内的物品都替换为性价比最高的那个物品。

所以一个合法的方案一定满足 \(s_i\bmod w_{max}\) 互不相同,根据容斥原理,物品个数最多有 \(w_{max}\) 个,所以有一个上界 \(w_{max}\times w\)。所以 \(m\ge w^2\) 时可以用上面的做法。但当 \(m < w^2\) 时就要用别的做法。

\(m < w^2\) 做法一

\(f_i\) 表示体积和 \(\le i\),最大的价值和。假设最终答案的选法选的物品的体积分别为 \(w_1,w_2\ldots w_k\)\(\sum w_i=m\),那么一定有个断点 \(p\),使得 \(\sum\limits_{i=1}^p w_i\in[\frac m 2-\frac w 2,\frac m 2+\frac w 2]\),于是:

\[f_m = \max_{\lfloor\frac m 2\rfloor-\lfloor\frac w 2\rfloor}^{\lfloor\frac m 2\rfloor}f_i+f_{m-i} \]

预处理 \(m\le w\)\(f_i\),然后求答案时递归下去,并加个记忆化即可。递归层数为 \(\mathcal{O}(\log w)\),每一层用到的状态数不超过 \(2w\) 个,于是时间复杂度为 \(\mathcal{O}(w^2\log w)\)

例题:Problem - L - Codeforces

代码
const int N = 1005,V = 1000;
int n,m;ll f[N];
cc_hash_table<int,ll> mp;
ll dfs(int x)
{
    if(x <= V+1)return f[x];
    if(mp.find(x) != mp.end())return mp[x];
    ll ans = 0;
    for(int i = x/2-V/2;i <= x/2;i++)ans = max(ans,dfs(i)+dfs(x-i));
    return mp[x] = ans;
}
int main()
{
    n = rd();m = rd();
    for(int i = 1;i <= n;i++)
    {
        int w = rd(),v = rd();
        for(int j = w;j < N;j++)
            f[j] = max(f[j],f[j-w]+v);
    }
    cout << dfs(m) << endl;
    return 0;
}

另外,这个还可以处理恰好选 \(k\) 个物品的限制。设 \(f_{i,k}\) 表示体积和 \(\le i\),恰好选 \(k\) 个物品的最大价值和。同样的.可以证明存在一种分配,使得体积和在前 \(\frac m 2\) 的物品个数恰好为 \(\lfloor\frac k 2\rfloor\) 个,那么还是可以直接递归下去,即:

\[f_{m,k} = \max_{\lfloor\frac m 2\rfloor-\lfloor\frac w 2\rfloor}^{\lfloor\frac m 2\rfloor}f_{i,\lfloor\frac k 2\rfloor}+f_{m-i,\lceil\frac k 2\rceil} \]

时间复杂度还是 \(\mathcal{O}(w^2\log w)\)

\(m < w^2\) 做法二

\(f_i\) 表示体积和 \(\le i\) 价值和最大是多少,那么一个一个加物品,要求 \(f_i\) 则只需要加入前 \(\min(\frac{3m^2}i,n)\) 个物品即可(证明不会),这个直接做就可以调和级数求出 \([1,w^2]\) 内的答案,时间复杂度 \(\mathcal{O}(w^2\log w)\),而且常数非常小。

多重背包

posted @ 2025-03-19 19:52  max0810  阅读(151)  评论(2)    收藏  举报