【洛谷5289】[十二省联考2019] 皮配(背包)
大致题意: 让你把\(m\)组共\(n\)个物品放入(蓝/红)(鸭/R)这\(4\)个背包,每组物品放入的背包阵营必须相同,且对于每种阵营和每种派系的背包各有一个容量总限制。另有\(k\)个限制规定某个物品不能放入某个背包,求总方案数。
考虑\(k=0\)
首先我们来考虑\(k=0\)的部分分。
则我们可以发现,城市选择的阵营与学校选择的派系两者是没有关系的,可以分开来计算。
如果你对这句话看得一脸懵逼,那么接下来就是对它的具体解释。
我们记每个学校的人数为\(s_i\),统计每个城市的总人数为\(t_i\)。
则由于同一个城市必须选择同一个阵营,而每个阵营又各有容量限制,因此就相当于把城市不重不漏分入两个有容量限制的背包中。
这是典型的背包问题,我们可以用\(k1_{i,j}\)表示前\(i\)个城市蓝阵营有\(j\)人的方案数,则转移为:
而考虑在城市确定阵营后,每个学校其实只能选择派系,因此同样相当于把学校不重不漏分入两个有容量限制的背包中。
与前面类似,我们用\(k2_{i,j}\)表示前\(i\)个学校鸭派系有\(j\)人的方案数,则转移为:
计算完之后,我们把它们相乘,就可以得到答案了。
考虑推广
虽然上面的方法有很大的局限性,但它是十分值得借鉴的。
所以,现在我们来考虑此方法的推广。
看数据范围可以发现,\(k\)其实很小,只有\(30\)。
则我们可以考虑,将不含限制学校的城市和无限制的学校依然像上面一样背包\(DP\),但是\(k1\)和\(k2\)的定义要稍作修改:
- \(k1_{i,j}\)表示前\(i\)个不含限制学校的城市蓝阵营有\(j\)人的方案数。
- \(k2_{i,j}\)表示前\(i\)个无限制的学校鸭派系有\(j\)人的方案数。
然后,对于含限制学校的城市和有限制的学校,我们单独考虑。
其实也只要设\(f_{i,j,k}\)表示前\(i\)个含限制学校的城市,蓝阵营有\(j\)人,鸭派系有\(k\)人的方案数,暴力\(DP\)即可。
关于内存和时间的一些优化
数组似乎开不下?
滚存一下即可。
复杂度似乎过不了?
这时我们就要用一个很普通但实用的优化:记录下当前城市总人数与\(c0\)的\(min\)值和学校总人数与\(d0\)的\(min\)值,然后以这个为\(DP\)转移的上界,复杂度就得到了大大优化。
具体实现详见代码。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000
#define V 2500
#define X 998244353
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
#define mem(x,v) memset(x,v,sizeof(x))
using namespace std;
int n,m,k,sum,c0,c1,d0,d1,bl[N+5],s[N+5],t[N+5],p[N+5],q[N+5];
int k1[V+5],k2[V+5],f[V+5][V+5],g[V+5][V+5];vector<int> o[N+5];
class FastIO
{
private:
#define FS 100000
#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
#define tn (x<<3)+(x<<1)
#define D isdigit(c=tc())
char c,*A,*B,FI[FS];
public:
I FastIO() {A=B=FI;}
Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
}F;
I int XSum(CI x,CI y) {return x+y>=X?x+y-X:x+y;}
I int XSub(CI x,CI y) {return x-y<0?x-y+X:x-y;}
int main()
{
RI Ttot,i,j,k,x,y,sz,l1,l2,ans;F.read(Ttot);W(Ttot--)
{
for(mem(k1,0),mem(k2,0),mem(f,0),mem(g,0),mem(t,0),i=1;i<=m;++i) o[i].clear();//清空
for(F.read(n,m,c0,c1,d0,d1),sum=0,i=1;i<=n;++i)//读入学校信息
F.read(bl[i],s[i]),p[i]=-1,sum+=s[i],t[bl[i]]+=s[i];//统计总人数和城市人数
for(i=1;i<=m;++i) q[i]=0;for(F.read(k),i=1;i<=k;++i)//读入限制
F.read(x,y),p[x]=y,q[bl[x]]=1,o[bl[x]].push_back(x);//标记城市有限制学校,将该学校扔入vector
if(c0+c1<sum||d0+d1<sum) {puts("0");continue;}//如果肯定无解,输出0并continue
#define L1(x) (l1^c0&&(l1+=t[x])>c0&&(l1=c0))//维护城市总人数与c0的min值
#define L2(x) (l2^d0&&(l2+=s[x])>d0&&(l2=d0))//维护学校总人数与d0的min值
for(k1[l1=0]=i=1;i<=m;++i) if(L1(i),t[i]&&!q[i])//如果城市含限制学校或人数为0跳过(注意人数为0必须判!我一开始偷懒没判就WA了。。。)
for(j=l1;j>=t[i];--j) Inc(k1[j],k1[j-t[i]]);//背包转移
for(k2[l2=0]=i=1;i<=n;++i) if(L2(i),s[i]&&!~p[i])//与上面类似
for(j=l2;j>=s[i];--j) Inc(k2[j],k2[j-s[i]]);
for(i=1;i<=c0;++i) Inc(k1[i],k1[i-1]);for(i=1;i<=d0;++i) Inc(k2[i],k2[i-1]);//统计前缀和
for(f[0][0]=i=1,l1=l2=0;i<=m;++i)
{
if(!t[i]||!q[i]) continue;for(j=0;j<=l1;++j) for(k=0;k<=l2;++k) g[j][k]=f[j][k];//将f复制一遍给g
for(x=0,sz=o[i].size();x^sz;++x) for(y=o[i][x],L2(y),j=l1;~j;--j) for(k=l2;~k;--k)//枚举有限制的城市进行转移
f[j][k]=XSum(p[y]^1?f[j][k]:0,k>=s[y]&&p[y]?f[j][k-s[y]]:0),//判断无法选择的情况
g[j][k]=XSum(p[y]^3?g[j][k]:0,k>=s[y]&&p[y]^2?g[j][k-s[y]]:0);//与上面类似
for(L1(i),j=l1;~j;--j) for(k=l2;~k;--k)//统计,也差不多是一个背包
f[j][k]=XSum(j>=t[i]?f[j-t[i]][k]:0,g[j][k]);
}
#define S1(x) XSub(k1[c0-(x)],sum-c1-(x)-1>=0?k1[sum-c1-(x)-1]:0)//求出合法范围内的方案数
#define S2(y) XSub(k2[d0-(y)],sum-d1-(y)-1>=0?k2[sum-d1-(y)-1]:0)//与上面类似
for(ans=i=0;i<=c0;++i) for(j=0;j<=d0;++j) Inc(ans,1LL*f[i][j]*S1(i)%X*S2(j)%X)%X;//最后计算答案
printf("%d\n",ans);//输出答案
}return 0;
}