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

本文探讨了一颗树的染色方案计数问题,树有n个结点,使用k种不同颜色染色,合法的染色方案需确保相同颜色的点对路径上所有点颜色一致。文章提供了两种解决方案,一种基于动态规划,另一种利用组合数学原理,详细介绍了算法实现及优化技巧。

题面

   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);
}
非常好的需求! 如果你希望在 **Verilog 中用** **一轮组合逻辑(combinational logic)** 实现 $ GF(2^8) $ 中的逆元计算,而不使用查找表(LUT),可以使用 **指数法(Exponentiation)** 来实现。 --- ## ✅ 一、为什么使用指数法? 在 $ GF(2^8) $ 中,非零元素构成一个 **乘法循环群**,其阶为 255。 根据费马小定理的有限域版本: $$ a^{-1} = a^{255 - 1} = a^{254}, \quad \text{当 } a \neq 0 $$ 所以,求逆元等价于计算 $ a^{254} \mod m(x) $,其中 $ m(x) = x^8 + x^4 + x^3 + x + 1 \Rightarrow 0x11b $ --- ## ✅ 二、组合逻辑实现思路 我们要在 **一个时钟周期内**(组合逻辑)完成: 1. **幂运算:a^254** 2. **模不可约多项式:mod 0x11b** 这可以通过 **平方-乘法算法(Square and Multiply)** 来实现,将 254 拆解为二进制表示: $$ 254 = 11111110_2 = 2^7 + 2^6 + 2^5 + 2^4 + 2^3 + 2^2 + 2^1 $$ --- ## ✅ 三、Verilog 实现代码(组合逻辑) 下面是一个 **完全组合逻辑的 Verilog 模块**,实现 $ a^{-1} = a^{254} \mod m(x) $ ```verilog module inv_gf8_comb ( input [7:0] a, output reg [7:0] inv ); parameter POLY = 8'h11b; // x^8 + x^4 + x^3 + x + 1 always @(*) begin if (a == 8'h00) begin inv = 8'h00; // 0 没有逆元 end else begin // 使用平方-乘法算法计算 a^254 mod POLY reg [7:0] result = 8'h01; reg [7:0] base = a; integer i; for (i = 0; i < 8; i = i + 1) begin if (i != 0) begin base = gf8_mul(base, base); // 平方 end if (254 & (1 << (7 - i))) begin result = gf8_mul(result, base); // 乘法 end end inv = result; end end // GF(2^8) 上的乘法函数 function [7:0] gf8_mul; input [7:0] a, b; reg [7:0] a_reg, b_reg; reg [15:0] product; reg [7:0] result; integer i; begin a_reg = a; b_reg = b; product = 16'h0; for (i = 0; i < 8; i = i + 1) begin if (b_reg[0]) begin product[15:8] = product[15:8] ^ a_reg; end product = {product[14:0], 1'b0}; // 左移 if (product[15]) begin product[15:8] = product[15:8] ^ POLY; end end gf8_mul = product[7:0]; end endfunction endmodule ``` --- ## ✅ 四、模块说明 | 信号 | 类型 | 说明 | |------|------|------| | `a` | 输入 | 待求逆元的字节 | | `inv` | 输出 | 逆元结果 | | `gf8_mul` | 函数 | 实现 $ GF(2^8) $ 上的乘法 | --- ## ✅ 五、关键函数解释 ### 1. **gf8_mul 函数** 这是有限域上的乘法函数,实现的是两个字节相乘后对不可约多项式取模的操作。 - 使用左移和异或模拟乘法 - 每次高位为1时,用模多项式消去高位 --- ### 2. **平方-乘法算法** ```verilog for (i = 0; i < 8; i = i + 1) begin if (i != 0) begin base = gf8_mul(base, base); end if (254 & (1 << (7 - i))) begin result = gf8_mul(result, base); end end ``` - 从最高位到最低位扫描指数 254 的每一位 - 如果该位为1,执行乘法 - 每轮执行平方操作 --- ## ✅ 六、性能与资源分析 | 指标 | 说明 | |------|------| | **延迟** | 较高,因为是组合逻辑,逻辑层级深 | | **资源占用** | 多,尤其乘法器部分 | | **是否可综合** | ✅ 可综合,但不如查表法高效 | | **是否适合FPGA** | ✅ 可用,但建议使用查找表更高效 | | **是否适合ASIC** | ✅ 可用,但时序要求高 | --- ## ✅ 七、使用建议 - 如果你对时序要求不高,但希望完全避免查表,此方法适用 - 如果你希望 **完全组合逻辑** 实现 AES S 盒,可以将此模块与仿射变换模块级联 - 如果你希望提高性能,可使用 **流水线结构** --- ##
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值