题意:

有一棵树包含N个节点,节点编号从1到N。节点总共有K种颜色,颜色编号从1到K。第i个节点的颜色为

表示恰好包含i种颜色的路径数量。请计算:

其中

思路:

做的时候完全无从下手QAQ

突破点是在这个k,由范围看出可以用状压,压什么呢?压路径中颜色的使用情况。

比如k=5,dp[10010]表示的就是路径中有第一种和第四种颜色的路径数(事实上表示的是只有第一种+只有第四种+恰好有第一种和第四种)。

那么知道了某一种状态,要如何求出路径数呢?

dfs一下,把有这些颜色的并连通的视为一个连通块,在连通块内部路径数为cnt*(cnt-1)/2(每两个不同的点之间一条路径),这里注意一个点也算一条路径,这个就放在最后进行计算。

所以枚举颜色的使用情况,进行dfs就可以算出dp。

之前括号里也说了这样子出来的dp表示的不是恰好有这些颜色的,而是有不超过这些颜色的路径总数。

所以这里就要用到容斥了。

ans表示的是恰好有这些颜色的路径数。

ans[111]

=dp[111]-ans[110]-ans[101]-ans[011]-ans[001]-ans[010]-ans[100]

=dp[111]-(dp[110]-dp[100]-dp[010])-(dp[101]-dp[100]-dp[001])-(dp[011]-dp[010]-dp[001])-dp[001]-dp[010]-dp[100]

=dp[111]-dp[110]-dp[101]-dp[011]+dp[001]+dp[010]+dp[100]

由此可以看出,如果子集和它相差的颜色是奇数的话,就减,如果相差的是偶数的话,就加。

最后再把一个结点的情况加上就可以了。

复杂度是

代码:

#define MOD 1000000007
typedef long long ll;
ll f[15];
int col[50010];
ll dp[1500];
vector<int>v[50010];
bool rcol[15];
bool vis[50010];
ll num[50010];
ll cnt=0;
ll ans[1500];
void dfs(int id)
{
    vis[id]=true;
    cnt++;
    for(int i=0;i<v[id].size();i++)
    {
        int temp=v[id][i];
        if(!vis[temp]&&rcol[col[temp]])
            dfs(temp);
    }
}
int main()
{
    int n,k,x,y;
    scanf("%d%d",&n,&k);
    f[0]=1;
    for(int i=1;i<=k;i++)
        f[i]=(f[i-1]*131)%MOD;
    for(int i=1;i<=n;i++)
        scanf("%d",&col[i]);
    for(int i=0;i<n-1;i++)
    {
        scanf("%d%d",&x,&y);
        v[x].push_back(y);
        v[y].push_back(x);
    }
    memset(dp,0,sizeof(dp));
    for(int i=1;i<(1<<k);i++)
    {
        memset(vis,0,sizeof(vis));
        memset(rcol,0,sizeof(rcol));
        cnt=0;
        for(int j=1;j<=k;j++)
        {
            if(1<<(j-1)&i)
            {
                rcol[j]=true;
                cnt++;
            }
        }
        num[i]=cnt;
        /*for(int i=1;i<=k;i++)
            printf("%d ",rcol[i]);
        printf("\n");*/
        for(int j=1;j<=n;j++)
        {
            if(!vis[j]&&rcol[col[j]])
            {
                cnt=0;
                dfs(j);
                //printf("(%d %d %lld)\n",i,j,cnt);
                dp[i]+=(cnt*(cnt-1)/2)%MOD;
            }
        }
    }
    /*for(int i=1;i<(1<<k);i++)
        printf("%lld ",num[i]);
    printf("\n");
    for(int i=1;i<(1<<k);i++)
        printf("%lld ",dp[i]);
    printf("\n");*/
    for(int i=1;i<(1<<k);i++)
    {
        ans[i]=dp[i];
        for(int j=1;j<(1<<k);j++)//j是i的子集
        {
            if((i|j)==i&&i!=j)
            {
                //printf("%d %d\n",i,j);
                if((num[i]-num[j])%2==1)ans[i]=(ans[i]-dp[j]+MOD)%MOD;
                else ans[i]=(ans[i]+dp[j])%MOD;
            }
        }
    }
    /*for(int i=1;i<(1<<k);i++)
        printf("%lld ",ans[i]);
    printf("\n");*/
    ll aans=0;
    for(int i=1;i<(1<<k);i++)
        aans=(aans+(ans[i]*f[num[i]])%MOD)%MOD;
    aans=(aans+n*f[1])%MOD;
    printf("%lld\n",aans);
    return 0;
}

总结:

看到范围小的,可以想到状压也可以想到全排列。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

莫队算法 Previous
宁夏邀请赛H & 计蒜之道复赛A 贪心 Next