[HEOI2013]SAO ——计数问题

题目大意:

Welcome to SAO ( Strange and Abnormal Online)。这是一个 VR MMORPG, 含有 n 个关卡。但是,挑战不同关卡的顺序是一个很大的问题。

有 n – 1 个对于挑战关卡的限制,诸如第 i 个关卡必须在第 j 个关卡前挑战, 或者完成了第 k 个关卡才能挑战第 l 个关卡。并且,如果不考虑限制的方向性, 那么在这 n – 1 个限制的情况下,任何两个关卡都存在某种程度的关联性。即, 我们不能把所有关卡分成两个非空且不相交的子集,使得这两个子集之间没有任 何限制。

对于每个数据,输出一行一个整数,为攻克关卡的顺序方案个数,mod 1,000,000,007 输出。

题目翻译:

发现最后一句话就是说:这是一棵树形图。

所以我们现在有了一棵树,只是边是有向边,挑战的限制就是边的方向,我们必须把所有指向x0的关卡全部通过,才能通过x0关卡。

其实,所有指向x0的边就是它的度数,所以可以看出来,

这个题是让我们求这个树形图有多少种拓扑序。

分析:

这个题即使看了题解也是理解了半天。网上题解也不是很多,做法类似。

树形计数问题,可以用树形DP,首先我们可以先尝试定义一维,定义f[i]表示以i为根的子树的拓扑序有多少种,现在我们需要考虑怎样将若干个儿子的值转移到父亲上。

发现,如果把两个儿子的拓扑序,看做是两个区间,那么我们做的其实是一个区间合并的操作。

但是由于边其实是有向的,(虽然我们是无向边建树)实际边的方向还决定父亲,该儿子的真正完全的拓扑序谁在前,谁在后。就是说,要先过了父亲,还是先过了儿子。

非常无从下手的感觉。我们需要再定义一维。

于是我们这样定义:

f[i][j]表示,在以i为根的子树中,根节点i排在第j位的拓扑序的种类数。(其实所有的拓扑序就是f[i][1-size])

可以发现,j不同时,方案数一定是独立的。

现在我们要考虑转移:

当我们循环到x的一个儿子y的时候,size[x]记录的是当前x与其前面所有儿子子树的size和,就是还没有包括y

那么前面说了,就是一个区间合并,我们以x的位置作为断点考虑合并。

先分类(因为我们无向边建树,但是实际上是有向边。)

①x<y 即先通过x,再通过y。

这个时候,拓扑序合并后x的排名一定在y的前面。

对于f[x][k],最终x前面有k-1个元素。可以从f[x][i](1<=i<=min(k,size))和 f[y][j](j的范围随后再确定)转移过来。转移之后,区间内共有size[x]+size[y]个数

就是说,我在合并后的拓扑序中,先从之前的f[x][i]中的方案数中拿出若干种,放进大区间里,再从f[y][j]里选择一些方案数,放进大区间里。所以这里i一定小于等于k

前k-1个位置,从之前的数中先挑出i-1个位置,有C(k-1,i-1)种选法,

后size[x]+size[y]-k个位置(不算x), 已经选择了i-1个数,还剩下size[x]-i个数(x自己不算),有C(size[x]+size[y]-k,size[x]-i)种选法。

再乘上每个选上的集合中自己的变化,也就是f[x][i]自己本身(类似多重集合的排列)

剩下的位置就是f[y][j]的了,不需要再乘组合数,只需乘上f[y][j]就好。

现在我们要确定j的取值范围:

对于x<y的情况,x之前的数,我们已经填了i-1个位置,还剩下k-i个位置要填,

为了使得y在x的后面,而y之前还能放j-1个数,所以要使得:j-1>=k-i,当然j<=size[y]

所以,j的循环范围是,k-i+1<=j<=size[y]

所以,对于x<y的情况,我们可以列出状态转移的方程是:

f[x][k]=(1<=i<=min(k,size[x]))(k-i+1<=j<=size[y]) f[x][i]*c[k-1][i-1]*c[size[x]+size[y]-k][size[x]-i]*f[y][j]

这样子,发现每次要循环一遍j,复杂度是O(n^4)的,直接挂掉。。。

又发现,对于同一个y,我们好像加的是同一些树,循环的是同一些j。。。

我们把这个式子用乘法分配律提出来一下:

f[x][k]=(1<=i<=min(k,size[x])) f[x][i]*c[k-1][i-1]*c[size[x]+size[y]-k][size[x]-i]*(f[y][k-i+1]+...f[y][size[y]])

所以,加粗部分是可以通过一个前缀合优化处理的,复杂度变成O(1)。

①x>y 即先通过y,再通过x。

其实是同理的。f[x][i](1<=i<=min(k,size)),i的范围没有变。

但是由于要保证y在x的前面,j-1个元素,必然不能填满k-i个位置

所以,j-1<k-i (注意是小于,不是小于等于,因为还有一个位置是y自己,所以要用j-1个位置填不满k-i个位置)并且j>=1

所以这里的状态转移方程是:

f[x][k]=(1<=i<=min(k,size[x]))(1<=j<=k-i) f[x][i]*c[k-1][i-1]*c[size[x]+size[y]-k][size[x]-i]*f[y][j]

同理可以乘法分配律,前缀和优化。

详见代码:

 

#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
using namespace std;
const int N=1000+10;
const int mod=1e9+7;
int n,t;
struct node{
    int nxt,to,val;
}bian[2*N];
int head[N],cnt;
void add(int x,int y,int z)
{
    bian[++cnt].to=y;
    bian[cnt].nxt=head[x];
    bian[cnt].val=z;
    head[x]=cnt;
}

ull f[N][N],sumdp[N][N];
ull c[N][N];
int size[N];
bool vis[N];
void dfs(int x)
{
    size[x]=1;
    f[x][1]=1;
    vis[x]=1;
    for(int o=head[x];o;o=bian[o].nxt)
    {
        int y=bian[o].to;
        if(!vis[y])
        {
        dfs(y);
        if(bian[o].val)//......x...y
        {
            for(int k=size[x]+size[y];k>=1;k--)
            {
                ull sum=0;
                for(int i=1;i<=min(size[x],k);i++)
                {    
                    int l=k-i,r=size[y];
                    ull del=(sumdp[y][size[y]]+mod-sumdp[y][k-i])%mod;//前缀和差值 
                    if(l<r)
                    {
                    ull q=(f[x][i]*del)%mod,p=(c[k-1][i-1]*c[size[x]+size[y]-k][size[x]-i])%mod;
                    p*=q;p=p%mod;sum+=p;sum%=mod;//这里,必须四个数分别计算并取模,否则会爆long long 
                    }    
                }
                f[x][k]=sum;
            }
        }
        else//.........y...x
        {
            for(int k=size[x]+size[y];k>=1;k--)
            {
                ull sum=0;
                for(int i=1;i<=min(size[x],k-1);i++)
                {
                    int r=min(size[y],k-i);
                    ull del=sumdp[y][r];
                    ull q=(f[x][i]*del)%mod,p=(c[k-1][i-1]*c[size[x]+size[y]-k][size[x]-i])%mod;
                    p*=q;p=p%mod;sum+=p;sum%=mod;
                }
                f[x][k]=sum;
            }
        }    
        size[x]+=size[y];
        }
    }
    for(int i=1;i<=size[x];i++)//处理完了x,赋值前缀和,以便后续使用 
        sumdp[x][i]=(sumdp[x][i-1]+f[x][i])%mod;    
}

void clear()//清空 
{
    cnt=0;
    for(int i=0;i<=n;i++)
    {
        head[i]=0;vis[i]=0;
        size[i]=1;
        for(int j=0;j<=n;j++)
         sumdp[i][j]=0,f[i][j]=0;
    }
}
int main()
{
    c[0][0]=1;
    for(int i=1;i<=1005;i++)
    {
        c[i][0]=1;
        for(int j=1;j<=i;j++)
         c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
    }//1000的范围,组合数打表 
    cin>>t;
    while(t)
    {
        scanf("%d",&n);
        clear();
        int x,y;
        char q[3];
        for(int i=1;i<=n-1;i++)
        {
            scanf("%d%s%d",&x,q,&y);
            x++,y++;//变成以1开始 
            if(q[0]=='<')
            {
                add(x,y,1);
                add(y,x,0);
            }
            else{
                add(y,x,1);
                add(x,y,0);
            }//建无向边,x,y距离是1,表示x<y 先过x后过 y 
        }
        dfs(1);
        ull ans=0;
        for(int i=1;i<=size[1];i++)
        {
            ans=(ans+f[1][i])%mod;
        }//方案数 
        printf("%llu\n",ans);
        t--;
    }
    return 0;
}

基本思路和代码参考shadowice1984https://www.luogu.org/blog/ShadowassIIXVIIIIV/solution-p4099

详细化了很多。

posted @ 2018-05-16 11:07  *Miracle*  阅读(332)  评论(0编辑  收藏  举报