题面
link 有一颗树,树有n个结点。有k种不同颜色的染料给树染色。一个染色方案是合法的,当且仅当对于所有相同颜色的点对(x,y),x到y的路径上的所有点的颜色都要与x和y相同。请统计方案数。
其中,
n
,
k
≤
300
n, k ≤ 300
n,k≤300
分析
若是从某个根节点在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
k−j 种涂法(已经涂过的
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[i−1][j]+dp[i−1][j−1]∗(k−j+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
n−1 条边,形成
n
n
n 个连通块。所以一棵树我们有
C
n
−
1
i
−
1
(
1
≤
i
≤
n
)
C_{n-1}^{i-1} (1 ≤ i ≤ n)
Cn−1i−1(1≤i≤n) 种切法,每次对于得到的
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
n∑iCn−1i−1Aki
复习一下组合数学的知识,
C
m
n
=
m
!
(
m
−
n
)
!
n
!
C_m^n = \frac{m !}{(m-n) !n !}
Cmn=(m−n)!n!m!,
A
m
n
=
m
!
(
m
−
n
)
!
A_m^n = \frac{m !}{(m-n) !}
Amn=(m−n)!m!, 其中阶乘我们可以先预处理出来,但这里有除法的取模,我们需要用到费马小定理: 对于质数
p
p
p, 若
a
a
a 不是
p
p
p 的倍数,则有
a
p
−
1
≡
1
(
m
o
d
p
)
a^{p-1} ≡ 1 (mod \ p)
ap−1≡1(mod p)
那么若记
a
−
1
≡
b
(
m
o
d
p
)
a^{-1} ≡ b(mod \ p)
a−1≡b(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)
a−1∗ap−1≡b∗ap−1(mod p), 又由于费马小定理可得
b
∗
a
p
−
1
=
b
(
m
o
d
p
)
b * a^{p-1} = b (mod \ p)
b∗ap−1=b(mod p), 于是
a
p
−
2
≡
b
(
m
o
d
p
)
a^{p-2} ≡ b(mod \ p)
ap−2≡b(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}
i−1 在模
p
p
p 运算下如何得出呢?
不妨记
p
=
k
∗
i
+
r
p = k*i + r
p=k∗i+r, 其中
r
<
i
,
1
<
i
<
p
r < i,1 < i <p
r<i,1<i<p, 那么
k
=
⌊
p
i
⌋
,
r
=
p
%
i
k = ⌊\frac{p}{i}⌋,r = p \%i
k=⌊ip⌋,r=p%i, 可以进行如下推导:
k
∗
i
+
r
≡
0
(
m
o
d
p
)
k * i + r ≡ 0 (mod \ p)
k∗i+r≡0(mod p)
两边同乘
r
−
1
i
−
1
r^{-1} i^{-1}
r−1i−1:
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)
k∗r−1+i−1≡0(mod p)i−1≡−k∗r−1(mod p)i−1≡−⌊ip⌋∗(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/i∗inv[p%i], 调整一下符号,有
i
n
v
[
i
]
=
(
p
−
p
/
i
)
∗
i
n
v
[
p
%
i
]
inv[i] = (p - p / i) * inv[p \%i]
inv[i]=(p−p/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);
}