[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);
}
}