P1450 [HAOI2008] 硬币购物 ↗
乍一看,是一个多重背包。但是这题“多测”数量大(n≤1000),直接按照多重背包的话,由于是统计方案数而不是求最优,因此无法使用二进制分组和单调队列优化,使用树状数组优化前缀和也只能做到 O(nslogs),乘上四种硬币的常数,过不了(能拿 10 pts)。
发现这个多测之间有很多公用信息,那就是硬币的面值 ci. 能否预处理出一个只与硬币面值有关的量,从而在每次查询可以快速得出答案?
一开始我是不信的,只是抱着试一试的态度点开了题解区,没想到解题思路立刻到账了。xdm,蓝链还在就是没领完,快去题解区冲冲冲!
完全背包+容斥原理#
第一次做还是很难想到的。
题目其实通过数据范围给我们锁定了一条窄路(当然也可能有神仙奇怪做法),那就是必须根据 ci 先预处理出一些量,再对快速对每组 di 做查询。想想仅仅根据 ci,我们能做什么?
完全背包#
我们可以求解出一个完全背包:在每种硬币数量都无限时,组合出特定钱数 s 的总方案数 f[s].
转移方程 f[s]+=f[s−ci],枚举每个 ci,再从小到大枚举 s.
获得了这个完全背包,我们考虑怎么在给定每个硬币的限制数量 di 时,求出满足限制的总方案数。
方案宝宝们#
为了方便理解,这里形式化一下“方案”的定义:
一个方案 p 就是一个四元组 (x1,x2,x3,x4),其中 xi 表示第 i 个硬币使用了多少个。两个方案是相同的当且仅当四个分量都相同。
一个方案 p 的钱数 S(p)=∑icixi.
那么,我们先前求出的完全背包就是 f[s]=∣{p∣S(p)=s}∣. 为了符号更加清晰,记 F(s)={p∣S(p)=s}.
F(s) 的意思是任何钱数等于 s 的方案组成的集合。我们考虑从中去除不满足要求的部分。
形式化地,一个方案 p 不满足要求,当且仅当 ∃i∈{1,2,3,4}, xi>di.
记 Ai={p∣xi>di},Bi=Ai∩F(s).
那么符合要求的方案组成的集合就是 G(s)=F(s)−⋃iAi.
容斥原理#
稍加推导:
G(s)=F(s)−i⋃Ai=F(s)∩(i⋃Ai)=F(s)∩(i⋂Ai)=F(s)∩A1∩A2∩A3∩A4=F(s)∩B1∩B2∩B3∩B4=F(s)−B1−B2−B3−B4
因此我们要求的就是 ∣G(s)∣=∣F(s)−B1−B2−B3−B4∣.
由于 Bi⊆F(s),根据容斥原理,∣G(s)∣=∣F(s)∣−∑i∣Bi∣+∑i=j∣Bi∩Bj∣−∑i=j,j=k,i=k∣Bi∩Bj∩Bk∣+∣B1∩B2∩B3∩B4∣
其中的每一项都可以通过已经求出的 f[s] 数组得到。怎么得到?
∣Bi∣ 表示 xi≥di+1 且 S(p)=s 的方案数,注意到这个数就等于 f[s−ci×(di+1)],即 xi≥0 且 S(p)=s−ci×(di+1) 的方案数。
从完全背包的定义出发可以较为直观地理解这个式子:每种硬币有无限个,组合出钱数 s 的方案数为 f[s],那么要求组合出钱数 s+ci×d 且至少使用 d 个第 i 种硬币的方案数,也是 f[s].
同理可以求出交集的元素数:∣Bi∩Bj∣=f[s−ci×(di+1)−cj×(dj+1)].
二进制枚举子集#
定义集合 B={B1,B2,B3,B4},只需要枚举 B 的子集 Bm∈P(B),然后把 (−1)∣Bm∣∣⋂Bi∈BmBi∣ 加到答案里。
可以使用二进制枚举子集技巧,初始化 m=s=(1<<4)-1,然后转移使用 s=(s-1)&m,详见二进制集合操作 ↗. 当然,也可以直接遍历 s∈{1,2,…,15}.
//
// main.cpp
// P1450
//
// Created by Leo Xia on 2026/4/2.
//
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int C = 4, S = 100010, N = 1010;
int c[C<<1],n;
int d[N][C<<1],s[N],smx;
ll f[S];
int main()
{
ios::sync_with_stdio(false);
cin>>c[1]>>c[2]>>c[3]>>c[4]>>n;
for(int i=1;i<=n;i++)
{
cin>>d[i][1]>>d[i][2]>>d[i][3]>>d[i][4]>>s[i];
smx=max(smx,s[i]);
}
f[0]=1;
for(int i=1;i<=C;i++)
{
for(int j=c[i];j<=smx;j++)
{
f[j]+=f[j-c[i]];
}
}
for(int i=1;i<=n;i++)
{
ll ans=f[s[i]];
for(int j=1;j<=15;j++)
{
int pcnt=0,ind=s[i];
for(int k=0;k<4;k++)if(j>>k&1)
{
ind-=c[k+1]*(d[i][k+1]+1);
pcnt^=1;
}
if(ind<0)continue;
ans+=-((pcnt<<1)-1)*f[ind];
}
cout<<ans<<'\n';
}
return 0;
}
cpp
由于这道题物品种类是常数,所以使用容斥原理的指数级复杂度(枚举子集)没有影响。一般的多重背包是不会考虑这么做的。