BZOJ 1494 [NOI2007]生成树计数

BZOJ 1494 [NOI2007]生成树计数

题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1494

题目大意: 给定 n 个点的无向图 。节点编号1......n
i 个点与第j个点有边.当且仅当:ijk
计算 n 个点时图的生成树数量。2k5  , 2n1015
答案 mod 65521

所以我们基于连通性进行递推。并通过构造矩阵进行快速幂。

为什么要基于连通性。如何表示状态。
首先:
当我们考虑加入一个新的点时。如果它不是我们加入的最后一个点。那么我们没有必要一定要使得加入这个点后 。建立一些边后。这个森林会成为一个树。
但我们需要使得更远点。与最后 k 个点有链接。

如果更远的点与最后k个点没有链接。新加入的点又不可能与之建立联系。

导致更远的点断开了链接。就不可能生成一个树。

其次。连通性其实就是特征。根据不同的连通性决定的一类森林进行转移。相同连通性的森林转移到另一连通性时。方案是一样的。

所以可以基于连通性进行递推。

那么如何表示后k点的连通性呢。

我们用 k 进制数字来表示。[0,k1]
k 个节点的第t个节点所在集合编号就是这个数字的第 t 位数码。

可能会出现重复状态:

例如:
0 1 11 2 2

这两个状态是一样的。
那么我们调整对状态表示的定义。对于确定的连通性。
某一点所在集合的编号如何确定是我们关心的。
那么不如所有集合中最早出现的点的名次作为集合编号。从 0 开始编号。

例如:
2 1 2 3 0

他的状态就是:
0 1 0 2 3

我们计算出所有可能的状态。并按顺序对这些状态编号。从0开始。

那么剩下的是构造转移矩阵:
对于状态 a b转移的方案数量我们记为: M[b][a]
显然之前在一个联通块的点不可能分开。
所以如果在 a 中是连通的在b中变为不连通是不可能的.这时候转移方案是0。
其次,如果在 a 中是不连通的。在b中连通的。这时候。如何两个点与最新的点不在一个集合。那也是不可能的。之所以连通只可能是新的点建立 了链接。所以此时方案也是0
除了这两个情况之外。我们开始计算转移方案:
对于与新的点在一个联通块的点。之前状态所在的集合在 k 个点中占有的点数就是新点与之链接的方案数。根据乘法原理:
M[b][a]=c1c2..cr
这表示让不相关的 r 个集合连通的方案只需要分开计算,相乘即可。

其次。构造出来的矩阵无法从0开始递推。

因为在n<k时情况的特殊性。

我们需要计算。 k 个点时。在不同连通状态下的方案数。作为右侧向量。

k递推到 n

对于这一步。与之前的想法差不多。对于第i个连通块的方案。

第一步:构造关于第i个连通块的子图。
第二步:构造关于这个子图的基尔霍夫矩阵。
第三步:对这个矩阵的 n1 阶进行对角化或变为上下三角。
第四步:对角化后计算对角线的乘机即位方案数。(即行列式的值)
单独计算每个连通块的搭建方案后乘即可。
这样就可以愉快的矩阵快速幂了。
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <cmath>
#define MAXN 1000
using namespace std;
typedef long long LL;
const int mod=65521;

int T[100][6],dep;
int D[100];
bool vis[6];
int C[6];
int A[5];
int M[60][60];
int E[10][10];
int tmE[10][10];
int tma[6],tmb[6];

int cmp(int k,int a,int b)  //a-->b
{
    for(int i=0;i<k;i++)
    {
        tma[i]=T[a][i];
        tmb[i]=T[b][i];
    }
    for(int i=0;i<k-1;i++)
        for(int j=i+1;j<k-1;j++)
        {
            if(tma[i+1]==tma[j+1]&&tmb[i]!=tmb[j])return 0;
            if(tma[i+1]!=tma[j+1]&&tmb[i]==tmb[j]&&tmb[i]!=tmb[k-1])return 0;
        }
    memset(vis,0,sizeof vis);
    int ans=1;
    for(int i=0;i<k-1;i++)
    {
        if(tmb[i]!=tmb[k-1])continue;
        if(vis[tma[i+1]])   continue;
        vis[tma[i+1]]=true;
        int c=0;
        for(int j=0;j<k;j++)
            if(tma[j]==tma[i+1])c++;
        ans=(LL)ans*c%mod;
    }
    return ans;
}

struct mat
{
    int m[60][60];
    mat()
    {
        memset(m,0,sizeof m);
    }
    void e()
    {
        for(int i=0;i<60;i++)m[i][i]=1;
    }
    mat operator *(const mat & a)const
    {
        mat b;
        for(int i=0;i<dep;i++)
            for(int j=0;j<dep;j++)
                for(int k=0;k<dep;k++)
                    b.m[i][j]=((LL)b.m[i][j]+(LL)m[i][k]*a.m[k][j])%mod;
        return b;
    }
};

int Iv[mod+10];

int Kirchhof(int n)
{
    n--;
    int r=0;
    for(int i=0; i<n ;i++)
    {
        for(int j=r; j<n ;j++)
            if(tmE[j][i]>0)
            {
                for(int k=i;k<n;k++)
                    swap(tmE[j][k],tmE[r][k]);
                break;
            }
        if(tmE[r][i]==0)continue;
        for(int j=0;j<n;j++)
            if(j!=r&&tmE[j][i]>0)
            {
                int tm=(LL)tmE[j][i]*Iv[tmE[r][i]]%mod;
                for(int k=i;k<n;k++)
                    tmE[j][k]=(tmE[j][k]-(LL)tm*tmE[r][k]%mod+mod)%mod;
            }
        r++;
    }
    int ans=1;
    for(int i=0;i<n;i++)
        ans=(LL)ans*tmE[i][i]%mod;
    return ans;
}

int B[100];

int clat(int k,int d)
{
    int ans=1;
    memset(vis,0,sizeof vis);
    for(int i=0;i<k;i++)
    {
        if(vis[T[d][i]])    continue;
        vis[T[d][i]]=true;
        memset(tmE,0,sizeof tmE);
        int u=0;
        for(int j=0;j<k;j++)
            if(T[d][j]==T[d][i])C[u++]=j;
        for(int j=0;j<k;j++)
            for(int h=0;h<k;h++)
            {
                if(T[d][j]!=T[d][i])continue;
                if(T[d][h]!=T[d][i])continue;
                int a=(int)(lower_bound(C,C+u,j)-C);
                int b=(int)(lower_bound(C,C+u,h)-C);
                tmE[a][b]=E[j][h];
            }
        for(int j=0;j<u;j++)
            for(int h=0;h<u;h++)
                if(j!=h)
                    tmE[j][j]=(tmE[j][j]-tmE[j][h]+mod)%mod;
        ans=(LL)ans*Kirchhof(u)%mod;
    }
    return ans;
}

int slove(LL n,int k)
{
    if(n<=(LL)k)
    {
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                tmE[i][j]=E[i][j];
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                if(i!=j)
                    tmE[i][i]=(tmE[i][i]-tmE[i][j]+mod)%mod;
        return Kirchhof((int)n);
    }
    mat a,tmp;
    tmp.e();
    for(int i=0;i<dep;i++)
        for(int j=0;j<dep;j++)
            a.m[i][j]=M[i][j];
    n-=k;
    while(n)
    {
        if(n&1)
            tmp=tmp*a;
        a=a*a;
        n>>=1;
    }
    int ans=0;
    for(int i=0;i<dep;i++)
        ans=(ans+(LL)tmp.m[0][i]*clat(k,i)%mod)%mod;
    return ans;
}

int main ()
{
    Iv[1]=1;
    for(int i=2;i<mod;i++)  Iv[i]=mod-(LL)(mod/i)*Iv[mod%i]%mod;
    LL n;
    int k;
    scanf("%d %lld",&k,&n);
    int size=0,ten=1,bi=0;

    while(bi<k)
    {
        ten*=10;
        size=size*10+(k-1);
        bi++;
    }

    ten/=10;
    size+=1;
    for(int i=0;i<size;i++)
    {
        for(int K=0,u=ten,d=i;K<k;K++)
        {
            A[K]=d/u;
            d=d%u;
            u/=10;
        }
        int cnt=0,flag=false;
        for(int K=0;K<k;K++)
        {
            if(A[K]>cnt)
            {
                flag=true;
                break;
            }
            if(A[K]==cnt)   cnt++;
        }
        if(flag)continue;
        for(int t=0;t<k;t++)    T[dep][t]=A[t];
        dep++;
    }
    for(int i=0;i<dep;i++)
        for(int j=0;j<dep;j++)
            M[i][j]=cmp(k,j,i);
    for(int i=0;i<10;i++)
        for(int j=0;j<10;j++)
        {
            if(i==j)continue;
            if(abs(j-i)<=k)E[i][j]=mod-1;
        }
    printf("%d\n",slove(n,k));
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值