ICPC2024杭州站J题题解
前言
赛时没有做出来,然后赛后被队友嘲讽说是简单题,还搞了一堆奇奇怪怪的容斥加减......
我认为都是假的,毕竟计数的难点并不在于设计怎样的状态,而在于怎么不算重,我在赛时已经想过很多容斥了,要么会算重,要么就是无法通过的。经过了两天的思考,直到自己真正把这题弄出来后,我方才发现这是一个很巧妙的题,从朴素的暴力到正解这一过程绝对不是平凡的,而是很复杂的
谨以此文记录这一不轻易想到的巧妙的思路
赛时的思路
注意到题目要求的是可重集对数,因此我需要知道左右两边的可重集合里有多少种元素后还需要用一个组合数来求解
最朴素的暴力,枚举左边的集合出现了哪些数字
-
(a)
右边集合的某些数字必选,记这些数字的集合为 -
(b)
有若干个形如 " 中至少选一个的" 的限制
我们将题目中的
如果在原来的图中某条边连接的两个点其中一个出现在左边的集合中,而另外一个没有出现,那么另外一个就必须出现在右边的集合中,这是 (a)限制
如果在原来的图中某条边连接的两个点都出现在了左边的集合里,那么这条边就转化成了 (b)限制
,
如果在原来的图中某条边连接的两个点都没有出现在左边的集合里,那么这样的左边的集合是不合法的
然后可以注意到,其实 (b)限制
拍在图上是 原图对于点集
因此可以把右边点集 (b)限制
的染了色的点集
不过在此之前还需要把原图里没有任何边的点提前抽出来,扔到集合
然后考虑怎么计算这个过程:
我们记
首先枚举
然后检查 (b)限制
,如果均能通过上述检验,那么
优化
这个优化不是平凡的
-
具体的,你关注到其实想要计算
你不需要 而是需要 -
另外一点,就是
符合(b)限制
的一个等价命题是: 点集 的导出子图是一个独立集
然后实际上有
因此就有
-
是独立集(这只与 有关,与 无关,我们成功分离了(b)限制
里的捆绑的关系) -
所以可以先把所有独立集
代码:
#include <bits/stdc++.h> #define int long long #define pb push_back using namespace std; inline int read(){ int x=0,f=1;char ch=getchar(); for(;!isdigit(ch);ch=getchar())f^=ch=='-'; for(;isdigit(ch);ch=getchar())x=x*10+(ch^48); return f?x:-x; } const int N=2e6+5,mo=1e9+7; inline void red(int &x){x>=mo?x-=mo:0;} inline int qpow(int x,int t){ int ret=1; for(;t;t>>=1,x=x*x%mo)if(t&1)ret=ret*x%mo; return ret; } int fac[N],ifac[N]; inline int C(int x,int y){ if(y>x||y<0||x<0)return 0; int ret=1; for(int i=x;i>x-y;--i)ret=ret*i%mo; return ret*ifac[y]%mo; } int F(int u,int a,int b){return C(u+b-1,a+b-1);} int n1,n2,m,k,lnk[23],f[N],vis[23],ans; vector<int> avi[2]; void solve(){ n1=read(),n2=read(),m=read(),k=read(),ans=0; for(int i=0;i<m;++i)lnk[i]=vis[i]=0; for(int i=0;i<(1<<m);++i)f[i]=0; for(int i=0;i<k;++i){ int x=read()-1,y=read()-1; vis[x]=vis[y]=1; lnk[x]|=1<<y; lnk[y]|=1<<x; } int tt=0,Z=0,sz=0;//tt is the size of X , Z is Q for(int i=0;i<m;++i){ tt+=!vis[i]; Z|=vis[i]<<i; sz+=vis[i]; } avi[0].clear(); avi[0].pb(0); for(int i=0;i<m;++i)if(vis[i]){ avi[1].clear(); for(int x:avi[0]){ avi[1].pb(x); if(!((x|(1<<i))&lnk[i]))avi[1].pb(x|(1<<i)); } avi[0]=avi[1]; } for(int x:avi[0]) f[x]=F(n2,sz-__builtin_popcountll(x),tt); for(int j=0;j<m;++j)for(int i=0;i<(1<<m);++i) if(i&(1<<j))red(f[i]+=f[i^(1<<j)]); for(int i=0;i<(1<<m);++i)if((i&Z)==i){ int tag=1; for(int j=0;j<m;++j)if(vis[j]){ if(!(i&(1<<j))){ if((lnk[j]&i)!=lnk[j])tag=0; } } if(tag)red(ans+=F(n1,__builtin_popcountll(i),tt)*f[i]%mo); } printf("%lld\n",ans); } signed main(){ fac[0]=1; for(int i=1;i<N;++i)fac[i]=fac[i-1]*i%mo; ifac[N-1]=qpow(fac[N-1],mo-2); for(int i=N-1;i;--i)ifac[i-1]=ifac[i]*i%mo; for(int cas=read();cas--;)solve(); }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!