• Codeforces 724F 【计数DP】


    题意

    (;)
    给定三个数(n,d,mod)。现在需要你求出,满足有(n)个节点,除了度数为(1)的节点,其它节点的度数都为(d)的不同构的树的数量。输出答案对(mod)取模的结果。

    [nleq 1000, ;dleq 10, ;10^8leq mod leq 10^9 ]

    Solution

    (;)
    一般来说,计数类问题需要满足的条件是不能重复,不能遗漏
    什么是不同构?如下三棵树就是同构的

    所以我们首先要确定一棵树的根,否则这棵树可能会被重复算多次。
    (;)

    确定根节点

    (;)
    但是根节点并不能随便的挑选一个。这里我们挑选重心作为根节点。
    原因就在于:重心在一棵树上是唯一的(当然有特殊情况是两个,这个我们后面会陈述),所以对于一棵树来说,它只会在以其重心为根时会被算一次,从而避免了算重复。
    而重心还满足什么性质?我们知道,重心将整棵树分成的联通块中最大的那个不超过整棵树大小的一半。
    而根据重心的这几条性质,我们就可以考虑进行DP。
    (;)

    考虑DP

    (;)
    状态设计:
    首先,因为我们最终要得到一颗(n)个节点的树,所以第一维状态表示这棵树有(i)个节点。
    其次,由于最终每个点的度数都为(d),所以第二维状态表示根节点有(j)棵子树。
    最后,由于我们要满足根节点是重心的条件,所以第三维状态表示根节点下面的子树大小都不超过(k)
    综上所述。(f_{i,j,k})表示(i)个节点,其中根节点有(j)棵子树,且子树大小都不超过(k)的树的数量。
    状态转移:
    由于树是不同构的,所以我们并不关心子树之间的相对顺序,所以我们DP的时候默认子树是从小到大排的。
    因为子树大小都是不超过(k)的,而若子树大小都小于(k),那么状态转移可得:(f_{i,j,k}=f_{i,j,k-1})
    否则说明有若干棵子树大小是等于(k)的,则我们枚举有(t)棵这样的子树,那么除去这(t)棵子树,剩下部分的状态即为(f_{i-t imes k,j-t,k-1})
    而由于子树之间是可以相同的。
    所以这(t)棵子树的方案数,实际就是从(f_{k,d-1,k-1})这么多数量中选出(t)棵且可以重复的方案数,即为:(C(f_{k,d-1,k-1}+t-1,t))
    那么状态转移可得:(f_{i,j,k}=f_{i-t imes k,j-t,k-1} imes C(f_{k,d-1,k-1}+t-1,t))
    (;)

    统计答案

    (;)
    直接输出(f_{n,d,lfloor frac{n}{2} floor})?那说明你太天真了~
    注意:这里的DP是基于一棵树只有一个重心的情况。而对于那些有双重心的树,是会被会算两遍的,所以我们要将这种情况减去一遍。
    那么我们来分析,什么时候会出现双重心的情况?
    显然,只有可能是一条边连接的两个点分别挂着两颗点数相同的子树,如下图:

    而我们发现,这种情况一定有偶数个节点。
    而对于这种情况的数量,如何求呢?
    因为是两个节点分别挂着两颗点数相同的子树,而这样树的数量显然就是(f_{n/2,d-1,n/2-1})
    而此时方案数即为从(f_{n/2,d-1,n/2-1})这么多数量选出不重复的两个,即为(C(f_{n/2,d-1,n/2-1},2))
    为什么这里必须是不重复?
    因为按照我们DP的过程,两边子树相同这样的情况只会被我们算一遍。(因为儿子是可以交换的)
    这个地方还需读者稍微理解一下。
    综上所述:
    (n)为奇数时,(Ans = f_{n,d,lfloor frac{n}{2} floor})
    (n)为偶数时,(Ans = f_{n,d,lfloor frac{n}{2} floor}-C(f_{n/2,d-1,n/2-1},2))
    那么整道题就做完了。
    但有一些DP的边界条件还是要多多注意,细节都在代码里。
    而且不要忘记要特判,当(nleq 2)时直接输出(1)即可
    (;)

    Code

    #include <bits/stdc++.h>
    const int N = 1010;
    int n, d, mod, f[N][20][N], fac[20], Inv[20];
    int ksm(int a, int b)
    {
    	int ans = 1;
    	while(b)
    	{
    		if(b & 1) ans = 1LL * ans * a % mod;
    		a = 1LL * a * a % mod;
    		b >>= 1;
    	}
    	return ans;
    }
    int C(int n, int m)
    {
    	if(m > n) return 0;
    	int ans = 1;
    	for(int i=n-m+1;i<=n;i++) ans = 1LL * ans * i % mod;
    	ans = 1LL * ans * Inv[m] % mod;
    	return ans;
    }
    int main()
    {
    	scanf("%d%d%d", &n, &d, &mod);
    	if(n == 1 || n == 2)
    	{
    		puts("1");
    		return 0;
    	}
    	fac[0] = Inv[0] = 1;
    	for(int i=1;i<=d;i++) fac[i] = 1LL * fac[i - 1] * i % mod;
    	Inv[d] = ksm(fac[d], mod - 2);
    	for(int i=d-1;i>=1;i--) Inv[i] = 1LL * Inv[i + 1] * (i + 1) % mod;
    	for(int i=0;i<=n;i++) f[1][0][i] = 1;
    	for(int i=2;i<=n;i++)
    	{
    		for(int j=1;j<=std::min(d,i-1);j++)
    		{
    			for(int k=1;k<=n;k++)
    			{
    				f[i][j][k] = f[i][j][k - 1];
    				for(int t=1;t<=j;t++)
    				{
    					if(i >= t * k && j >= t && k - 1 >= 0)
    					{
    						//printf("%d
    ", C(f[k][d - 1][k - 1] + t - 1, t));
    						if(k > 1)
    						f[i][j][k] = (f[i][j][k] + 1LL * f[i - t * k][j - t][k - 1] * 
    						C(f[k][d - 1][k - 1] + t - 1, t) % mod) % mod;
    						else
    						{
    							f[i][j][k] = (f[i][j][k] + 1LL * f[i - t * k][j - t][k - 1] * 
    							C(f[k][0][k - 1] + t - 1, t) % mod) % mod;
    						}
    					}
    				}
    			}
    		}
    	}
    	int res;
    	if(n & 1) 	
    		res = f[n][d][n / 2];
    	else
    		res = (f[n][d][n / 2] - C(f[n / 2][d - 1][n / 2 - 1], 2) + mod) % mod;
    	printf("%d", res);
    	return 0; 
    } 
    
  • 相关阅读:
    QSettings读写注册表、配置文件
    QSettings介绍
    桥接模式
    Qt之启动外部程序
    StringJDBC更改数据库的两种方式
    写代码注意事项
    maven仓库
    java关闭流,解压缩后的清除
    Java删除文件夹和文件
    浏览器禁用后退
  • 原文地址:https://www.cnblogs.com/czyty114/p/13474327.html
Copyright © 2020-2023  润新知