[BZOJ 1494][NOI 2007]生成树计数(状压DP+最小表示法+矩阵乘法)

142 篇文章 0 订阅
59 篇文章 0 订阅

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=1494

思路

由于每个点 i 只能和点iK i1 连边,因此点 iK+1 和其前面的所有点一定是都在同一个联通块。因此我们只需要用一个长度为 K 的序列来表示点iK+1到点 i 的连通性,而其他的点一定是连通的,就不必表示连通性了。这个连通性的序列可以用最小表示法压缩成一个K K 进制数S。用 f[i][S] 表示前 i 个点,[iK+1,i]的连通性状态为 S 的生成树个数。可以得到下面的方程

f[i][S]=SlastSf[i1][Slast]

初始DP边界为

f[K][S]=numnum2,numS

显然这个DP可以通过矩乘优化。假设当前共有 m 种不同的连通性状态。构造一个1行m列的矩阵 A ,其中第i列上的数字代表初始的 f[K][Si] ,再构造一个 m m列的矩阵 mp ,代表状态之间的转移,其中 i j列的元素表示从 Si 转移到 Sj 的方案数。
我们可以先用一个DFS预处理出所有可能出现的连通性的状态。然后再枚举连通性状态 S 以及下一个点和S里的 K 个点中的哪些点连边,再判断从连通性状态S转移出来的新状态 S 是否是合法的,若合法,在邻接矩阵里,标记从 S S的方案数加1

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <map>

#define MAXN 200
#define MOD 65521

using namespace std;

typedef long long int LL;

int tot=0; //状态总数
int sizeoftree[]={1,1,1,3,16,125}; //sizeoftree[i]=点的个数为i的完全图的生成树个数
int K;
LL n; //!!!!!

struct Matrix
{
    LL num[MAXN][MAXN]; //!!!!!!
    int n,m;
    Matrix()
    {
        n=m=0;
        memset(num,0,sizeof(num));
    }
}one,mp,A;

Matrix operator*(Matrix a,Matrix b)
{
    Matrix c;
    c.n=a.n,c.m=b.m;
    for(int k=1;k<=a.m;k++)
        for(int i=1;i<=c.n;i++)
            for(int j=1;j<=c.m;j++)
                c.num[i][j]=(c.num[i][j]+a.num[i][k]*b.num[k][j]%MOD)%MOD;
    return c;
}

Matrix fastPow(Matrix base,LL pow)
{
    Matrix ans=one;
    while(pow)
    {
        if(pow&1) ans=ans*base;
        base=base*base;
        pow>>=1;
    }
    return ans;
}

int num[MAXN],status[600],hash[1<<16]; //hash[S]=状态S的编号

void DFS(int pos,int sta) //当前要加入第pos个点的联通状态,当前的状态为sta
{
    if(pos==K+1)
    {
        memset(num,0,sizeof(num)); //num[i]=最小表示法里第i个联通块的大小
        A.num[1][++tot]=1;
        for(int i=1;i<=K;i++)
            num[sta>>((i-1)*3)&7]++;
        for(int i=0;i<K;i++)
            A.num[1][tot]*=sizeoftree[num[i]];
        status[tot]=sta;
        hash[sta]=tot;
        return;
    }
    int tmp=-1; //联通块的最大编号,联通块编号的区间是[0,K-1]
    for(int i=1;i<pos;i++) //!!!当前的sta里只保存了1~pos-1这些点的连通性
        tmp=max(tmp,sta>>((i-1)*3)&7);
    for(int i=0;i<=tmp+1&&i<K;i++)
        DFS(pos+1,sta<<3|i);
}

int f[MAXN];

int findSet(int x)
{
    if(f[x]==x) return x;
    return f[x]=findSet(f[x]);
}

int getstatus() //用当前的并查集来求出新的点2到点k+1的最小表示
{
    int sta=0,tot=0;
    bool vis[MAXN];
    memset(vis,false,sizeof(vis));
    for(int i=K+1;i>=2;i--)
    {
        if(!vis[i])
        {
            vis[i]=true;
            sta|=tot<<((i-2)*3);
            for(int j=i-1;j>=2;j--)
            {
                if(findSet(i)==findSet(j)) //i和j在同一联通块
                {
                    vis[j]=true;
                    sta|=tot<<((j-2)*3); //!!!!!
                }
            }
            tot++;
        }
    }
    return hash[sta];
}

void calc(int sta,int addsta) //用加边状态addsta去更新最小表示法sta,addsta里的第i位为1表示第k+1个点要和点i+1连新边
{
    for(int i=0;i<=K+1;i++) f[i]=i;
    for(int i=1;i<=K;i++) //枚举点对(i,j)是否在最小表示法里的同一联通块内,将最小表示法中的连通性用并查集表示
    {
        for(int j=i+1;j<=K;j++)
            if((status[sta]>>((i-1)*3)&7)==(status[sta]>>((j-1)*3)&7))
            {
                int rooti=findSet(i);
                int rootj=findSet(j);
                if(rooti!=rootj)
                    f[rooti]=rootj;
            }
    }
    for(int i=1;i<=K;i++)
        if(addsta&(1<<(i-1)))
        {
            int rooti=findSet(i);
            int rootj=findSet(K+1);
            if(rooti==rootj) return; //加的新边的两端点原来就是联通的,加入新边后会出现环
            f[rooti]=rootj;
        }
    bool flag=false; //flag=true表示有点和点1联通
    for(int i=2;i<=K+1;i++)
        if(findSet(i)==findSet(1))
        {
            flag=true;
            break;
        }
    if(!flag) return; //点1不链接后面的点,那么这个生成树不联通
    mp.num[sta][getstatus()]++;
}

int main()
{
    scanf("%d%lld",&K,&n);
    DFS(1,0);
    A.n=1;
    A.m=mp.n=mp.m=one.n=one.m=tot;
    for(int i=1;i<=tot;i++) one.num[i][i]=1;
    for(int i=1;i<=tot;i++)
        for(int addsta=0;addsta<(1<<K);addsta++)
            calc(i,addsta);
    A=A*fastPow(mp,n-K);
    printf("%lld\n",A.num[1][1]);
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值