zoukankan      html  css  js  c++  java
  • 【BZOJ1494】【NOI2007】生成树计数(动态规划,矩阵快速幂)

    【BZOJ1494】【NOI2007】生成树计数(动态规划,矩阵快速幂)

    题面

    Description

    最近,小栋在无向连通图的生成树个数计算方面有了惊人的进展,他发现:

    ·n个结点的环的生成树个数为n。

    ·n个结点的完全图的生成树个数为n^(n-2)。这两个发现让小栋欣喜若狂,由此更加坚定了他继续计算生成树个数的

    想法,他要计算出各种各样图的生成树数目。一天,小栋和同学聚会,大家围坐在一张大圆桌周围。小栋看了看,

    马上想到了生成树问题。如果把每个同学看成一个结点,邻座(结点间距离为1)的同学间连一条边,就变成了一

    个环。可是,小栋对环的计数已经十分娴熟且不再感兴趣。于是,小栋又把图变了一下:不仅把邻座的同学之间连

    一条边,还把相隔一个座位(结点间距离为2)的同学之间也连一条边,将结点间有边直接相连的这两种情况统称

    为有边相连,如图1所示。

    img

    img

    小栋以前没有计算过这类图的生成树个数,但是,他想起了老师讲过的计算任意图的生成树个数的一种通用方法:

    构造一个n×n的矩阵A={aij},其中

    img

    img

    img

    img

    其中di表示结点i的度数。与图1相应的A矩阵如下所示。为了计算图1所对应的生成数的个数,只要去掉矩阵A的最

    后一行和最后一列,得到一个(n-1)×(n-1)的矩阵B,计算出矩阵B的行列式的值便可得到图1的生成树的个数所以

    生成树的个数为|B|=3528。小栋发现利用通用方法,因计算过于复杂而很难算出来,而且用其他方法也难以找到更

    简便的公式进行计算。于是,他将图做了简化,从一个地方将圆桌断开,这样所有的同学形成了一条链,连接距离

    为1和距离为2的点。例如八个点的情形如下:

    img

    img

    这样生成树的总数就减少了很多。小栋不停的思考,一直到聚会结束,终于找到了一种快捷的方法计算出这个图的

    生成树个数。可是,如果把距离为3的点也连起来,小栋就不知道如何快捷计算了。现在,请你帮助小栋计算这类

    图的生成树的数目。

    Input

    包含两个整数k,n,由一个空格分隔。k表示要将所有距离不超过k(含k)的结点连接起来,n表示有n个结点。

    Output

    输出一个整数,表示生成树的个数。由于答案可能比较大,所以你 只要输出答案除65521 的余数即可。

    Sample Input

    3 5

    Sample Output

    75

    HINT

    img img

    题解

    这是一道神仙题啊。
    鉴于我自己在网上研究了很久各种题解才知道怎么做。
    我还是打算好好地把这道题目从头到尾写一写。
    从部分分开始吧。


    (Task1 60pts:kle 5,nle 100)
    送分的一档
    直接暴力构图,矩阵树定理直接算就好了
    时间复杂度(O(n^3))

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<vector>
    #include<queue>
    using namespace std;
    #define ll long long
    #define RG register
    #define MOD 65521
    #define MAX 111
    inline int read()
    {
        RG int x=0,t=1;RG char ch=getchar();
        while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
        if(ch=='-')t=-1,ch=getchar();
        while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
        return x*t;
    }
    int n,K,c[MAX][MAX],ans=1;
    int main()
    {
        K=read();n=read();
        for(int i=1;i<=n;++i)
            for(int j=i+1;j<=min(n,i+K);++j)
                c[i][i]++,c[j][j]++,c[i][j]--,c[j][i]--;
        for(int i=2;i<=n;++i)
            for(int j=i+1;j<=n;++j)
                while(c[j][i])
                {
                    int t=c[i][i]/c[j][i];
                    for(int k=i;k<=n;++k)c[i][k]=(c[i][k]-1ll*c[j][k]*t%MOD)%MOD,swap(c[i][k],c[j][k]);
                    ans=-ans;
                }
        for(int i=2;i<=n;++i)ans=1ll*ans*c[i][i]%MOD;
        printf("%d
    ",ans);
        return 0;
    }
    

    (Task2 80pts:kle 5,nle 10000)
    这档分其实写出来基本就会满分了
    发现(k)的范围十分的小,也就是每个点的边并不会很多
    换而言之,如果一个点要和前面的所有点处在一个生成树中
    意味着它必须和前面(k)个点中的至少一个处在同一个联通块中
    所以,我们只需要考虑当前点和它前面(K)个点的状态就行了

    那么,我们的状态是什么?
    我们思考一下,对于生成树而言,我们比较在意的只有两个方面:联通块和边
    对于相邻的(K)个点,单独拿出来显然是一个完全图,因此不需要考虑边的问题
    所以,我们的状态和联通块有关。
    现在的问题又变成了如何记录前面(k)个点的联通块呢?
    我们可以按照顺序编号,保证任意一个编号的联通块在出现之前,所有小于它的编号都已经出现过。(最小表示法吼啊)
    (不一定要这样,只需要一种能够保证所有相同的联通块方案只会被算一次就行了)
    编完号之后,我们可以把前面(k)个点所在的联通块给压成十进制
    因为(kle 5),就只需要三个二进制位,所以可以用八进制来把联通块的情况给压成十进制位。
    那么,不同的联通块方案数有多少呢?
    相当于把(n)个球放进任意数量个集合,也就是(Bell)数,算出来是(52)

    我们不关心每个联通块之间的连接方案,我们只关心如何对于两种联通块之间进行转移
    所以我们可以枚举当前点和前面(k)个点的连边方案(最多就(k)条边)
    然后暴力(用并查集)判断是否成环,
    同时,最前面的那个点也必须和当前这(k)个点中的一个在同一个联通块中
    这样就可以转移到另外一个联通块的情况
    再用上面的最小表示法把它的编号还原出来
    这样证明可以从当前状态向后面的状态进行转移。

    (dp)要的是初始状态和转移。
    显然搞出来了转移,考虑初始状态。
    显然不能一开始不满(k)个点
    所以直接从(k)个点开始计算
    因为每个联通块之间是完全图,所以可以暴力计算联通块大小为(x)时的方案数
    那么(k)个点时,某个联通块情况的方案数就是(prod num_{size})
    联通块大小为(1,2)时,有(1)种方法
    联通块大小为(3)(3)
    联通块大小为(4)(16)
    联通块大小为(5)(125)种。

    这样子,我们就可以(O(52*2^5*K^2+n*52^2))转移了
    这样子可以过(80pts)

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<vector>
    #include<queue>
    using namespace std;
    #define ll long long
    #define RG register
    #define MOD 65521
    #define MAX 55
    inline ll read()
    {
        RG ll x=0,t=1;RG char ch=getchar();
        while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
        if(ch=='-')t=-1,ch=getchar();
        while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
        return x*t;
    }
    ll n;int K,cnt;
    int p[1<<20],st[MAX];
    bool check(int t)//检查一个状态是否合法
    {
        int tmp=1<<1;//因为第一个点一定属于一号联通快,所以先把一号联通快放进去检查
        for(int i=3;i<K+K+K;i+=3)
        {
            for(int j=1;j<((t>>i)&7);++j)//检查比当前编号小的所有编号是否都已经出现过
                if(!(tmp&(1<<j)))return false;
            tmp|=1<<((t>>i)&7);//将当前编号也给放进来
        }
        return true;
    }
    void dfs(int x,int t)//暴力找出所有状态,每个编号利用3个二进制位存
    {
        if(x==K){if(check(t))p[t]=++cnt,st[cnt]=t;return;}
        for(int i=1;i<=K;++i)dfs(x+1,t|(i<<(x+x+x)));
    }
    int fa[MAX],a[MAX];
    int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
    int f[11111][MAX],g[MAX][MAX];
    int main()
    {
        K=read();n=read();dfs(1,1);
        for(int i=1;i<=cnt;++i)
        {
            f[K][i]=1;memset(a,0,sizeof(a));
            for(int j=0;j<K;++j)++a[(st[i]>>(j*3))&7];
            for(int j=1;j<=K;++j)
                if(a[j]==3)f[K][i]=3;
                else if(a[j]==4)f[K][i]=16;
                else if(a[j]==5)f[K][i]=125;
            int t=st[i];
            for(int s=0;s<(1<<K);++s)//暴力枚举当前点对于前面几个点的连边状态
            {
                for(int j=0;j<=K;++j)fa[j]=j;
                for(int j=0;j<K;++j)//利用并查集维护联通性
                    for(int k=j+1;k<K;++k)
                        if(((t>>(3*j))&7)==((t>>(3*k))&7))
                            fa[getf(j)]=getf(k);
                bool cir=false;
                for(int j=0;j<K;++j)//检查当前点的连边
                    if(s&(1<<j))
                    {
                        if(getf(K)==getf(j)){cir=true;break;}//出现了环
                        fa[getf(K)]=getf(j);
                    }
                if(cir)continue;//连边不合法
                for(int j=1;j<=K;++j)//最前面的点必须和后面的一个点联通,否则就无法联通了
                    if(getf(0)==getf(j)){cir=true;break;}
                if(!cir)continue;
                int now=0,used=0;
                for(int j=0;j<K;++j)//当前存在合法的联通方案,因此当前的状态可以转移到另外的一个状态上去
                    if(!(now&(7<<(j*3))))//当前点不在任意一个联通块中
                    {
                        now|=++used<<(j*3);//新的联通块
                        for(int k=j+1;k<K;++k)//把所有在一个联通块里的点丢到状态里去
                            if(getf(j+1)==getf(k+1))
                                now|=used<<(k*3);
                    }
                g[i][p[now]]++;
            }
        }
        for(int i=K+1;i<=n;++i)
            for(int j=1;j<=cnt;++j)
                for(int k=1;k<=cnt;++k)
                    if(g[j][k])f[i][k]=(f[i][k]+1ll*g[j][k]*f[i-1][j])%MOD;
        printf("%d
    ",f[n][1]);
        return 0;
    }
    
    

    (AC:nle 10^{15})
    明摆着(log)算法,
    发现每次转移相同,直接矩阵快速幂就行了
    时间复杂度(O(logn·52^3)),预处理的时间写在上面了。。

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<vector>
    #include<queue>
    using namespace std;
    #define ll long long
    #define RG register
    #define MOD 65521
    #define MAX 55
    ll n;int K,cnt;
    int p[1<<20],st[MAX];
    struct Matrix
    {
    	int s[MAX][MAX];
    	void clear(){memset(s,0,sizeof(s));}
    	void init(){clear();for(int i=1;i<=cnt;++i)s[i][i]=1;}
    }G;
    Matrix operator*(Matrix a,Matrix b)
    {
    	Matrix ret;ret.clear();
    	for(int i=1;i<=cnt;++i)
    		for(int j=1;j<=cnt;++j)
    			for(int k=1;k<=cnt;++k)
    				ret.s[i][j]=(ret.s[i][j]+1ll*a.s[i][k]*b.s[k][j])%MOD;
    	return ret;
    }
    Matrix fpow(Matrix a,ll b)
    {
    	Matrix s;s.init();
    	while(b){if(b&1)s=s*a;a=a*a;b>>=1;}
    	return s;
    }
    bool check(int t)//检查一个状态是否合法
    {
    	int tmp=1<<1;//因为第一个点一定属于一号联通快,所以先把一号联通快放进去检查
    	for(int i=3;i<K+K+K;i+=3)
    	{
    		for(int j=1;j<((t>>i)&7);++j)//检查比当前编号小的所有编号是否都已经出现过
    			if(!(tmp&(1<<j)))return false;
    		tmp|=1<<((t>>i)&7);//将当前编号也给放进来
    	}
    	return true;
    }
    void dfs(int x,int t)//暴力找出所有状态,每个编号利用3个二进制位存
    {
    	if(x==K){if(check(t))p[t]=++cnt,st[cnt]=t;return;}
    	for(int i=1;i<=K;++i)dfs(x+1,t|(i<<(x+x+x)));
    }
    int fa[MAX],a[MAX];
    int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
    int f[MAX],g[MAX][MAX];
    int main()
    {
    	scanf("%d%lld",&K,&n);dfs(1,1);
    	for(int i=1;i<=cnt;++i)
    	{
    		f[i]=1;memset(a,0,sizeof(a));
    		for(int j=0;j<K;++j)++a[(st[i]>>(j*3))&7];
    		for(int j=1;j<=K;++j)
    			if(a[j]==3)f[i]=3;
    			else if(a[j]==4)f[i]=16;
    			else if(a[j]==5)f[i]=125;
    		int t=st[i];
    		for(int s=0;s<(1<<K);++s)//暴力枚举当前点对于前面几个点的连边状态
    		{
    			for(int j=0;j<=K;++j)fa[j]=j;
    			for(int j=0;j<K;++j)//利用并查集维护联通性
    				for(int k=j+1;k<K;++k)
    					if(((t>>(3*j))&7)==((t>>(3*k))&7))
    						fa[getf(j)]=getf(k);
    			bool cir=false;
    			for(int j=0;j<K;++j)//检查当前点的连边
    				if(s&(1<<j))
    				{
    					if(getf(K)==getf(j)){cir=true;break;}//出现了环
    					fa[getf(K)]=getf(j);
    				}
    			if(cir)continue;//连边不合法
    			for(int j=1;j<=K;++j)//最前面的点必须和后面的一个点联通,否则就无法联通了
    				if(getf(0)==getf(j)){cir=true;break;}
    			if(!cir)continue;
    			int now=0,used=0;
    			for(int j=0;j<K;++j)//当前存在合法的联通方案,因此当前的状态可以转移到另外的一个状态上去
    				if(!(now&(7<<(j*3))))//当前点不在任意一个联通块中
    				{
    					now|=++used<<(j*3);//新的联通块
    					for(int k=j+1;k<K;++k)//把所有在一个联通块里的点丢到状态里去
    						if(getf(j+1)==getf(k+1))
    							now|=used<<(k*3);
    				}
    			g[i][p[now]]++;
    		}
    	}
    	for(int i=1;i<=cnt;++i)
    		for(int j=1;j<=cnt;++j)
    			G.s[i][j]=g[i][j];
    	G=fpow(G,n-K);
    	int ans=0;
    	for(int i=1;i<=cnt;++i)
    		ans=(ans+1ll*G.s[i][1]*f[i])%MOD;
    	printf("%d
    ",ans);
    	return 0;
    }
    
    
  • 相关阅读:
    nginx变量(日志log_format)
    nginx中间件
    social-auth-app-django集成第三方登录
    Django REST Framework限速
    django-redis
    Django REST Framework extensions
    django-rest-framework-jwt
    FromXml 支付回调 xml 转数组
    下个月此时
    PHP 暂停函数 sleep() 与 usleep() 的区别
  • 原文地址:https://www.cnblogs.com/cjyyb/p/9154673.html
Copyright © 2011-2022 走看看