下面提到的所有知识不一定同属一个范畴,但是它们的思想和作用相似,所以将其归为一篇文章。
莫比乌斯反演
请先学习狄利克雷卷积,否则您会一脸懵逼。
一般式:
F
(
n
)
=
∑
d
∣
n
G
(
d
)
→
G
(
n
)
=
∑
d
∣
n
μ
(
n
d
)
F
(
d
)
F
(
n
)
=
∑
n
∣
d
G
(
d
)
→
G
(
n
)
=
∑
n
∣
d
μ
(
d
n
)
F
(
d
)
\begin{aligned} F(n) = \sum_{d | n} G(d) \rightarrow G(n) = \sum_{d | n} \mu(\frac{n}{d}) F(d) \\ F(n) = \sum_{n | d} G(d) \rightarrow G(n) = \sum_{n | d} \mu(\frac{d}{n}) F(d) \end{aligned}
F(n)=d∣n∑G(d)→G(n)=d∣n∑μ(dn)F(d)F(n)=n∣d∑G(d)→G(n)=n∣d∑μ(nd)F(d)
证明可以考虑下面恒等式的第一条。
然后再放一些有助于反演的恒等式:(其中
∗
*
∗ 指狄利克雷卷积,
ϵ
(
n
)
\epsilon(n)
ϵ(n) 表示
[
n
=
1
]
[n = 1]
[n=1])
μ
∗
1
=
ϵ
φ
∗
1
=
i
d
μ
∗
i
d
=
μ
∗
1
∗
φ
=
ϵ
∗
φ
=
φ
φ
(
n
)
=
∑
i
=
1
n
ϵ
(
gcd
(
n
,
i
)
)
φ
(
n
m
)
=
φ
(
n
)
φ
(
m
)
gcd
(
n
,
m
)
φ
(
gcd
(
n
,
m
)
)
∑
i
=
1
n
∑
j
=
1
n
ϵ
(
gcd
(
i
,
j
)
)
=
2
⋅
(
∑
i
=
1
n
φ
(
i
)
)
−
1
∑
d
∣
i
j
1
=
∑
x
∣
i
∑
y
∣
j
ϵ
(
gcd
(
x
,
y
)
)
∑
d
∣
i
j
k
1
=
∑
x
∣
i
∑
y
∣
j
∑
z
∣
k
ϵ
(
gcd
(
x
,
y
)
)
ϵ
(
gcd
(
y
,
z
)
)
ϵ
(
gcd
(
x
,
z
)
)
⋯
⋯
\begin{aligned} &\mu * 1 = \epsilon \\ & \varphi * 1 = id \\ & \mu * id = \mu * 1 * \varphi = \epsilon * \varphi = \varphi \\ &\varphi(n) = \sum_{i = 1}^n \epsilon(\gcd(n, i)) \\ &\varphi(nm) = \frac{\varphi(n)\varphi(m)\gcd(n, m)}{\varphi(\gcd(n, m))} \\ &\sum_{i=1}^n\sum_{j = 1}^n \epsilon(\gcd(i, j)) = 2 \cdot (\sum_{i = 1}^n\varphi(i)) - 1 \\ & \sum_{d | ij} 1 = \sum_{x|i} \sum_{y|j} \epsilon(\gcd(x, y)) \\ & \sum_{d | ijk} 1 = \sum_{x|i} \sum_{y|j} \sum_{z|k} \epsilon(\gcd(x, y))\epsilon(\gcd(y, z))\epsilon(\gcd(x, z)) \\ &\cdots\cdots \end{aligned}
μ∗1=ϵφ∗1=idμ∗id=μ∗1∗φ=ϵ∗φ=φφ(n)=i=1∑nϵ(gcd(n,i))φ(nm)=φ(gcd(n,m))φ(n)φ(m)gcd(n,m)i=1∑nj=1∑nϵ(gcd(i,j))=2⋅(i=1∑nφ(i))−1d∣ij∑1=x∣i∑y∣j∑ϵ(gcd(x,y))d∣ijk∑1=x∣i∑y∣j∑z∣k∑ϵ(gcd(x,y))ϵ(gcd(y,z))ϵ(gcd(x,z))⋯⋯
推完式子之后,可以考虑用一些数论筛法预处理某些项,或是用整除分块求值。
拿一道例题来讲解吧。
一共
T
T
T 组询问
(
T
≤
1
0
4
)
(T \le 10^4)
(T≤104),给定常数
K
K
K,每次询问单独给定
N
N
N,满足
N
≤
1
0
7
N \le 10^7
N≤107,求出:
∑
i
=
1
N
∑
j
=
1
N
(
i
+
j
)
K
gcd
(
i
,
j
)
μ
2
(
gcd
(
i
,
j
)
)
(
m
o
d
2
32
)
\sum_{i = 1}^N\sum_{j = 1}^N(i + j)^K \gcd(i, j)\mu^2(\gcd(i, j))~(\bmod 2^{32})
i=1∑Nj=1∑N(i+j)Kgcd(i,j)μ2(gcd(i,j)) (mod232)
看模数
2
32
2^{32}
232,显然可以自然溢出,这可以帮助我们减小常数。
接下来表演推式子:
原
式
=
∑
d
=
1
N
∑
i
=
1
N
∑
j
=
1
N
[
gcd
(
i
,
j
)
=
d
]
(
(
i
+
j
)
K
d
μ
2
(
d
)
)
=
∑
d
=
1
N
d
K
+
1
μ
2
(
d
)
∑
i
=
1
⌊
N
d
⌋
∑
j
=
1
⌊
N
d
⌋
ϵ
(
gcd
(
i
,
j
)
)
(
i
+
j
)
K
=
∑
d
=
1
N
d
K
+
1
μ
2
(
d
)
∑
i
=
1
⌊
N
d
⌋
∑
j
=
1
⌊
N
d
⌋
(
i
+
j
)
K
∑
x
∣
gcd
(
i
,
j
)
μ
(
x
)
=
∑
d
=
1
N
d
K
+
1
μ
2
(
d
)
∑
x
=
1
⌊
N
d
⌋
x
K
μ
(
x
)
∑
i
=
1
⌊
N
d
x
⌋
∑
j
=
1
⌊
N
d
x
⌋
(
i
+
j
)
K
=
∑
T
=
1
N
T
K
∑
i
=
1
⌊
N
T
⌋
∑
j
=
1
⌊
N
T
⌋
(
i
+
j
)
K
∑
d
∣
T
d
μ
2
(
d
)
μ
(
T
d
)
\begin{aligned} 原式 &= \sum_{d = 1}^N\sum_{i = 1}^N\sum_{j = 1}^N[\gcd(i, j) = d]((i + j)^K d\mu^2(d)) \\ &= \sum_{d = 1}^Nd^{K + 1}\mu^2(d)\sum_{i = 1}^{\lfloor\frac{N}{d}\rfloor} \sum_{j = 1}^{\lfloor\frac{N}{d}\rfloor} \epsilon(\gcd(i, j))(i + j)^K \\ &= \sum_{d = 1}^N d^{K + 1}\mu^2(d)\sum_{i = 1}^{\lfloor\frac{N}{d}\rfloor}\sum_{j = 1}^{\lfloor\frac{N}{d}\rfloor}(i + j)^K\sum_{x | \gcd(i, j)} \mu(x) \\ &= \sum_{d = 1}^Nd^{K + 1}\mu^2(d)\sum_{x = 1}^{\lfloor\frac{N}{d}\rfloor}x^K\mu(x)\sum_{i = 1}^{\lfloor\frac{N}{dx}\rfloor}\sum_{j = 1}^{\lfloor\frac{N}{dx}\rfloor}(i + j)^K \\ &= \sum_{T = 1}^NT^K\sum_{i = 1}^{\lfloor\frac{N}{T}\rfloor} \sum_{j = 1}^{\lfloor\frac{N}{T}\rfloor}(i + j)^K \sum_{d|T}d\mu^2(d)\mu(\frac{T}{d}) \end{aligned}
原式=d=1∑Ni=1∑Nj=1∑N[gcd(i,j)=d]((i+j)Kdμ2(d))=d=1∑NdK+1μ2(d)i=1∑⌊dN⌋j=1∑⌊dN⌋ϵ(gcd(i,j))(i+j)K=d=1∑NdK+1μ2(d)i=1∑⌊dN⌋j=1∑⌊dN⌋(i+j)Kx∣gcd(i,j)∑μ(x)=d=1∑NdK+1μ2(d)x=1∑⌊dN⌋xKμ(x)i=1∑⌊dxN⌋j=1∑⌊dxN⌋(i+j)K=T=1∑NTKi=1∑⌊TN⌋j=1∑⌊TN⌋(i+j)Kd∣T∑dμ2(d)μ(dT)
到这里式子就是可做的了。
我猜你现在肯定一脸懵。
那就先来讲一讲吧。
- S t e p 1 Step~1 Step 1
我们考虑枚举这个 gcd \gcd gcd 的值。
为什么这里要做这样一个看似只会添一层循环的无意义举动呢?
可以发现,在后面的式子中多出了一个艾弗森括号,这个括号里写下的等式是可以转化为单位元的形式的。
- S t e p 2 Step~2 Step 2
我们考虑对于每一个 d d d,只枚举 d d d 的倍数。
(实际上,此时枚举的 i , j i, j i,j 在后面的运算中会乘上一个 d d d。)
这样,艾弗森括号中的等式就可以被改写了,具体来说就是 [ gcd ( i d , j d ) = d ] = [ gcd ( i , j ) = 1 ] [\gcd(id, jd) = d] =[\gcd(i, j) = 1] [gcd(id,jd)=d]=[gcd(i,j)=1]。
在上面的式子中,后者已经写成了单位元的形式。
值得注意的是,由于 i , j i, j i,j 实际上除去了一个 d d d,所以运算中多出来的一个 d K d^K dK 要提到前面去。同时只和 d d d 有关的一项 d μ 2 ( d ) d\mu^2(d) dμ2(d) 也要提到前面去。
- S t e p 3 Step~3 Step 3
还记得上面的公式 μ ∗ 1 = ϵ \mu * 1 = \epsilon μ∗1=ϵ 吗?我们可以直接把这个单位元转化为莫比乌斯函数和 1 1 1 的狄利克雷卷积!
所以,把卷积用好了,莫比乌斯反演就不难了,再难也只可能难在函数的筛法上。
然后呢?我们再把这个枚举的 x x x 提前,把 i , j i, j i,j 再次强制转化为 d x dx dx 的倍数,多出来的 x K x^K xK 同 μ ( x ) \mu(x) μ(x) 一起往前提。
继续,我们发现这个式子代数还是太多了,于是考虑枚举 T = d x T = dx T=dx,那么 d d d 必然是 T T T 的因数,此时的 x x x 则恰好等于 T d \frac{T}{d} dT,这样化简出来,式子已经可做了。
问题是,我们怎样知道式子是可做的呢?
我们设 S ( N ) = ∑ i = 1 N ∑ j = 1 N ( i + j ) K S(N) = \sum_{i = 1}^N\sum_{j = 1}^N(i + j)^K S(N)=∑i=1N∑j=1N(i+j)K, f ( T ) = ∑ d ∣ T d μ 2 ( d ) μ ( T d ) f(T) = \sum_{d|T}d\mu^2(d)\mu(\frac{T}{d}) f(T)=∑d∣Tdμ2(d)μ(dT)。
现在整个式子化简成了这样一个形式: ∑ T = 1 N T K S ( ⌊ N T ⌋ ) f ( T ) \sum_{T = 1}^N T^KS(\lfloor\frac{N}{T}\rfloor)f(T) ∑T=1NTKS(⌊TN⌋)f(T)。
接下来分别考虑线性预处理 S S S 和 f f f。
可以发现在 S ( N ) S(N) S(N) 中,从 2 ∼ 2 N 2 \sim 2N 2∼2N 的每一个数 d K d^K dK 一共被计算了 min ( d − 1 , 2 N + 1 − d ) \min(d - 1, 2N + 1 - d) min(d−1,2N+1−d) 次。
于是我们可以再设两个辅助函数 F ( N ) = ∑ i = 1 N i K F(N) = \sum_{i = 1}^N i^K F(N)=∑i=1NiK, G ( N ) = ∑ i = 1 N F ( i ) G(N) = \sum_{i = 1}^N F(i) G(N)=∑i=1NF(i),显然这两个函数都是可以线性预处理的。
那么我们有式子: S ( N ) = ∑ i = N + 1 2 N F ( i ) − ∑ i = 1 N F ( i ) = G ( 2 N ) − G ( N ) S(N)=\sum_{i=N+1}^{2N}F(i)-\sum_{i = 1}^N F(i) = G(2N)-G(N) S(N)=∑i=N+12NF(i)−∑i=1NF(i)=G(2N)−G(N),同样也可以线性预处理。
再来看 f f f,可以发现 f f f 是一个狄利克雷卷积的标准形式,并且卷的是两个积性函数,说明整个式子也是一个积性函数。如果您阅读过我的文章《积性函数的筛法》,可以知道这样的函数是可以欧拉筛的。
我们来讨论一下 f f f 在 p k ( p ∈ P r i m e ) p^k~(p \in Prime) pk (p∈Prime) 处的取值吧。
- k = 0 k = 0 k=0,显然有 f ( 1 ) = 1 f(1) = 1 f(1)=1;
- k = 1 k = 1 k=1,有 f ( p ) = μ ( p ) + p μ 2 ( p ) = p − 1 f(p) = \mu(p) + p\mu^2(p) = p - 1 f(p)=μ(p)+pμ2(p)=p−1;
- k = 2 k = 2 k=2,有 f ( p 2 ) = μ ( p 2 ) + p μ 2 ( p ) μ ( p ) + p 2 μ ( p 2 ) = − p f(p^2) = \mu(p^2) + p\mu^2(p)\mu(p) + p^2\mu(p^2) = -p f(p2)=μ(p2)+pμ2(p)μ(p)+p2μ(p2)=−p;
- k ≥ 3 k \ge 3 k≥3,则每一项都存在 μ \mu μ 为 0 0 0 的情况,答案为 0 0 0。
次数很好判断,所以分讨即可。
最后,可以发现 N T \frac{N}{T} TN 的取值个数是 N \sqrt{N} N 级别的,所以可以套用整除分块。同时为了方便,可以把 T K T^K TK 这一项扔进 f f f 中一起预处理(最后求前缀和时配合 F F F 数组一起求即可)。
为了省空间,把 F F F 和 G G G 当作一个数组,毕竟有了 G G G, F F F 也没什么用了。
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#define MAXN 20000005
using uint = unsigned int;
using namespace std;
int T, N, K, len, Prime[MAXN];
uint f[MAXN], F[MAXN];
bool NotPrime[MAXN];
inline uint Pow(uint a, uint b)
{
uint res = 1;
while(b)
{
if(b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
inline void Sieve(int n, int k)
{
f[1] = F[1] = 1;
for(register int i = 2; i <= n; i++)
{
if(!NotPrime[i])
{
Prime[++len] = i;
f[i] = i - 1;
F[i] = Pow(i, k);
}
for(register int j = 1; j <= len && i * Prime[j] <= n; j++)
{
NotPrime[i * Prime[j]] = true;
F[i * Prime[j]] = F[i] * F[Prime[j]];
if(i % Prime[j] == 0)
{
if((i / Prime[j]) % Prime[j]) f[i * Prime[j]] = -Prime[j] * f[i / Prime[j]];
break;
}
f[i * Prime[j]] = f[i] * f[Prime[j]];
}
}
for(register int i = 2; i <= n; i++) f[i] = f[i - 1] + f[i] * F[i], F[i] += F[i - 1];
for(register int i = 2; i <= n; i++) F[i] += F[i - 1];
return;
}
inline uint Count(int k) { return F[k * 2] - F[k] * 2; }
int main()
{
scanf("%d%d%d", &T, &N, &K), Sieve(N << 1, K);
while(T--)
{
int n; scanf("%d", &n);
uint ans = 0;
for(register int l = 1, r; l <= n; l = r + 1)
{
r = n / (n / l);
ans += Count(n / l) * (f[r] - f[l - 1]);
}
printf("%u\n", ans);
}
return 0;
}
二项式反演
其实二项式反演有一个更不广为人知的名字——广义容斥原理。
一般有两种形式:
f
(
n
)
=
∑
i
=
0
n
C
n
i
g
(
i
)
⇔
g
(
n
)
=
∑
i
=
0
n
(
−
1
)
n
−
i
C
n
i
f
(
i
)
f
(
n
)
=
∑
i
=
n
m
C
i
n
g
(
i
)
⇔
g
(
n
)
=
∑
i
=
n
m
(
−
1
)
i
−
n
C
i
n
f
(
i
)
\begin{aligned} f(n) = \sum_{i = 0}^nC_n^ig(i) \Leftrightarrow g(n) = \sum_{i = 0}^n(-1)^{n - i}C_n^if(i) \\ f(n) = \sum_{i = n}^mC_i^ng(i) \Leftrightarrow g(n) = \sum_{i = n}^m(-1)^{i - n} C_i^nf(i) \end{aligned}
f(n)=i=0∑nCnig(i)⇔g(n)=i=0∑n(−1)n−iCnif(i)f(n)=i=n∑mCing(i)⇔g(n)=i=n∑m(−1)i−nCinf(i)
正如前面所说,我们一般设
g
(
n
)
g(n)
g(n) 为 “恰好满足
n
n
n 个性质” 的方案数。对于形式一和形式二,我们一般设
f
(
n
)
f(n)
f(n) 为 “至多满足
n
n
n 个性质” 和 “至少满足
n
n
n 个性质” 的方案数。
显然 f ( n ) f(n) f(n) 更好求,并且 g ( n ) g(n) g(n) 一般都做为题目要求的东西。
例题可以康康这道,太板了就不放题解和代码了。
单位根反演
其实就是这样一个式子: [ n ∣ k ] = 1 n ∑ i = 0 n − 1 ω n i k [n | k] = \frac{1}{n}\sum_{i = 0}^{n - 1} \omega_n^{ik} [n∣k]=n1∑i=0n−1ωnik。
ω \omega ω 指单位根。从数学上讲,它是一个复数。但是从广义上讲,只要在给定的数域和运算内满足 ω n n \omega^n_n ωnn(即 n n n 阶单位根的 n n n 次方)为 1 1 1,那么它就可以称做单位根。所以 ω \omega ω 实际上还可以是原根。
证明略去~~,显然是因为不会~~。
康康例题
首先我们有 ⌊ i k ⌋ = ( ∑ j = 0 i [ k ∣ j ] ) − 1 \lfloor\frac{i}{k}\rfloor = (\sum_{j = 0}^i[k | j]) - 1 ⌊ki⌋=(∑j=0i[k∣j])−1,这样就转化为单位根反演的形式了。
(可以不管后面的减 1 1 1,因为可以直接拆开然后二项式定理搞掉。)
然后我们直接莽式子:
∑
i
=
0
n
C
n
i
p
i
(
(
∑
j
=
0
i
[
k
∣
j
]
)
−
1
)
=
(
∑
i
=
0
n
C
n
i
p
i
∑
j
=
0
i
1
k
∑
t
=
0
k
−
1
ω
k
j
t
)
−
(
p
+
1
)
n
=
1
k
(
∑
t
=
0
k
−
1
∑
i
=
0
n
C
n
i
p
i
∑
j
=
0
i
(
ω
k
t
)
j
)
−
(
p
+
1
)
n
=
1
k
(
∑
t
=
0
k
−
1
∑
i
=
0
n
C
n
i
p
i
ω
k
t
(
i
+
1
)
−
1
ω
k
t
−
1
)
−
(
p
+
1
)
n
=
1
k
(
∑
t
=
0
k
−
1
(
∑
i
=
0
n
C
n
i
p
i
ω
k
t
(
i
+
1
)
)
−
(
p
+
1
)
n
ω
k
t
−
1
)
−
(
p
+
1
)
n
=
1
k
(
∑
t
=
0
k
−
1
(
ω
k
t
(
p
ω
k
t
+
1
)
n
−
(
p
+
1
)
n
ω
k
t
−
1
)
−
(
p
+
1
)
n
\begin{aligned} &\sum_{i = 0}^n C_n^i p^i ((\sum_{j = 0}^i [k | j]) - 1) \\ =&(\sum_{i = 0}^nC_n^i p^i\sum_{j = 0}^i\frac{1}{k}\sum_{t = 0}^{k - 1}\omega_k^{jt}) - (p + 1)^n \\ =&\frac{1}{k}(\sum_{t = 0}^{k - 1}\sum_{i = 0}^n C_n^i p^i\sum_{j = 0}^i(\omega_k^t)^j) - (p + 1)^n \\ =&\frac{1}{k}(\sum_{t = 0}^{k - 1}\sum_{i = 0}^n C_n^i p^i\frac{\omega_k^{t(i + 1)} - 1}{\omega_k^t - 1}) - (p + 1)^n \\ =&\frac{1}{k}(\sum_{t = 0}^{k - 1}\frac{(\sum_{i = 0}^n C_n^i p^i \omega_k^{t(i + 1)}) - (p + 1)^n}{\omega_k^t - 1}) - (p + 1)^n \\ =&\frac{1}{k}(\sum_{t = 0}^{k - 1}\frac{(\omega_k^t(p\omega_k^t + 1)^n- (p + 1)^n}{\omega_k^t - 1}) - (p + 1)^n \end{aligned}
=====i=0∑nCnipi((j=0∑i[k∣j])−1)(i=0∑nCnipij=0∑ik1t=0∑k−1ωkjt)−(p+1)nk1(t=0∑k−1i=0∑nCnipij=0∑i(ωkt)j)−(p+1)nk1(t=0∑k−1i=0∑nCnipiωkt−1ωkt(i+1)−1)−(p+1)nk1(t=0∑k−1ωkt−1(∑i=0nCnipiωkt(i+1))−(p+1)n)−(p+1)nk1(t=0∑k−1ωkt−1(ωkt(pωkt+1)n−(p+1)n)−(p+1)n
可以发现,上面的式子中大量地用到了二项式定理,这玩意确实会经常和单位根反演配合使用。
上面的式子已经可做了,时间复杂度为 O ( k log 2 n ) O(k \log_2 n) O(klog2n),带 log \log log 是因为快速幂。
然后您会发现您连样例都过不去。
我们再来审视一下整个式子。可以发现,当 t = 0 t = 0 t=0 时, ω k t = 1 \omega_k^t = 1 ωkt=1,此时分母为 0 0 0,这一段贡献是算错了的。
我们回到原来的式子: ∑ i = 0 n C n i p i ∑ j = 0 i ( ω k t ) j \sum_{i = 0}^n C_n^i p^i\sum_{j = 0}^i(\omega_k^t)^j ∑i=0nCnipi∑j=0i(ωkt)j,当 ω k t = 1 \omega_k^t = 1 ωkt=1 时,该式子为 ∑ i = 0 n C n i p i ( i + 1 ) \sum_{i = 0}^n C_n^i p^i(i + 1) ∑i=0nCnipi(i+1)。
我们考虑一下这个式子怎么做。
首先我们把后面的加 1 1 1 提出来,直接做一遍二项式定理。
剩下的就是 ∑ i = 0 n C n i p i i \sum_{i = 0}^n C_n^i p^ii ∑i=0nCnipii。
然后我们有一个广为人知的组合恒等式:
i
C
n
i
=
n
C
n
−
1
i
−
1
iC_n^i = nC_{n - 1}^{i - 1}
iCni=nCn−1i−1。
证明: i C n i = i ⋅ n ! i ! ( n − i ) ! = n ! ( i − 1 ) ! ( n − i ) ! = n ⋅ ( n − 1 ) ! ( i − 1 ) ! ( ( n − 1 ) − ( i − 1 ) ) ! = n C n − 1 i − 1 i C_n^i = i \cdot \frac{n!}{i!(n - i)!} = \frac{n!}{(i - 1)!(n - i)!} = n \cdot \frac{(n - 1)!}{(i - 1)!((n - 1) - (i - 1))!} = n C_{n - 1}^{i - 1} iCni=i⋅i!(n−i)!n!=(i−1)!(n−i)!n!=n⋅(i−1)!((n−1)−(i−1))!(n−1)!=nCn−1i−1。
由于
i
=
0
i = 0
i=0 时没有贡献,因此我们从
i
=
1
i = 1
i=1 开始枚举。
∑
i
=
1
n
C
n
i
p
i
i
=
n
∑
i
=
1
n
C
n
−
1
i
−
1
p
i
=
n
∑
i
=
0
n
−
1
C
n
−
1
i
p
i
+
1
=
n
p
∑
i
=
0
n
−
1
C
n
−
1
i
p
i
=
n
p
(
p
+
1
)
n
−
1
\begin{aligned} &\sum_{i = 1}^n C_n^i p^ii \\ = &n\sum_{i = 1}^nC_{n - 1}^{i - 1} p^i \\ = &n \sum_{i = 0}^{n - 1} C_{n - 1}^i p^{i +1} \\ = &np\sum_{i = 0}^{n - 1}C_{n - 1}^i p^i \\ = & np(p + 1)^{n - 1} \end{aligned}
====i=1∑nCnipiini=1∑nCn−1i−1pini=0∑n−1Cn−1ipi+1npi=0∑n−1Cn−1ipinp(p+1)n−1
算法到此就大功告成了。
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#define MOD 998244353
using namespace std;
int n, p, k, ans;
inline int GetMOD(int k) { if(k >= MOD) k -= MOD; return k; }
inline int Pow(int k, int q)
{
int res = 1;
while(q)
{
if(q & 1) res = 1LL * res * k % MOD;
k = 1LL * k * k % MOD;
q >>= 1;
}
return res;
}
inline int Inv(int k) { return Pow(k, MOD - 2); }
int main()
{
scanf("%d%d%d", &n, &p, &k);
const int G = Pow(3, (MOD - 1) / k);
int w = 1;
for(register int t = 0; t < k; t++)
{
if(w == 1) ans = GetMOD(ans + GetMOD(1LL * n * p % MOD * Pow(p + 1, n - 1) % MOD + Pow(p + 1, n)));
else ans = GetMOD(ans + 1LL * GetMOD(1LL * w * Pow(GetMOD(1LL * p * w % MOD + 1), n) % MOD - Pow(p + 1, n) + MOD) * Inv(w - 1) % MOD);
w = 1LL * w * G % MOD;
}
ans = 1LL * ans * Inv(k) % MOD;
ans = GetMOD(ans - Pow(p + 1, n) + MOD);
printf("%d\n", ans);
return 0;
}
快速沃尔什变换
FWT
\operatorname{FWT}
FWT,又称废物树,主要用于处理子集之间的关系。
用例题来讲:
给定两个长度为
2
n
2^n
2n 的数组
A
,
B
A, B
A,B,设:
C
i
=
∑
j
⊗
k
=
i
A
j
⋅
B
k
C_{i} = \sum_{j \otimes k = i} A_j\cdot B_k
Ci=j⊗k=i∑Aj⋅Bk
求当
⊗
\otimes
⊗ 为
or
,
and
,
xor
\operatorname{or}, \operatorname{and}, \operatorname{xor}
or,and,xor 时的
C
C
C。
(
n
≤
17
)
(n \le 17)
(n≤17)
解法:
对于 or \operatorname{or} or,我们考虑构造 F i ′ = ∑ j or i = i F j {F}'_i = \sum_{j \operatorname{or} i = i} F_j Fi′=∑jori=iFj。
为什么要这样构造?
因为有:
A
′
×
B
′
=
∑
j
or
i
=
i
A
j
∑
k
or
i
=
i
B
k
=
∑
j
or
i
=
i
∑
k
or
i
=
i
A
j
B
k
=
∑
(
j
or
k
)
or
i
=
i
A
j
B
k
=
C
′
\begin{aligned} A' \times B' &= \sum_{j \operatorname{or} i = i} A_j \sum_{k \operatorname{or} i = i} B_k \\ &= \sum_{j \operatorname{or} i = i} \sum_{k \operatorname{or} i = i} A_j B_k \\ &=\sum_{(j \operatorname{or} k) \operatorname{or} i = i} A_jB_k \\ &= C' \end{aligned}
A′×B′=jori=i∑Ajkori=i∑Bk=jori=i∑kori=i∑AjBk=(jork)ori=i∑AjBk=C′
现在我们考虑如何将
A
→
A
′
A \to A'
A→A′。
其实就是一个类似 CDQ \operatorname{CDQ} CDQ 分治的思想。
可以发现,在原数组的基础上,每一个数都会对自己二进制表示下的真超集造成贡献,而真超集的特点在于:至少存在一位,满足本身为 0 0 0 而超集为 1 1 1。
所以做法就很显然了:我们类似快速傅里叶变换的递归过程,从大到小枚举位数,把该位上为 0 0 0 的位置贡献到该位为 1 1 1 位置。
而逆变换怎么办呢?直接减回来呗。
可以发现两个变换可以合并:
inline void Or(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < k; j++)
f[i + j + k] = GetMOD(f[i + j + k] + GetMOD(1LL * f[i + j] * t % MOD));
}
return;
}
与的构造和变换同理:
inline void And(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < k; j++)
f[i + j] = GetMOD(f[i + j] + GetMOD(1LL * f[i + j + k] * t % MOD));
}
return;
}
异或的变换就有点麻烦了,因为存在交叉贡献。
我们定义 a ⊗ b = popcount ( a and b ) m o d 2 a \otimes b = \operatorname{popcount}(a\operatorname{and} b)~\bmod~2 a⊗b=popcount(aandb) mod 2,其中 popcount ( k ) \operatorname{popcount}(k) popcount(k) 表示 k k k 在二进制下 1 1 1 的个数。
那么我们存在一个性质: ( i ⊗ j ) xor ( i ⊗ k ) = i ⊗ ( j xor k ) (i \otimes j) \operatorname{xor} (i \otimes k) = i \otimes (j \operatorname{xor} k) (i⊗j)xor(i⊗k)=i⊗(jxork)。
证明的话,考虑化原式为 popcount ( i and j ) + popcount ( i and k ) ≡ popcnt ( i and ( j xor k ) ) \operatorname{popcount}(i \operatorname{and} j) + \operatorname{popcount}(i \operatorname{and} k) \equiv \operatorname{popcnt}(i \operatorname{and} (j \operatorname{xor} k)) popcount(iandj)+popcount(iandk)≡popcnt(iand(jxork)),然后按位分讨即可,这里就不详细写了。
然后我们构造 F i ′ = ∑ i ⊗ j = 0 F j − ∑ i ⊗ j = 1 F j F'_i = \sum_{i \otimes j = 0}F_j- \sum_{i \otimes j = 1}F_j Fi′=∑i⊗j=0Fj−∑i⊗j=1Fj。
那么有:
A
′
×
B
′
=
(
∑
i
⊗
j
=
0
A
j
−
∑
i
⊗
j
=
1
A
j
)
(
∑
i
⊗
k
=
0
B
k
−
∑
i
⊗
k
=
1
B
k
)
=
(
∑
i
⊗
j
=
0
A
j
)
(
∑
i
⊗
k
=
0
B
k
)
−
(
∑
i
⊗
j
=
0
A
j
)
(
∑
i
⊗
k
=
1
B
k
)
−
(
∑
i
⊗
j
=
1
A
j
)
(
∑
i
⊗
k
=
0
B
k
)
+
(
∑
i
⊗
j
=
1
A
j
)
(
∑
i
⊗
k
=
1
B
k
)
=
∑
i
⊗
(
j
xor
k
)
=
0
A
j
B
k
−
∑
i
⊗
(
j
xor
k
)
=
1
A
j
B
k
=
C
′
\begin{aligned} A' \times B' &= (\sum_{i \otimes j = 0}A_j- \sum_{i \otimes j = 1}A_j)(\sum_{i \otimes k = 0}B_k- \sum_{i \otimes k = 1}B_k) \\ &= (\sum_{i \otimes j = 0}A_j)(\sum_{i \otimes k = 0}B_k) - (\sum_{i \otimes j = 0}A_j)(\sum_{i \otimes k = 1}B_k) - (\sum_{i \otimes j = 1}A_j)(\sum_{i \otimes k = 0}B_k) + (\sum_{i \otimes j = 1}A_j)(\sum_{i \otimes k = 1}B_k) \\ &=\sum_{i \otimes (j \operatorname{xor} k) = 0} A_jB_k - \sum_{i\otimes (j \operatorname{xor} k) = 1} A_jB_k \\ &= C' \end{aligned}
A′×B′=(i⊗j=0∑Aj−i⊗j=1∑Aj)(i⊗k=0∑Bk−i⊗k=1∑Bk)=(i⊗j=0∑Aj)(i⊗k=0∑Bk)−(i⊗j=0∑Aj)(i⊗k=1∑Bk)−(i⊗j=1∑Aj)(i⊗k=0∑Bk)+(i⊗j=1∑Aj)(i⊗k=1∑Bk)=i⊗(jxork)=0∑AjBk−i⊗(jxork)=1∑AjBk=C′
现在的问题就在于计算贡献。
还是考虑从小到大枚举每一位 i i i。
我们设 F j ′ F'_j Fj′ 为枚举到的位置 j j j 的 F ′ F' F′ 的值, F j 0 ′ F'_{j_0} Fj0′ 表示的是 j j j 的前 i − 1 i - 1 i−1 位和第 i i i 位的 0 0 0 组成的数的 F ′ F' F′ 值, F j 1 ′ F'_{j_1} Fj1′ 同理。(注意前面的 F j ′ F'_j Fj′ 是要求的,后面的 F j 0 ′ , F j 1 ′ F'_{j_0},F'_{j_1} Fj0′,Fj1′ 是已经求出来的)
对于第 i i i 位本身就为 0 0 0 的位置,在 and \operatorname{and} and 意义下这一位必然为 0 0 0,所以这个位置的贡献等于 j j j 在前 i − 1 i - 1 i−1 位所对应的两个位置的数值和,即 F j ′ = F j 0 ′ + F j 1 ′ F'_j = F'_{j_0} + F'_{j_1} Fj′=Fj0′+Fj1′。
对于第 i i i 位为 1 1 1 的位置,显然 j j j 对应的第 i i i 位为 0 0 0 的位置会对其产生正贡献,第 i i i 位为 1 1 1 的位置会对其产生负贡献,所以 F j ′ = F j 0 ′ − F j 1 ′ F'_j = F'_{j_0} - F'_{j_1} Fj′=Fj0′−Fj1′。
如果你学过小学奥数就可以发现就是在原来的基础上乘上了一个
1
2
\frac{1}{2}
21。
inline void Xor(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < (len >> 1); j++)
{
f[i + j] = GetMOD(f[i + j] + f[i + j + k]);
f[i + j + k] = GetMOD(f[i + j] - f[i + j + k] * 2LL % MOD + MOD);
f[i + j] = 1LL * f[i + j] * t % MOD, f[i + j + k] = 1LL * f[i + j + k] * t % MOD;
}
}
return;
}
贴上完整代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define MAXN 200005
#define MOD 998244353
#define Inv2 499122177
using namespace std;
int n, A[MAXN], B[MAXN], a[MAXN], b[MAXN];
inline int GetMOD(int k) { if(k >= MOD) k -= MOD; return k; }
inline void Copy() { return memcpy(a, A, sizeof A), memcpy(b, B, sizeof B), void(); }
inline void Multiply() { for(register int i = 0; i < n; i++) a[i] = 1LL * a[i] * b[i] % MOD; return; }
inline void Output() { for(register int i = 0; i < n; i++) printf("%d ", a[i]); return putchar('\n'), void(); }
inline void Or(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < k; j++)
f[i + j + k] = GetMOD(f[i + j + k] + GetMOD(1LL * f[i + j] * t % MOD));
}
return;
}
inline void And(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < k; j++)
f[i + j] = GetMOD(f[i + j] + GetMOD(1LL * f[i + j + k] * t % MOD));
}
return;
}
inline void Xor(int *f, int t = 1)
{
for(register int len = 2; len <= n; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < n; i += len)
for(register int j = 0; j < (len >> 1); j++)
{
f[i + j] = GetMOD(f[i + j] + f[i + j + k]);
f[i + j + k] = GetMOD(f[i + j] - f[i + j + k] * 2LL % MOD + MOD);
f[i + j] = 1LL * f[i + j] * t % MOD, f[i + j + k] = 1LL * f[i + j + k] * t % MOD;
}
}
return;
}
int main()
{
scanf("%d", &n), n = (1 << n);
for(register int i = 0; i < n; i++) scanf("%d", A + i);
for(register int i = 0; i < n; i++) scanf("%d", B + i);
Copy(), Or(a), Or(b), Multiply(), Or(a, MOD - 1), Output();
Copy(), And(a), And(b), Multiply(), And(a, MOD - 1), Output();
Copy(), Xor(a), Xor(b), Multiply(), Xor(a, Inv2), Output();
return 0;
}
有了 FWT \operatorname{FWT} FWT,我们还可以做一件事情——子集卷积:
给定序列
A
A
A 和
B
B
B,序列
C
C
C 满足:
C
k
=
∑
i
and
j
=
0
,
i
or
j
=
k
A
i
B
j
C_k = \sum_{i \operatorname{and} j = 0, i \operatorname{or} j = k} A_i B_j
Ck=iandj=0,iorj=k∑AiBj
求出
C
C
C。
题解:
对于限制 i or j = k i \operatorname{or} j = k iorj=k,废物树可以直接干。
对于限制 i and j = 0 i \operatorname{and} j = 0 iandj=0,我们发现可以转化为 ∣ i ∣ + ∣ j ∣ = ∣ i and j ∣ |i| + |j| = |i \operatorname{and} j| ∣i∣+∣j∣=∣iandj∣。
所以,我们对每个集合大小跑一遍 FWT \operatorname{FWT} FWT,然后计算的时候同时用两层循环枚举集合大小即可,最后再跑 IFWT \operatorname{IFWT} IFWT。
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#define FILE
#define Bitcnt(k) __builtin_popcount(k)
#define MAXN 2000005
#define MAXLOG 21
#define MOD 1000000009
using namespace std;
int n, m, a[MAXLOG][MAXN], b[MAXLOG][MAXN], c[MAXLOG][MAXN];
inline int GetMOD(int k) { if(k >= MOD) k -= MOD; return k; }
inline void FWT(int *f, int t = 1)
{
for(register int len = 2; len <= m; len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < m; i += len)
for(register int j = 0; j < k; j++)
f[i + j + k] = GetMOD(f[i + j + k] + 1LL * f[i + j] * t % MOD);
}
return;
}
int main()
{
scanf("%d", &n), m = (1 << n);
for(register int i = 0; i < m; i++) scanf("%d", &a[Bitcnt(i)][i]);
for(register int i = 0; i < m; i++) scanf("%d", &b[Bitcnt(i)][i]);
for(register int i = 0; i <= n; i++) FWT(a[i]), FWT(b[i]);
for(register int i = 0; i <= n; i++)
for(register int j = 0; j <= i; j++)
for(register int k = 0; k < m; k++) c[i][k] = GetMOD(c[i][k] + 1LL * a[j][k] * b[i - j][k] % MOD);
for(register int i = 0; i <= n; i++) FWT(c[i], MOD - 1);
for(register int i = 0; i < m; i++) printf("%d ", c[Bitcnt(i)][i]);
return 0;
}
子集反演
实际上,子集反演的前置知识就是上面提到的废物树和子集卷积。
它其实就是类似这样的一个式子:
F
(
S
)
=
∑
T
⊂
S
G
(
T
)
⇔
G
(
S
)
=
∑
T
⊂
S
(
−
1
)
∣
S
−
T
∣
F
(
T
)
F(S) = \sum_{T \subset S} G(T) \Leftrightarrow G(S) = \sum_{T \subset S} (-1)^{|S - T|} F(T)
F(S)=T⊂S∑G(T)⇔G(S)=T⊂S∑(−1)∣S−T∣F(T)
我们设 F ( S ) F(S) F(S) 为有多少种方案使得玩具的集合是 S S S 的子集,设 f ( S ) f(S) f(S) 为有多少个盒子的玩具集合是 S S S 的子集,那么显然有 F ( S ) = 2 f ( S ) − 1 F(S) = 2^{f(S)} - 1 F(S)=2f(S)−1(减不减 1 1 1 无所谓,反正最后会容斥掉)。再设 G ( S ) G(S) G(S) 为有多少种方案使得玩具集合恰好为 S S S,那么可以直接套用上面的子集反演公式。
(事实上,大多数计数题也用到了这个设置状态的思路,即设置 “至少”、“至多” 和 “恰好”。)
然后考虑求 f ( S ) f(S) f(S),可以发现这就是一个废物树的变换,直接套上即可。
(这里提一句,
f
(
S
)
f(S)
f(S) 实际上称作高维前缀和,而这种用
FWT
\operatorname{FWT}
FWT 优化其的方法称做
s
o
s
_
d
p
sos\_dp
sos_dp,又叫求救优化。)
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#define MAXN 2000005
#define MAXM 21
#define MOD 1000000007
using namespace std;
int n, m, f[MAXN], F[MAXN], ans;
inline int GetMOD(int k) { if(k >= MOD) k -= MOD; return k; }
inline int Bitcnt(int k) { return __builtin_popcount(k); }
inline void FWT(int *f)
{
for(register int len = 2; len <= (1 << m); len <<= 1)
{
int k = len >> 1;
for(register int i = 0; i < (1 << m); i += len)
for(register int j = 0; j < k; j++)
f[i + j + k] = GetMOD(f[i + j + k] + f[i + j]);
}
return;
}
inline int Pow(int k, int p)
{
int res = 1;
while(p)
{
if(p & 1) res = 1LL * res * k % MOD;
k = 1LL * k * k % MOD;
p >>= 1;
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for(register int i = 1; i <= n; i++)
{
int ki; scanf("%d", &ki);
int st = 0;
for(register int j = 1; j <= ki; j++)
{
int t; scanf("%d", &t);
st |= (1 << (t - 1));
}
++f[st];
}
FWT(f);
for(register int i = 0; i < (1 << m); i++) F[i] = Pow(2, f[i]) - 1;
for(register int i = 0; i < (1 << m); i++)
{
if(Bitcnt(((1 << m) - 1) ^ i) & 1) ans = GetMOD(ans - F[i] + MOD);
else ans = GetMOD(ans + F[i]);
}
printf("%d\n", ans);
return 0;
}
425



被折叠的 条评论
为什么被折叠?



