[BZOJ2873]-光之大陆-计数dp

说在前面

并没有什么想说的,但是要保持格式=w=


题目

BZOJ2873传送门

题目大意

N N 个点的「带环的树」计数。环与环不能共点,树不能有重边。答案对 M 取模
N200,M106 N ≤ 200 , M ≤ 10 6

输入输出格式

输入格式:
一行两个数字 N,M N , M

输出格式:
输出一个数字表示答案


解法

(很早之前就见过这道题,题面很赞hhhh,然而me并不会于是去看的题解)
题目让我们统计,环不共点的带环树的数量。带环树肯定不好统计,于是想办法向树转化

我们可以这么想这棵带环树,这棵树相当于是由一些「点块」拼接起来的。「点块」,要么是单个点,要么是一个环
那么「点块」之间的拼接方案,也就是 n n 个点形成了 m 个块的生成树计数。由prufer 我们可以知道, n n 个点的生成树计数是 nn2,类比过来可以知道, n n 个点 m 个块的生成树数量是 nm2 n m − 2 (关于Prufer下面有补充)

那么现在处理完了块与块之间连接的方案数,再来考虑分块的方案数
定义 dp[i][j] d p [ i ] [ j ] 表示,把前 i i 个点分成 j 块的方案数。考虑转移,与图的dp类似,枚举当前块的大小,然后考虑有哪些点和当前点在同一个环,可得转移: dp[i][j]=dp[ik][j1]Ck1i1f[k] d p [ i ] [ j ] = ∑ d p [ i − k ] [ j − 1 ] ∗ C i − 1 k − 1 ∗ f [ k ]
其中 f[k] f [ k ] k k 个点组成的环的方案数,f[k]=(k1)!2k,特别的 f[2]=0 f [ 2 ] = 0 (因为不存在大小为2的块)

Ck1i1 C i − 1 k − 1 就是,除了当前点之外,从剩下的 i1 i − 1 个点选 k1 k − 1 个点,与当前点构成一个环的方案数。然后使用 f[k] f [ k ] 给选出来的这些点分配标号,首先是个环排列,但是这个环是可以对称的,于是有 (k1)!2 ( k − 1 ) ! 2 ,再加上环的朝向有 k k 种,所以得出 f[k]

关于环的朝向问题,比较抽象,建议多想一想(比如下图,环之间的连接方式相同,编号相同,但是朝向不同):
这里写图片描述
另外,对于 n n 个点都在一个环上的情况,需要单独统计
然后这题就做完了


关于Prufer的一点补充(附Matrix67传送门
每一棵 n 个节点的树都可以唯一的对应到 n2 n − 2 长度的prufer上去,而每个Prufer都一定可以还原到一棵树上来,也即每一个Prufer编码都和一棵树唯一对应。又因为Prufer编码的 n2 n − 2 个数都可以取 1 1 n 中的任意一个数字,因此 n n 个节点的树有 nn2 种。(这道题也是一样的,每次去掉一个块,因此只有 m2 m − 2 个数字,但是数字可以在 1 1 n 中任取)

Q:为什么编码长度不能是 n1 n − 1 呢?这样的话岂不是就有 nn1 n n − 1 种了?
A:其实,当已经编码了前 n2 n − 2 个数字的时候,第 n1 n − 1 个数字就已经唯一确定了(因为根据编码过程,第 n1 n − 1 个数字一定是剩下的较小的那个数字,所以长度为 n2 n − 2 就已经足够了)


下面是自带大常数的代码

随手写了一发Rk1了= =

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , P ;
long long dp[205][205] , C[205][205] , hp[205] , ans ;

void preWork(){
    for( int i = 0 ; i <= N ; i ++ ){
        C[i][0] = 1 ;
        for( int j = 1 ; j <= i ; j ++ )
            C[i][j] = ( C[i-1][j-1] + C[i-1][j] ) %P ;
    } hp[1] = 1 , hp[3] = 3 ;
    for( int i = 4 ; i <= N ; i ++ ) hp[i] = hp[i-1] * i %P ;
}

void solve(){
    dp[0][0] = 1 ;
    short i , j , k ;
    for( i = 1 ; i <= N ; i ++ ){ // 点数
        for( j = 1 ; j <= i ; j ++ ){ // 块数
            for( k = 1 ; k <= i - j + 1 ; k ++ ) // 当前环
                dp[i][j] += dp[i-k][j-1] * hp[k] * C[i-1][k-1]%P ;
            dp[i][j] %= P ;
        //  printf( "dp[%d][%d] = %lld\n" , i , j , dp[i][j] ) ;
        }
    } ans = hp[N-1] ; // 单独累计只有一个环的情况
    long long tmp = 1 ;
    for( int i = 2 ; i <= N ; i ++ ){
        ans += dp[N][i] * tmp ;
        tmp = tmp * N %P ;
    } printf( "%lld" , ans %P ) ;
}

int main(){
    scanf( "%d%d" , &N , &P ) ;
    preWork() ; solve() ;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值