【十二省联考2019】皮配(背包神题)

传送门


我zxyoi学OI两年,什么场面没见过。

这场面我真没见过。

真正的DP好题,想清楚之后完全不难写。

虽然写完再去看觉得窒息得一批。

准确来说,这道题的考察内容就是背包计数。

是的,只有背包计数,除了乘法原理甚至可以说没有任何组合数学内容。

纯粹到极致的背包DP神仙题。

题解:

S S S s s s之和,即总人数。

首先考虑低分暴力(因为正解里面会用到)。

直接维护三位导师旗下的学员人数进行转移,显然第四位导师可以直接算出来,转移复杂度 O ( n M 3 ) O(nM^3) O(nM3)

发现其实可以直接分阵营和派系维护,即我们只维护蓝阵营和R派系的人数即可,复杂度 O ( n M 2 ) O(nM^2) O(nM2)

考虑 k = 0 k=0 k=0的情况。

我们重新理一遍题意,发现这种情况下阵营和派系的选择互不影响。

分别背包DP,设 f [ i ] f[i] f[i]表示蓝阵营中人数为 i i i的方案数, g [ i ] g[i] g[i]表示鸭派系中人数为 j j j的方案数。

则由乘法原理,答案为 ∑ i = S − C 1 C 0 ∑ j = S − D 1 D 0 f [ i ] g [ j ] \sum\limits_{i=S-C_1}^{C_0}\sum\limits_{j=S-D_1}^{D_0}f[i]g[j] i=SC1C0j=SD1D0f[i]g[j]


接下来考虑存在某些学校讨厌某个导师。

发现对于没有任何一个学校讨厌某个导师的城市,和没有讨厌的导师的学校,这部分计数可以直接套用上面的做法。

然后对于有限制的,可以直接套用暴力解法。

枚举有限制的学校分在蓝阵营,鸭派系的有多少人,剩下的部分直接乘法原理计算即可,需要维护一下上面背包结果的前缀和。

背包过程中需要比较精细的上界优化,不然复杂度假的。

单组数据复杂度 O ( ( c + n ) m + k 2 s M ) O((c+n)m+k^2sM) O((c+n)m+k2sM)


代码:

#include<bits/stdc++.h>
#define ll long long
#define re register
#define gc get_char
#define cs const

namespace IO{
	inline char get_char(){
		static cs int Rlen=1<<22|1;
		static char buf[Rlen],*p1,*p2;
		return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++;
	}
	
	template<typename T>
	inline T get(){
		char c;T num;
		while(!isdigit(c=gc()));num=c^48;
		while(isdigit(c=gc()))num=(num+(num<<2)<<1)+(c^48);
		return num;
	}
	inline int gi(){return get<int>();}
}
using namespace IO;

using std::cerr;
using std::cout;

cs int mod=998244353;
inline int add(int a,int b){a+=b-mod;return a+(a>>31&mod);}
inline void Inc(int &a,int b){a+=b-mod;a+=a>>31&mod;}

cs int N=1e3+7,M=2.5e3+7;

bool ban[N];
int sum[N],hate[N],b[N],s[N];
int f[M],F[M][M];
int g[M],G[M][M];
std::vector<int> C[N];

int n,c,C0,C1,D0,D1,tot,K;

inline void solve(){
	n=gi(),c=gi();
	C0=gi(),C1=gi(),D0=gi(),D1=gi();tot=0;
	for(int re i=1;i<=c;++i)ban[i]=false,sum[i]=0,C[i].clear();
	for(int re i=1;i<=n;++i){
		b[i]=gi(),s[i]=gi();
		sum[b[i]]+=s[i];hate[i]=-1;
		tot+=s[i];C[b[i]].push_back(i);
	}
	K=gi();while(K--){int x=gi();hate[x]=gi();ban[b[x]]=true;}
	
	int lim=0,tmp=0;
	memset(f,0,sizeof(int)*(C0+1));f[0]=1;lim=0;
	for(int re i=1;i<=c;++i)if(!ban[i]&&sum[i])
	for(int re j=lim=std::min(lim+(tmp=sum[i]),C0);j>=tmp;--j)Inc(f[j],f[j-tmp]);
	for(int re i=1;i<=C0;++i)Inc(f[i],f[i-1]);
	memset(g,0,sizeof(int)*(D0+1));g[0]=1;lim=0;
	for(int re i=1;i<=n;++i)if(!~hate[i])
	for(int re j=lim=std::min(lim+(tmp=s[i]),D0);j>=tmp;--j)Inc(g[j],g[j-tmp]);
	for(int re i=1;i<=D0;++i)Inc(g[i],g[i-1]);
	int CS=0,SS=0;F[0][0]=1;
	for(int re ct=1;ct<=c;++ct)if(ban[ct]){
		CS=std::min(CS+sum[ct],C0);
		for(int re i=0;i<=CS;++i)
		memcpy(G[i],F[i],sizeof(int)*(SS+1));
		for(int re a:C[ct])if(~hate[a]){
			int t=s[a];SS=std::min(SS+t,D0);
			if(hate[a]==1)
				for(int re i=0;i<=CS;++i){
					for(int re j=SS;j>=t;--j)F[i][j]=F[i][j-t];
					memset(F[i],0,sizeof(int)*t);
				}
			if(hate[a]>=2)
				for(int re i=0;i<=CS;++i)
				for(int re j=SS;j>=t;--j)Inc(F[i][j],F[i][j-t]);
			if(hate[a]==3)
				for(int re i=0;i<=CS;++i){
					for(int re j=SS;j>=t;--j)G[i][j]=G[i][j-t];
					memset(G[i],0,sizeof(int)*t);
				}
			if(hate[a]<=1)
				for(int re i=0;i<=CS;++i)
				for(int re j=SS;j>=t;--j)Inc(G[i][j],G[i][j-t]);
		}
		for(int re j=0,tmp=sum[ct];j<=SS;++j){
			for(int re i=CS;i>=tmp;--i)F[i][j]=F[i-tmp][j];
			for(int re i=tmp-1;~i;--i)F[i][j]=0;
		}
		for(int re i=0;i<=CS;++i)
		for(int re j=0;j<=SS;++j)Inc(F[i][j],G[i][j]);
	}
	int res=0;
	for(int re i=0;i<=CS;++i)
	for(int re j=0;j<=SS;++j){
		int l1=std::max(0,tot-C1-i),r1=C0-i;
		int l2=std::max(0,tot-D1-j),r2=D0-j;
		if(l1>r1)std::swap(l1,r1);
		if(l2>r2)std::swap(l2,r2);
		int v1=f[r1];if(l1)Inc(v1,mod-f[l1-1]);
		int v2=g[r2];if(l2)Inc(v2,mod-g[l2-1]);
		Inc(res,(ll)v1*v2%mod*F[i][j]%mod);
	}
	cout<<res<<"\n";
	for(int re i=0;i<=CS;++i)
	memset(F[i],0,sizeof(int)*(SS+1)),
	memset(G[i],0,sizeof(int)*(SS+1));
}

signed main(){
#ifdef zxyoi
	freopen("match.in","r",stdin);
#endif
	int T=gi();while(T--)solve();
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值