对角化
在众多 dp 问题中,我们经常可以用矩阵快速幂进行优化。更进一步地,如果这个递推矩阵是一个形如 (A = begin{pmatrix} 3 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & 9 end{pmatrix}),矩阵快速幂就显得大财小用了。因为显然 (A^k = begin{pmatrix} 3^k & 0 & 0 \ 0 & 1^k & 0 \ 0 & 0 & 9^k end{pmatrix})。
对于这种只有主对角线上有值的矩阵,称为对角矩阵,它显然拥有很好的性质。
那么我们不禁思考,如何将一个普通矩阵变为对角矩阵 (Lambda) 呢。
矩阵是线性变换,为什么对角阵有好的性质,是因为其仅仅是在各个坐标轴方向上的伸缩。
对于一个普通的矩阵,如果我们能通过相似变换把它换到对角阵的基下,那么计算它的幂次将也非常容易。
具体来说,我们反过来做。如果有一个对角阵 (Lambda),考虑先将其通过 (P^{-1}) 转换坐标到矩阵 (A) 的基底的意义下,然后施加操作 (A),最后再 (P) 回来。这与直接做 (Lambda) 的效果是一样的。
也就是 (Lambda = P^{-1} A P)。
现在的问题变成了如何求 (P)。两边同时左乘 (P),得到 (P Lambda = A P)。我们设 (Lambda = operatorname{diag}(lambda_1,lambda_2, cdots, lambda_n)),(P = [p_1, p_2, p_3, cdots, p_n])(列向量组),最终左边乘出来的结果是 ([lambda_1p_1,lambda_2p_2, cdots, lambda_np_n]),右边乘出来是 ([Ap_1, Ap_2, cdots, Ap_n])。由于我们还要求 (P) 可逆,因此,选出来的这个 (p) 向量组还得线性无关。也就是说,对于这个矩阵 (A),我们要找到一些向量 (p_1,p_2,cdots,p_n),满足 (Ap_i = lambda_i p_i)。这就引出了我们接下来的主角。
特征值与特征向量
如果对于矩阵 (A),存在一个 非零向量 (x),和值 (lambda),满足 (lambda x = Ax)。那么就称 (lambda) 是 (A) 的一个特征值,(x) 是 (A) 的一个属于 (lambda) 的特征向量。
意思是,(x) 在 (A) 的作用下只做了伸缩操作。同时这个 (x) 不能为 0,要不然上面的 (P) 矩阵肯定就线性相关了。同时注意,特征向量表示的是矩阵变换中只有伸缩变换没有旋转变换的方向向量,特征值是这个方向的伸缩系数,一个方向当然只有一个伸缩系数。所以,不同特征值对应的特征向量是互相线性无关的。
考虑如何求解特征值和特征向量,对上式移项,得 ((A - lambda I) x = 0)。由于 (x) 不是 0,这意味着发生了坍缩,(det(A - lambda I)) 要等于 (0)。这个行列式是一个关于 (lambda) 的 (n) 次多项式,我们考虑对其进行因式分解,变成
注意这里我们把重根合并起来写了。然后就可以解出所有特征值了。如果还想求解特征向量,我们把每一个特征值回代,然后解出哪些向量在该线性变化下被缩到了 0。具体来说就是求出一个齐次线性方程组的基础解系之后再求出通解。
回到上面,我们称 (f(lambda)) 是矩阵 (A) 的特征多项式,(alpha_i) 是 (lambda_i) 的代数重数,而一个 (lambda_i) 对应所有线性无关的特征向量的个数叫做几何重数,也即 (dim ker(A - lambda I))(直观上就是能让几个不同方向的向量坍缩到 0)。注意到 (sum alpha_i = n)。
接下来,我们给出一条重磅性质,一个矩阵 (A) 能被相似对角化,当且仅当对于每个特征值几何重数 = 代数重数。
为了解决上面那个问题,我们进一步考虑,什么情况下一个矩阵无法被相似对角化。
一开始,我们提到 (P = [p_1, p_2, p_3, cdots, p_n]),也就是说,我们必须能选出 (n) 个线性无关的向量,来作为相似变换的 (P) 矩阵。观察发现,这个几何重数之和正好对应了我们能选出多少个线性无关的向量。那么稍微想一下,上面那个结论就很自然了。
既然有一些矩阵注定无法被对角化,那么就拿他们没办法了吗?由于最小多项式和特征多项式的一些性质和接下来的内容是分不开的,所以我们花一些篇幅来讲如何对一些没办法完全对角化的矩阵进行类似对角化的化简。
广义特征向量与 Jordan 型
我们延续上面对角化的思路,采用相似变换的方式。现在的问题是,我们在 $ker(A - lambda I) $ 中找不到足够的线性无关的向量。那么我们能否找一些别的向量来替补呢?
为此我们引入广义特征向量链,具体来说:特征向量 (x) 是满足 ((A - lambda I) x = 0) 的向量,那么层级(秩)为 (k) 的广义特征向量 (x) 是满足 ((A - lambda I)^k x = 0),并且 ((A - lambda I)^{k- 1} x ne 0) 的非零向量。现在我们不是孤立地看一个广义特征向量,而是看一串有递推关系的向量。我们从一个秩最高的广义特征向量开始构造一条链。我们先选择一个秩为 (m) 的广义特征向量 (v_m)。然后我们逆向生成 (v_{m - 1} = (A - lambda I) v_m)。以此类推 (v_i = (A - lambda I)v_{i + 1})。这样我们最多生成 (m) 个向量,因为到 (v_1 = (A - lambda I)^{m - 1} v_m) 时,再乘一个就变成 0 了,也就是说 ((A - lambda I) v_1 = 0)。那么 (v_1) 就是特征向量。
进一步地,我们发现上面这个递推式 (v_i = (A - lambda I)v_{i + 1} iff Av_{i + 1} = lambda v_{i + 1} + v_i)。也就是说,我们如果正向考虑整个过程,(Av_{i + 1}) 相当于在正常特征向量只在本方向拉伸的基础上,混入了上一层向量的影响。现在,我们在 ({v_1, v_2, cdots, v_m}) 的基意义下考虑 (A) 的影响。由于 (Av_{i + 1} = lambda v_{i + 1} + v_i),也就是说 (A) 的影响是这样的:
这个特殊的矩阵被称为特征值 (lambda) 的 (m) 阶 Jordan 块。更精确地,Jordan 块 (J_m(lambda) = (a_{ij})) 的元素满足:(a_{ii} = lambda) (对角线元素均为特征值 (lambda)),(a_{i,i+1} = 1) (主对角线上方的次对角线元素为 1),(a_{ij} = 0) (其他所有元素为 0)。这个矩阵已经近乎于对角矩阵了,如果我们能通过相似变换把矩阵 (A) 化为这种形式,也是一种不错的结果。下面给出步骤:
- 求出所有的特征值 (lambda)。
- 对于每个特征值,令 (N = A - lambda I),有 (ker (N^i) subset ker (N^{i + 1}))。并且我们总能找到一个最小的 (d),使得 (ker(N^d) = ker(N^{d + 1})),这个核称为 (lambda) 的广义特征空间。这是因为核的维度随着幂次而增长,而维度是有上限的((A) 只是一个 (n times n) 矩阵),而一旦停止增长之后就不会重新恢复增长。注意,我们有结论 (dim(ker(N^d))) 就是 (lambda) 的代数重数 (alpha)。现在我们开始构造这个特征值对应的 Jordan 块。假设这个特征值对应的 (k) 阶 Jordan 块有 (b_k) 个,那么要满足 (sum k times b_k) 为代数重数(要有代数重数个向量),(sum b_k) 为几何重数(也就是对应有多少个广义特征向量链)。观察到 (dim(ker(N^j)) - dim(ker(N^{j - 1})) = sum_{i ge j} b_i),因为每个 (ge j) 的 Jordan 块中都会包含一个 (ker(N^j) - ker(N^{j - 1})) 的向量。
因此 (b_k = sum_{i ge k}b_i - sum_{i ge k + 1}b_i = 2dim(ker(N^k)) - dim(ker(N^{k - 1})) - dim(ker(N^{k + 1})))。接着对每个 Jordan 块找到它对应广义特征向量链。具体来说,先从 (ker(N^k) - ker(N^{k - 1})) 找一个向量 (v),然后继续在 (ker(N^k) - ker(N^{k - 1})) 找,并且我们找到的这个向量不能再之前已经找到的向量的张成之中,直到这其中找不到,再去 (ker(N^{k - 1}) - ker(N^{k - 2})) 找。不难发现这样的构造刚好满足了前面的那两个条件。 - 合并每个特征值的特征向量空间。就像各个特征值对应的特征向量线性无关,我们每个特征值的广义特征空间也是不交的,这是保证矩阵可逆的条件。我们构造矩阵 (P),对于每条链 (v_1, v_2, cdots, v_{k}),我们按顺序把它放进 (P),也就是 (P = (cdots,v_1,v_2,cdots,v_{k},cdots))。
- 最后,我们有 (J = P^{-1}AP)。其中 (J) 是一个分块对角矩阵 (operatorname{diag}(J_1,J_2,cdots,J_{g})),其中 (g) 是所有特征值的几何重数之和,每一个 (J_i) 是一个 Jordan 块,注意这里向量链的放入顺序要和 Jordan 块一致。可以验证,此时,(PJ = AP)。
这个 (J) 就是 Jordan 型。这个是对于任意矩阵都是存在且唯一的,也是在无法对角化的情况下,能被化简到的最好的情况。Jordan 型同时给出了一种把所有矩阵划分为如果等价类的方法(如果我们不考虑 Jordan 块之间的顺序)。
最小多项式
回到一开始,我们选择将一个矩阵变成 Jordan 型(对角矩阵是特殊的 Jordan 型),是因为计算他的幂将会变得非常容易。而当我们处理矩阵多项式时,就避不开幂的处理。
别忘了标题的后半部分线性递推,到现在我们仍然没有讨论过。还是以 dp 问题为背景。假设从第 (i) 个阶段到第 (i + 1) 个阶段,有 (f_{i + 1} = Af_i),其中 (f_i) 是一个向量(表示所有 (i) 阶段的 dp 值),(A) 是转移矩阵。那么 (f_{k} = A^k f_0)。如果存在一个次数为 (p) 的矩阵多项式 (Q(A) = 0) 也就是 0 矩阵,并且我们求出 (R(A) = A^k pmod {Q(A)})(就算用暴力多项式取模,搭配快速幂也只需要 (O(p^2 log k)) 求这个),也就是说 (A^k = Q(A)P(A) + R(A) = R(A)),那么:
也就是说,只要递推出前 (p - 1) 项,就可以算出 (k) 很大时的情况。
那么问题就变成了求一个次数最小的 (Q(A) = 0),我们也称这样能让 (A) 最后变成 0 的多项式叫化零多项式,其中次数最小的首一多项式就叫做最小多项式。
别忘了,现在我们处理矩阵幂有了强有力的工具,Jordan 型。那么对于矩阵多项式 (f(A)) 和 (A) 对应的 (J),有 (f(A) = Pf(J)P^{-1})。这是因为 (J = P^{-1}AP iff A = PJP^{-1}),而 (A^k = PJ^kP^{-1})。这揭示了相似变换不改变化零多项式的性质,想让 (A) 化零等价于让 (J) 化零(当然,相似变换也不改变最小多项式)。
那么我们先研究如何让一个 Jordan 块化 0。可以发现,想让一个 (J_k(lambda)) 化零,((J - lambda I)^k) 是一个次数最小的多项式。因为 (N = J - lambda I) 是一个只有主对角线上方一条斜线全是 1 的矩阵,乘一次就会让这个斜线向右上方移动一格。
貌似一切都明朗了。对于一个矩阵 (A),我们找到其每个特征值 (lambda_i),然后求出它的 Jordan 型 (J)。对于每个 (lambda_i),假设其对应的 Jordan 块中阶数最大的是 (d_i),那么这个 Jordan 型的最小多项式(也是 (A) 的最小多项式)就是 :
进一步地,由于 (d_i) 小于等于代数重数,因此 (m_A(A) | f(A)),也就是说特征多项式 (f) 如果直接带入 (A) 的,也是可以化零的,这一结论也即 Cayley-Hamilton 定理。
在实际应用时,我们可以直接根据特征值猜最小多项式。注意,我们线性递推时只是用了化零的性质来简化计算,有些时候最小多项式不便观察时,直接用特征多项式/别的化零多项式也是可以的(只要复杂度不爆,也就是特征多项式的次数可以接受)。
一道例题
GDKOI2023 马戏团里你最忙:有一个数字,初始是 (x_0)。进行 (K) 次操作,第 (i) 次操作从 ([0, 2^n)) 均匀随机一个数字 (x),(x_i) 有 (p) 的概率是 (x_{i - 1} operatorname{or} x),有 (1 - p) 的概率是 (x_{i - 1} operatorname{and} x)。一种方案的权值是 (sum_{i=1}^k c_{x_{i}})。对每个 (i in [0, 2^n)) 求出,(x_K = i) 的所有方案中,权值乘概率之和,对 (998244353) 取模。(K le 10^9, n le 17)。
考虑暴力 (f_{i, j}) 表示做了 (i) 次操作,最后一位是 (j) 的概率,(g_{i, j}) 是对应的答案。转移高维前缀和(FWT),可以获得 20 pts。
我们考虑 (n = 1) 时怎么做。先考虑概率,此时 and 的转移矩阵是 (X = begin{pmatrix} 1 & frac{1}{2} \ 0 & frac{1}{2} end{pmatrix}),or 的转移矩阵是 (Y = begin{pmatrix} frac{1}{2} & 0 \ frac{1}{2} & 1 end{pmatrix})。为了推广到 (n) 更大的情况,这里我们引入矩阵张量积的概念:
设有两个矩阵 (A) 和 (B),其中 (A) 是一个 (m times n) 的矩阵,(B) 是一个 (p times q) 的矩阵。(A) 和 (B) 的张量积(Kronecker积),记作 (A otimes B),是一个 ((m times p) times (n times q)) 的分块矩阵。它的定义如下:
[A otimes B = begin{pmatrix} a_{11}B & a_{12}B & cdots & a_{1n}B \ a_{21}B & a_{22}B & cdots & a_{2n}B \ vdots & vdots & ddots & vdots \ a_{m1}B & a_{m2}B & cdots & a_{mn}B end{pmatrix}]可以验证:((AB) otimes (CD) = (A otimes C)(B otimes D)),进一步地 ((Potimes Q)^{-1} = P^{-1} otimes Q^{-1})。'
由于每一位的转移情况是类似的,我们惊奇地发现,概率的转移矩阵正好就是 (A = pY^{otimes n} + (1 - p)X^{otimes n})。可以自行验证。那么再把 (g) 加进来,整个转移就是 (begin{pmatrix} f_{i + 1} \ g_{i + 1} end{pmatrix} gets begin{pmatrix} A & 0 \ B & A end{pmatrix} begin{pmatrix} f_i \ g_i end{pmatrix} := T begin{pmatrix} f_i \ g_i end{pmatrix}),其中 (B_{i, j} = c_i A_{i, j})。
先考虑求 (A) 的最小多项式。因为下三角/上三角矩阵的特征值是方便观察的,即对角线上的元素(可以把 (det) 算一下)。而这里 (X,Y) 都是三角的,并且他们对角线上的元素都是 ({1, frac{1}{2}})。那么 (X^{otimes n}) 和 (Y^{otimes n}) 也都是三角的,并且可以发现对角线上的元素都是 ({2^{-i} | i in mathbb{N} cap [0, n]})。实际上这一步可以不用观察,根据这里的思路 (C = A otimes B) 时,他们的特征值集合也会相应地进行笛卡尔积,考察 Jordan 型即可说明。那么 (X^{otimes n},Y^{otimes n}) 的最小多项式就是 (prod_{i = 0}^n(lambda - 2^{-i}))((X,Y) 的张量积幂次是可对角化的,因为 (X) 和 (Y) 都是可对角化的)。我们加减和数乘什么的肯定不改变最小多项式的化零性的,可能就系数变一变,而这里前面的系数加起来刚好为 1。最后就可以得出 (A) 的最小多项式 (m_A(lambda) = prod_{i = 0}^n(lambda - 2^{-i}))。更严谨的证明请见masterhuang大佬的题解。
求出 (m_A) 之后,我们发现 ((m_A(T))^2) 就是 (T) 的一个化零多项式。这是因为 (T^k = begin{pmatrix} A^k & 0 \ B' & A^k end{pmatrix}),所以 (m_A(T) = begin{pmatrix} 0 & 0 \ B' & 0 end{pmatrix}),这里的 (B') 并不重要,因为我们自乘一次,就可以发现 ((m_A(T))^2 = begin{pmatrix} 0 & 0 \ B' & 0 end{pmatrix}begin{pmatrix} 0 & 0 \ B' & 0 end{pmatrix} = begin{pmatrix} 0 & 0 \ 0 & 0 end{pmatrix})。
那么我们就取 (Q(T) = prod_{i = 0}^n(lambda - 2^{-i})^2) 作为 (T) 的化零多项式,这个次数是 (2n + 2) 的,非常可以接受。然后再像上面说的那样,把 (R(T) = T^k pmod {Q(T)}) 也求出来。这样,我们就只需要算出前 (2n + 1) 项就可以了,这个直接跑 20 pts 的暴力即可。
瓶颈不在后面的多项式操作,因此直接跑 (O(N^2)) 的卷积和多项式除法也是可过的。复杂度就是 (O(n^22^n + n^2 log K))。
Code
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 17, mod = 998244353, i2 = 499122177; ll f[1 << N], g[1 << N], pf[1 << N], pg[1 << N], sf[1 << N], sg[1 << N], n, p, k, x, pw[20], ipw[20], c[1 << N], ans[1 << N]; int pop[1 << N]; void add(ll &x, ll y){ (x += y) %= mod; } void getp(ll s[]){ for(int j = 0; j < n; ++j){ for(int i = 0; i < (1 << n); ++i){ if(i & (1 << j)) add(s[i], s[i ^ (1 << j)]); } } } void gets(ll s[]){ for(int j = 0; j < n; ++j){ for(int i = 0; i < (1 << n); ++i){ if(!(i & (1 << j))) add(s[i], s[i ^ (1 << j)]); } } } struct poly{ ll d, a[4 * N + 5]; void operator *= (const poly & b) { poly c; memset(c.a, 0, sizeof(c.a)); c.d = d + b.d; for(int i = 0; i <= c.d; ++i){ for(int j = 0; j <= i; ++j){ add(c.a[i], a[j] * b.a[i - j] % mod); } } *this = c; } void operator %= (const poly &b){ for(int i = d; i >= b.d; --i){ ll k = mod - a[i]; for(int j = 0; j <= b.d; ++j){ add(a[i - j], k * b.a[b.d - j] % mod); } } if(d >= b.d) d = b.d - 1; } }Q, tmp, R; poly qpow(poly a, int k){ poly ret; memset(ret.a, 0, sizeof(ret.a)); ret.d = 0, ret.a[0] = 1; for(; k; k >>= 1, a *= a, a %= Q){ if(k & 1) ret *= a, ret %= Q; } return ret; } int main(){ cin.tie(nullptr)->sync_with_stdio(0); cin >> n >> p >> k >> x; pw[0] = ipw[0] = 1; int D = (1 << n) - 1; for(int i = 1; i <= n; ++i){ pw[i] = (pw[i - 1] * 2) % mod; ipw[i] = (ipw[i - 1] * i2) % mod; } for(int i = 0; i <= D; ++i){ cin >> c[i]; for(int j = 0; j < n; ++j){ if(i & (1 << j)) pop[i]++; } } Q.d = 0, Q.a[0] = 1; for(int i = 0; i <= n; ++i){ tmp.d = 1, tmp.a[0] = mod - ipw[i], tmp.a[1] = 1; Q *= tmp, Q *= tmp; } tmp.d = 1, tmp.a[0] = 0, tmp.a[1] = 1; R = qpow(tmp, k); f[x] = 1; int d = 2 * n + 1; for(int i = 0; i < min(1ll * d, k); ++i){ memset(pf, 0, sizeof(pf)); memset(pg, 0, sizeof(pg)); memset(sf, 0, sizeof(sf)); memset(sg, 0, sizeof(sg)); for(int s = 0; s < (1 << n); ++s){ ll o = pop[s], pp = (mod + 1 - p) * pw[n - o] % mod * ipw[n] % mod; add(sf[s], f[s] * pp % mod); add(sg[s], g[s] * pp % mod); pp = p * pw[o] % mod * ipw[n] % mod; add(pf[s], f[s] * pp % mod); add(pg[s], g[s] * pp % mod); } memset(f, 0, sizeof(f)); memset(g, 0, sizeof(g)); gets(sf), gets(sg), getp(pf), getp(pg); for(int s = 0; s < (1 << n); ++s){ f[s] = (sf[s] + pf[s]) % mod; g[s] = (sg[s] + pg[s] + f[s] * c[s] % mod) % mod; add(ans[s], g[s] * R.a[i + 1] % mod); } } if(k <= d){ for(int i = 0; i < (1 << n); ++i){ cout << g[i] << ' '; } return 0; } for(int i = 0; i < (1 << n); ++i) cout << ans[i] << ' '; return 0; }