[组合数学] NC13611树 (逆元的计算)

题面

   link 有一颗树,树有n个结点。有k种不同颜色的染料给树染色。一个染色方案是合法的,当且仅当对于所有相同颜色的点对(x,y),x到y的路径上的所有点的颜色都要与x和y相同。请统计方案数。
    其中, n , k ≤ 300 n, k ≤ 300 n,k300

分析

   若是从某个根节点在dfs 或者 bfs 过程中统计,是非常麻烦的事。子结点和父亲一个颜色,这种情况还比较简单,若是不同颜色,则其兄弟结点也不能和该子结点一个颜色(因为这两者通过父结点连接),这样整个过程就不好计算了。
   或许可以换一种思路,若是我们已经涂了一个部分,并且这些部分是联通的,即涂了这棵树一棵结点数为 i i i 的子树,这棵树共有 j j j 种不同颜色,那么可以记涂法为 d p [ i ] [ j ] dp[i][j] dp[i][j], 若是已经涂过颜色的 v p v_p vp 与 没有涂过颜色的 v q v_{q} vq 相连,那么可以扩展,若 C o l o r ( v p ) = C o l o r ( v q ) Color(v_p) = Color(v_q) Color(vp)=Color(vq),那么 v q v_{q} vq 有一种涂法;若是 C o l o r ( v p ) ≠ C o l o r ( v q ) Color(v_p) ≠ Color(v_q) Color(vp)=Color(vq),那么 v q v_{q} vq k − j k - j kj 种涂法(已经涂过的 j j j 种颜色不能再用了,因为 v q v_q vq 与已经涂过颜色的结点之间的路径必须经过 v p v_p vp)。于是可以得到如下转移式子:

d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − 1 ] ∗ ( k − j + 1 ) dp[i][j] = dp[i-1][j] + dp[i-1][j-1] * (k - j + 1) dp[i][j]=dp[i1][j]+dp[i1][j1](kj+1)
   而我们所需要的答案就是 ∑ i = 1 n d p [ n ] [ i ] \sum_{i=1}^{n}dp[n][i] i=1ndp[n][i],复杂度为 O ( n k ) O(nk) O(nk) 所以通过动态规划得出答案就可以。

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
typedef pair<int, long long> P;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
 
ll dp[302][302], ans;
int n, k;
 
int main()
{
    scanf("%d %d", &n, &k);
    dp[1][1] = k;                                  //一个结点涂一种颜色有k种答案
    for(int i = 2; i <= n; i++)
        for(int j = 1; j <= k; j++)                 //进行dp
        {
            dp[i][j] = dp[i-1][j] + dp[i-1][j-1] * (k - j + 1);
            dp[i][j] %= mod;
        }
    for(int i = 1; i <= k; i++)                      //累加答案
        ans = (ans + dp[n][i]) % mod;
    printf("%lld\n", ans);
}

   
   或许还可以这样想,相同颜色的结点即构成一个连通块,而对于树来说,每切去一条边就多一个连通块,最多可以切去 n − 1 n - 1 n1 条边,形成 n n n 个连通块。所以一棵树我们有 C n − 1 i − 1 ( 1 ≤ i ≤ n ) C_{n-1}^{i-1} (1 ≤ i ≤ n) Cn1i1(1in) 种切法,每次对于得到的 i i i 个连通块,用 k k k 种颜色去涂,总共有 A k i A_k^i Aki 种涂法,所以最终答案为:
∑ n i C n − 1 i − 1 A k i \sum_n^iC_{n-1}^{i-1}A_k^i niCn1i1Aki
   复习一下组合数学的知识, C m n = m ! ( m − n ) ! n ! C_m^n = \frac{m !}{(m-n) !n !} Cmn=(mn)!n!m!, A m n = m ! ( m − n ) ! A_m^n = \frac{m !}{(m-n) !} Amn=(mn)!m!, 其中阶乘我们可以先预处理出来,但这里有除法的取模,我们需要用到费马小定理: 对于质数 p p p, a a a 不是 p p p 的倍数,则有 a p − 1 ≡ 1 ( m o d   p ) a^{p-1} ≡ 1 (mod \ p) ap11(mod p)
    那么若记 a − 1 ≡ b ( m o d   p ) a^{-1} ≡ b(mod \ p) a1b(mod p), 那么 a − 1 ∗ a p − 1 ≡ b ∗ a p − 1 ( m o d   p ) a^{-1} * a^{p-1}≡ b * a^{p-1}(mod \ p) a1ap1bap1(mod p), 又由于费马小定理可得 b ∗ a p − 1 = b ( m o d   p ) b * a^{p-1} = b (mod \ p) bap1=b(mod p), 于是 a p − 2 ≡ b ( m o d   p ) a^{p-2} ≡ b(mod \ p) ap2b(mod p), 这样就将一个求负幂次的形式变成了求正幂次的形式,这样我们可以用快速幂 O ( l o g n ) O(logn) O(logn) 的时间进行计算了,这样总复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn)

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
typedef pair<int, long long> P;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
 
ll fac[302], ans;
int n, k;
 
ll qpow(ll x, ll n)                 //快速幂
{
    ll t = 1;
    for (; n; n >>= 1, x = x * x % mod)
        if (n & 1)
            t = t * x % mod;
    return t;
}
 
ll C(int n, int k)         
{
    return fac[n] * qpow(fac[k] * fac[n-k] % mod, mod - 2) % mod;
}
 
ll A(int n, int k)
{
    return fac[n] * qpow(fac[n-k] % mod, mod - 2) % mod;
}
 
int main()
{
    scanf("%d %d", &n, &k);
 
    fac[0] = fac[1] = 1;
    for(int i = 2; i <= n; i++)          //预处理阶乘
        fac[i] = fac[i-1] * i % mod;
 
    for(int i = 1; i <= min(n, k); i++)
        ans = (ans + C(n - 1, i - 1) * A(k, i) % mod) % mod;
    printf("%lld\n", ans);
}

   
   上面用快速幂求逆元用了对数时间,而其实可以先用线性时间先预处理出逆元,这样总复杂度就可以降到 O ( n ) O(n) O(n) i − 1 i^{-1} i1 在模 p p p 运算下如何得出呢?
   不妨记 p = k ∗ i + r p = k*i + r p=ki+r, 其中 r < i , 1 < i < p r < i,1 < i <p ri1ip, 那么 k = ⌊ p i ⌋ , r = p % i k = ⌊\frac{p}{i}⌋,r = p \%i k=ipr=p%i, 可以进行如下推导:
k ∗ i + r ≡ 0 ( m o d   p ) k * i + r ≡ 0 (mod \ p) ki+r0(mod p)
两边同乘 r − 1 i − 1 r^{-1} i^{-1} r1i1:
k ∗ r − 1 + i − 1 ≡ 0 ( m o d   p ) i − 1 ≡ − k ∗ r − 1 ( m o d   p ) i − 1 ≡ − ⌊ p i ⌋ ∗ ( p % i ) − 1 ( m o d   p ) k * r^{-1} + i^{-1} ≡ 0 (mod \ p) \\ i^{-1} ≡ -k*r^{-1}(mod \ p) \\ i^{-1} ≡ - ⌊\frac{p}{i}⌋ * (p \%i)^{-1} (mod \ p) kr1+i10(mod p)i1kr1(mod p)i1ip(p%i)1(mod p)
    若记 i n v [ i ] inv[i] inv[i] 表示 i i i 的逆元,那么 i n v [ i ] = − p / i ∗ i n v [ p % i ] inv[i] = - p / i * inv[p \%i] inv[i]=p/iinv[p%i], 调整一下符号,有 i n v [ i ] = ( p − p / i ) ∗ i n v [ p % i ] inv[i] = (p - p / i) * inv[p \%i] inv[i]=(pp/i)inv[p%i], 然后算组合数的时候由于求的是阶乘,所以还要前缀和处理一下。

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
typedef pair<int, long long> P;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
 
ll inv[302], fac[302], ans;
int n, k;
 
ll A(int n, int k)
{
    return fac[n] * inv[n-k] % mod;
}
 
ll C(int n, int k)
{
    return fac[n] * inv[k] % mod * inv[n-k] % mod;
}
 
int main()
{
    scanf("%d %d", &n, &k);
 
    inv[0] = inv[1] = 1;
    fac[0] = fac[1] = 1;
    for(int i = 2; i <= n; i++)           //求逆元和阶乘
    {
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        fac[i] = fac[i-1] * i % mod;
    }
    for(int i = 2; i <= n; i++)               //对阶乘进行前缀和处理
        inv[i] = inv[i] * inv[i-1] % mod;
 
    for(int i = 1; i <= min(n, k); i++)
        ans = (ans + C(n - 1, i - 1) * A(k, i) % mod) % mod;
    printf("%lld\n", ans);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值