51NOD 1806 wangyurzee的树(容斥原理 + 组合数学)

传送门
wangyurzee有n个各不相同的节点,编号从1到n。wangyurzee想在它们之间连n-1条边,从而使它们成为一棵树。
可是wangyurzee发现方案数太多了,于是他又给出了m个限制条件,其中第i个限制条件限制了编号为u[i]的节点的度数不能为d[i]。
一个节点的度数,就是指和该节点相关联的边的条数。
这样一来,方案数就减少了,问题也就变得容易了,现在请你告诉wangyurzee连边的方案总数为多少。
答案请对1000000007取模。

样例解释
总方案共有3种,分别为{(1,2),(1,3)},{(1,2),(2,3)},{(2,3),(1,3)}。其中第二种方案节点1的度数为2,不符合要求,因此答案为2。
Input
第一行输入2个整数n(1<=n<=1000000),m(0<=m<=17)分别表示节点个数以及限制个数。
第2行到第m+1行描述m个限制条件,第i+1行为2个整数u[i],d[i],表示编号为u[i]的节点度数不能为d[i]。
为了方便起见,保证1<=ui<=m。同时保证1<=ui<=n,1<=di<=n-1,保证不会有两条完全相同的限制。
Output
输出一行一个整数表示答案。
Input示例
3 1
1 2
Output示例
2

解题思路:

首先需要知道一个 prufer序列,具体详见prufer数列学习笔记
因为只要一个purfer序列确定了,那么由这个序列所组成的无根树也就确定了,所以只用算purfer序列的方案就可以了。
我们还需要知道的一个知识点就是: n 个点的生成树的数量为 nn2
这里解释一下:底数是指 n 个点,指数是指 n 个点所对应的 prufer 序列有几个位置。
然后我们发现题目中给出的限制条件很少 m17 ,最多也就只有 2m 种状态,然后根据这些状态进行容斥。
那么我们假设选取了 x 个条件。
因为一个 n 的点的树所对应的 prufer 序列有 n2 个数值,然后每个数字出现的个数就是这个点的 度数-1。
那么我们现在先求出 x 个条件所对应的度数之和 sum=(vi1)
然后 sum 就对应了 prufer 序列中的 sum 个位置,所以从 n2 个位置里选 sum 个位置,方案数为 C(n2,sum)
现在就对 prufer 序列中已知的条件进行排列
现在对 sum v11 个位置进行组合(因为 每个数字都是一样的) 所以方案数为 C(sum,v11)
现在还剩 tmp=sumv1+1 个位置
然后现在对 tmp v21 个位置进行组合(因为 每个数字都是一样的) 所以方案数为 C(tmp,v21)

直到这选定 x 个条件全部判断完,然后在根据乘法原理,将所有的方法数乘起来。
现在还剩余 nx 个点,这些是没有限制的,然后就是 (nx)(n2sum)
将所有的乘起来之后就是如下公式:
ans=(n2)!(n2sum)!(vi1)!(nx)(n2sum)
然后相应的进行容斥,将最开始的总方案数算出来,然后奇减偶加。
trick:这里有几个坑
1): 当只有一个点的时候,如果 m=1 直接输出 0,否则都是 1
2):有一种情况是一个点可能有多种限制情况,这样的需要考虑:
比如 :
n = 4 m = 2
1 1
1 2

代码:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <math.h>
using namespace std;
typedef long long LL;
const LL MOD = 1e9+7;
const int MAXN = 20;
int u[MAXN], v[MAXN], vis[MAXN];
LL Pow(LL a, LL b){
    LL ans = 1;
    while(b){
        if(b & 1) ans = ans * a % MOD;
        b>>=1;
        a = a * a % MOD;
    }
    return ans;
}
LL fac[1000005], Inv[1000005];
void Init(){
    fac[0] = fac[1] = Inv[0] = Inv[1] = 1;
    for(int i=2; i<1000005; i++) fac[i] = fac[i-1] * i % MOD;
    for(int i=2; i<1000005; i++) Inv[i] = (MOD - MOD / i) * Inv[MOD % i] % MOD;
    for(int i=2; i<1000005; i++) Inv[i] = Inv[i-1] * Inv[i] % MOD;
}
int main()
{
    Init();
    int n, m;
    scanf("%d%d", &n, &m);
    if(n == 1){
        if(m == 1) {puts("0"); return 0;}
        else {puts("1"); return 0;}
    }
    for(int i=0; i<m; i++) scanf("%d%d", &u[i], &v[i]);
    LL ans = Pow(n, n-2);
    int state = (1<<m);
    int aa = 0;
    for(int i=1; i<state; i++){
        int cnt = 0, sum = 0;
        memset(vis, 0, sizeof(vis));
        LL t = 1;
        for(int j=0; j<m; j++){
             if(i & (1<<j)){
                if(vis[u[j]]) goto A;
                cnt++;
                sum += (v[j]-1);
                vis[u[j]] = 1;
                t = t * Inv[v[j]-1] % MOD;
            }
        }
        if(sum > n-2) { A:; continue; }
        LL tmp = fac[n-2] * Inv[n-2-sum] % MOD;
        tmp = tmp * Pow(n-cnt, n-2-sum) % MOD;
        tmp = tmp * t % MOD;
        if(cnt & 1) ans = (ans - tmp) % MOD;
        else ans = (ans + tmp) % MOD;
    }
    ans = (ans % MOD + MOD) % MOD;
    printf("%lld\n",ans);
    return 0;
}
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值