zoukankan      html  css  js  c++  java
  • [GDSOI2017]逃亡(状压DP)

    [GDSOI2017]逃亡(状压DP)

    题面

    给出一棵(n)个点有向树,只能从父亲走向儿子。每个节点有一个攻击力(b_i),如果(i)能到达(j),且(b_i>b_j),则(i)会向(j)发动(a_i)次战争。给出(b_i)的范围([0,m])求使得战争发生次数(leq K)(b_i)赋值方案数。
    (n leq 14,K leq 20,m leq 10^9)

    分析

    看到这么小的数据范围显然考虑状压。

    对于这类和权值相关题目,我们可以从小到大对节点赋值,这样当前点的值一定比已经赋值的节点要大,这方便了我们计数。注意(m)的范围很大,我们可以考虑离散化后的权值范围(min(m,n-1))来dp,

    不妨设(f_{m{S},i,j})表示,发生(i)场战斗,点值离散化后填满([0,j])(即每一个值都在节点中存在),已经赋值的点集为(m{S}),实现上用二进制表示。

    那么我们可以枚举(x otin m{S}),它的值为(j),尝试将(x)加入点集.记(cnt(x))为能与(x)发生战争的点的个数。
    那么可以用刷表法

    [f_{m{S} cup { x},k+cnt(x)cdot a_x,i} leftarrow f_{m{S} cup { x},k+cnt(x)cdot a_x,i}+f_{m{S},k,i}+ f_{m{S},k,i-1} (k+cnt(x)cdot a_x in [1,k]) ]

    这是因为x的值是i,最终状态填满([0,i]),原来的状态可能填满([0,i-1]),也可能已经填满([0,i])

    最后统计答案,因为我们离散化了,最后还要还原。离散化的后的([0,j])变成([0,m]),就要从(m+1)个数里选(j+1)个递增的数,方案数为(C_{m+1}^{j+1})

    那么结果就是

    [ans_i=sum_{j=0}^{min(m,n-1)} C_{m+1}^{j+1} cdot f_{ {1,2,3,dots n },i,j} ]

    代码

    #include<iostream>
    #include<cstdio>
    #include<cstring> 
    #include<vector>
    #include<algorithm>
    #include<queue>
    #define maxn 14
    #define maxk 20
    #define mod 1000000007
    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);
    }
    inline ll C(int n,int m){
    	ll ans=1;
    	for(int i=n;i>=n-m+1;i--) ans=ans*i%mod;
    	for(int i=1;i<=m;i++) ans=ans*inv(i)%mod;
    	return ans;
    }
    
    int n,m,K,maxv;
    int fa[maxn+5],a[maxn+5];
    vector<int>E[maxn+5];
    bool is_cn[maxn+5][maxn+5];//cn[i][j]=1表示i能到达j 
    int seq[maxn+5];//按拓扑序DP 
    void topo_sort(){
    	int ptr=0;
    	static int in[maxn+5];
    	queue<int>q;
    	for(int i=2;i<=n;i++) in[i]++; 
    	for(int i=1;i<=n;i++) if(!in[i]) q.push(i); 
    	while(!q.empty()){
    		int x=q.front();
    		q.pop();
    		seq[++ptr]=x;
    		for(int i=0;i<(int)E[x].size();i++){
    			int y=E[x][i];
    			in[y]--;
    			if(in[y]==0) q.push(y);
    		}
    	} 
    }
    //按值从小到大给每个节点赋值,注意把值域离散化看成[0,m],就可以放进dp状态 
    ll dp[(1<<maxn)][maxk+1][maxn+1];//dp[s][i][j] 产生i场战斗,点值离散化后填满[0,j],已经赋值的点集s(值都<=当前值) 
    int main(){
    	scanf("%d %d %d",&n,&m,&K);
    	maxv=min(n-1,m);//值离散化后的个数 
    	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    	for(int i=2;i<=n;i++){
    		scanf("%d",&fa[i]);
    		E[fa[i]].push_back(i);
    	} 
    	for(int i=1;i<=n;i++) for(int j=i;j;j=fa[j]) is_cn[j][i]=1;
    	topo_sort();
    	for(int i=1;i<(1<<n);i++) dp[i][0][0]=1;
    	for(int i=1;i<=maxv;i++){
    		for(int j=1;j<=n;j++){
    			int x=seq[j];
    			for(int s=0;s<(1<<n);s++){
    				if(!(s&(1<<(x-1)))){//尝试往s中加入状态x 
    					int sz=0;//当前状态s中x能够到达的个数,那么发生战斗数就是sz*a[x] 
    					for(int u=1;u<=n;u++) if(( s&(1<<(u-1)) )&&is_cn[x][u]) sz++; 
    					for(int k=0;k+sz*a[x]<=K;k++){
    						dp[s|(1<<(x-1))][k+sz*a[x]][i]+=dp[s][k][i]+dp[s][k][i-1];
    						//x的值是i,最终状态填满[0,i],原来的状态可能填满[0,i-1],也可能已经填满[0,i] 
    						dp[s|(1<<(x-1))][k+sz*a[x]][i]%=mod;
    					}
    				}
    			}
    		}
    	}
    	for(int i=0;i<=K;i++){
    		ll ans=0;
    		for(int j=0;j<=maxv;j++){
    			ans+=C(m+1,j+1)*dp[(1<<n)-1][i][j]%mod; //把值还原,[0,j]变成[0,m],就要从m+1个数理选j+1个递增的数 
    			ans%=mod;
    		}
    		printf("%lld
    ",ans);
    	}
    	
    }
    
  • 相关阅读:
    sql 在日期范围内搜索
    js 处理日期时间字符串显示的方法
    matlab练习程序(并行计算)
    C++程序运行时间
    matlab练习程序(KNN,K最邻近分类法)
    多媒体指令(像素处理)
    ubuntu启动/重启/停止apache
    matlab练习程序(matlab调用c/c++)
    我的vim设置
    matlab练习程序(c/c++调用matlab<engine>)
  • 原文地址:https://www.cnblogs.com/birchtree/p/12740280.html
Copyright © 2011-2022 走看看