zoukankan      html  css  js  c++  java
  • [NOIOnline2提高组]游戏(二项式反演+树形背包)

    [NOIOnline2提高组]游戏(二项式反演+树形背包).md

    题面

    分析

    二项式反演的套路,设(f(i))为非平局回合数至少为(i)的情况. (g(i))为非平局回合数恰好为(i)的情况,则(f(i)=sum_{j=i}^m C_j^i g(j)).(这里的"至少"指的是我们"钦定"有(i)局一定平局,其他不一定,所以至少为(i).因此对(f(i))求后缀和不是(g(i)),会算重)我们要求的是(g(k)),但是(g)不太好求,考虑求(f)再反演得出(g).根据二项式反演定理(g(i)=sum_{j=i}^m C_{j}^i f(j))

    (f)可以用树形背包来求。(dp_{x,i})(x)的子树内至少(i)对选择的有祖先-后代关系的点(即非平局的回合).那么(f(i)=dp_{1,i}cdot(m-i)!)(剩下的(m-i)对关系可以任意排序)

    DP的时候先用模板的树形背包合并子树,然后考虑(x)这个点对答案的影响,有
    (dp_{x,i} leftarrow dp_{x,i}+dp_{x,i-1}cdots( ext{x子树内不被x的拥有者拥有的节点个数}-(i-1)))
    这是因为(x)可以和子树内不被(x)的拥有者拥有的,且还没被配对(减去(i-1))的节点形成非平局回合。

    代码

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define mod 998244353
    #define maxn 5000
    using namespace std;
    typedef long long ll;
    inline ll fast_pow(ll x,ll k){
    	ll ans=1;
    	while(k){
    		if(k&1) ans=ans*x%mod;
    		x=x*x%mod;
    		k>>=1;
    	}
    	return ans;
    }
    inline ll inv(ll x){
    	return fast_pow(x,mod-2);
    }
    ll fact[maxn+5],invfact[maxn+5];
    void ini(int n){
    	fact[0]=1;
    	for(int i=1;i<=n;i++) fact[i]=fact[i-1]*i%mod;
    	invfact[n]=inv(fact[n]);
    	for(int i=n-1;i>=0;i--) invfact[i]=invfact[i+1]*(i+1)%mod;
    }
    ll C(int n,int m){
    	return fact[n]*invfact[n-m]%mod*invfact[m]%mod;
    } 
    
    int m,n;
    int a[maxn+5];
    struct edge {
    	int from;
    	int to;
    	int next;
    } E[maxn*2+5];
    int head[maxn+5];
    int esz=1;
    void add_edge(int u,int v) {
    	esz++;
    	E[esz].from=u;
    	E[esz].to=v;
    	E[esz].next=head[u];
    	head[u]=esz;
    }
    
    int sz[maxn+5];
    int cnt[maxn+5][2];
    ll dp[maxn+5][maxn+5];//x的子树内,至少有i对祖先关系的方案数(有i对我们已经钦定,剩下的随便搞 
    //在dp转移中,我们钦定i对关系来转移,最后再乘上(m-i)!,表示剩下的关系可以任意匹配 
    //因为只是钦定,转移的时候就不需要考虑剩下的关系。而如果直接定义状态为恰好i对,转移时无法保证合法 
    void dfs(int x,int fa) {
    	static ll tmp[maxn+5];
    	sz[x]=1;
    	cnt[x][a[x]]++;
    	dp[x][0]=1;
    	for(int i=head[x];i;i=E[i].next){//先合并子树 
    		int y=E[i].to;
    		if(y!=fa){
    			dfs(y,x);
    			for(int j=0;j<=sz[x]+sz[y];j++) tmp[j]=0;
    			for(int j=0;j<=sz[x];j++){
    				for(int k=0;k<=sz[y];k++){
    					tmp[j+k]=(tmp[j+k]+dp[x][j]*dp[y][k]%mod)%mod;
    				}
    			}
    			for(int j=0;j<=sz[x]+sz[y];j++) dp[x][j]=tmp[j];
    			sz[x]+=sz[y];
    			cnt[x][0]+=cnt[y][0];
    			cnt[x][1]+=cnt[y][1];
    		}
    	}
    	//计算x的贡献 
    	for(int i=cnt[x][a[x]^1];i>=1;i--){//对于dp[x][i],x可以和子树里还没被选的与a[x]颜色相反的点匹配 
    		//于是贡献为dp[x][i-1]*(cnt[x][a[x]^1]-(i-1)) 倒序是为了防止重复更新 
    		dp[x][i]+=dp[x][i-1]*(cnt[x][a[x]^1]-(i-1))%mod;
    		dp[x][i]%=mod;
    	}
    }
    
    ll f[maxn+5],g[maxn+5];
    int main() {
    	int u,v;
    	scanf("%d",&n);
    	m=n/2;
    	ini(n);
    	for(int i=1;i<=n;i++) scanf("%1d",&a[i]);
    	for(int i=1;i<n;i++){
    		scanf("%d %d",&u,&v);
    		add_edge(u,v);
    		add_edge(v,u);
    	}
    	dfs(1,0);
    	for(int i=0;i<=m;i++) f[i]=dp[1][i]*fact[m-i]%mod;//求出真正的dp值
    	for(int i=0;i<=m;i++){
    		for(int j=i;j<=m;j++){
    			if((j-i)%2==1) g[i]-=C(j,i)*f[j]%mod;
    			else g[i]+=C(j,i)*f[j]%mod;
    			g[i]=(g[i]+mod)%mod;
    		}
    		printf("%lld
    ",g[i]);
    	} 
    }
    
  • 相关阅读:
    植物园偶遇一直喵
    植物园偶遇一直喵
    美食篇
    美食篇
    端午节路过南站
    端午节路过南站
    黄山云海
    黄山云海
    Android (1)
    树和树算法(1)
  • 原文地址:https://www.cnblogs.com/birchtree/p/12804870.html
Copyright © 2011-2022 走看看