線性遞迴淺淺談

24 minute read

本篇將會介紹快速求線性遞迴數列某項的方法,以及Berlekamp-Massey演算法和一些在矩陣上的應用。
主要是一個整理資料還有學習筆記的功能,還有老實說這東西算是偏門又毒瘤,追求實用的人不要看XD。

Fast Linear Recurrence

首先先來介紹如何快速求線性遞迴。

定義

已知序列 ⟨an⟩ \langle a_n \rangle 滿足遞迴關係 ∀i≥k,ai=∑j=0k−1sjai−1−j \displaystyle \forall i \geq k, a_i = \sum _ {j=0} ^ {k-1} s _ j a _ {i-1-j} ,並且已經給定 ss 跟 a0,a1,…,ak−1a_0, a_1, \dots, a _ {k-1}
現在想要求 an a_n 的值,其中 1≤k≤5000,0≤n≤109 1 \leq k \leq 5000, 0 \leq n \leq 10^9

許多人大概會很快想到矩陣快速冪,複雜度是 O(k3log⁡n) \mathcal{O}(k^3 \log n) 。但我們要更快!

通靈

定義一個函數 GG,對於形式冪級數 f(x)=∑cixi,G(f)=∑ciaif(x) = \sum c_i x^i, G(f) = \sum c_i a_i 。
顯然G(f±g)=G(f)±G(g)G(f \pm g) = G(f) \pm G(g)。
根據 ss 構造一個多項式 SS

S(x)=xk−∑i=0k−1sixk−1−i S(x) = x^k - \sum _ {i=0} ^ {k-1} s_i x^{k-1-i}

可以發現 G(S)=0G(S) = 0,因為代進去正好是 ai−∑j=0k−1sjai−1−j=0a_i - \sum\limits _ {j=0} ^ {k-1} s _ j a _ {i-1-j} = 0 。
而且,平移之後也會滿足遞迴關係,所以 G(Sx)=G(Sx2)=⋯=0G(Sx) = G(Sx^2) = \dots = 0。
由上面兩條可以得知,對於任何多項式ff都有G(Sf)=0G(Sf) = 0。

我們想求的第nn項,正好就是G(xn)G(x^n),不妨取 f=⌊xnS⌋\displaystyle f = \lfloor \frac{x^n}{S} \rfloor,則G(xn)=G(xn−Sf)=G(xnmod  S)G(x^n) = G(x^n - Sf) = G(x^n \mod S)。
上述⌊xnS⌋\lfloor \frac{x^n}{S} \rfloor 和 xnmod  Sx^n \mod S 分別是多項式帶餘除法的商和餘數。

結論

證明也很快。
總而言之,只要求得xnmod  Sx^n \mod S(注意這個東西的degree是O(k)\mathcal{O}(k)),再帶進GG就能得到 ana_n!
可以對nn做類似快速冪的事情,每次算xn/2⋅xn/2mod  Sx^{n/2} \cdot x^{n/2} \mod S之類的,如果是mod質數的話,甚至可以利用FFT或是NTT來快速多項式帶餘除法作到總複雜度 O(klog⁡klog⁡n)\mathcal{O}(k\log k \log n),不過超出今天的篇幅所以這邊只放O(k2log⁡n)\mathcal{O}(k^2 \log n)的寫法。
老實說O(k2)\mathcal{O}(k^2)的mod  S\mod S比想像中好寫,只要從大到小不斷把xkx^k換成∑i=0k−1sixk−1−i \sum\limits _ {i=0} ^{k-1} s_i x^{k-1-i} 就好

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <typename T>
T fastLinearRecurrence(vector<T> a, vector<T> s, int n) {
    if (n < (int)a.size()) return a[n];
    if (s.empty()) return 0;
    // a 是前 k 項, s是遞迴關係,
    // a[i] = \sum s[j] * a[i-j-1]
    vector<T> r = {1}; // 1
    vector<T> e = {0, 1}; // x;
    auto mul = [&s](vector<T> a, vector<T> b) {
        // return a * b % (x^k - s)
        vector<T> c(a.size() + b.size() - 1);
        for (size_t i = 0; i < a.size(); i++)
            for (size_t j = 0; j < b.size(); j++)
                c[i+j] += a[i] * b[j];
        for (size_t i = c.size()-1; i >= s.size(); i--)
            for (size_t j = 0; j < s.size(); j++)
                c[i-j-1] += c[i] * s[j];
        c.resize(s.size());
        return c;
    };
    while (n) {
        if (n & 1)
            r = mul(r, e);
        e = mul(e, e);
        n >>= 1;
    }
    T sum = 0;
    for (size_t j = 0; j < r.size(); j++) sum += r[j] * a[j];
    return sum;
}

上面的T可以是double或是自定義的同餘算術型別。

不過這其實有點不太實用。通常這種線性遞迴數列的遞迴式基本上長度都是超小的常數,而需要交給電腦推的遞迴式通常都是形如vi=Aiv0v _ i = A^i v _ 0的形式,也就是說有很多個狀態要一起考慮,沒辦法想成單一變數的線性遞迴形式,矩陣快速冪的通用性還是比較高。後面也許會提到這種東西要怎麼算得比矩陣快速冪還快,不過如果AA不是稀疏的也只是少一個log⁡\log而已。

Berlekamp-Massey 演算法

這東西可神奇了,可以說是弱化版的OEIS。不過還有很多神奇的應用。
以下簡稱Berlekamp-Massey演算法為BM演算法。

定義

給定一個序列 a0,a1,…,an−1 a_0, a_1, \dots, a _ {n-1} ,請找出一個階數最小的線性遞迴關係 ⟨si⟩ \langle s _ i \rangle ,
使得 ∀i≥k,ai=∑j=0k−1sjai−1−j \forall i \geq k, a_i = \sum\limits _ {j=0} ^ {k-1} s _ j a _ {i-1-j} 。其中 kk 即是此線性遞迴關係的階數。

當然,求出來的遞迴關係可能不是是唯一解,也不知道能不能對應到原本的遞迴式。
不過可以證明,如果已知遞迴式的階數最多是 kk 的話,只要取前 2k2k 項求解得到一個遞迴關係之後,任何更長的前綴都會吻合該關係,也就是說該關係是整個無限數列的最短遞迴式。
證明請看2019 IOI中國國家隊論文…

步驟

BM演算法包含了一些迭代法跟greedy的思維。我們由短到長逐步考慮每個前綴 [0..i][0 .. i] ,假如第 ii 個數字加進去之後和舊的遞迴關係不吻合,我們就修正該遞迴關係。一開始遞迴式 ss 初始化為空。
修正的部份可以分成兩個case:

  1. 不是第一次修正。
    假設這次誤差叫做 ε\varepsilon 好了,也就是 ai−∑j=0k−1sjai−1−j=εa_i - \sum _ {j=0} ^ {k-1} s _ j a _ {i-1-j} = \varepsilon
    因為不是第一次修正,所以設之前有一次是在加入第 i′i' 個數字後修正的,該次修正之前的遞迴式為s′s',誤差為 ε′\varepsilon'。也就是說,ai′−∑j=0k′−1sj′ai′−1−j=ε′a _ {i'} - \sum _ {j=0} ^{k'-1} s' _ j a _ {i'-1-j} = \varepsilon'
    可以發現,εε′(ax−∑j=0k′−1sj′ax−1−j)\frac{\varepsilon}{\varepsilon'} (a _ x - \sum _ {j=0} ^ {k'-1} s' _ j a _ {x-1-j})是一個僅在 x=i′x = i' 是 ε\varepsilon,其他位置是 00 的序列。在 s′s' 前面補 00 ,使得前面的項都不會被影響到,而第 ii 項恰好對到 ε\varepsilon 那一項,如此兩者相消就能讓第 ii 項的誤差變為 00 。也就是說,如果當前的 ss 出錯了,我們就會把 ss 加上「之前某次的 s′s' 乘上若干倍再平移」以修正該次的誤差。
    總之就是取前面算過的東西拿來消掉新加入的項的誤差啦。至於 i′i' 的取法似乎是要讓要補的 00 盡量少,最後的結果才會是最短的遞迴式。
    在下面的 code 中, bestPos維護的是最好的 i′i' ; best 維護的是最好的 s′s' ,取負號之後前面補 11 ,然後除以 ε′ \varepsilon'
  2. 第一次修正
    表示 ii 是第一個非零元素,將 ss 修正為 i+1i+1 個 0 ,表示前 i+1i+1 項應該會是在給定的 kk 項之中。
    best 更新成單項式 1/ε1 / \varepsilon

總之code雖然很簡潔,但好難懂…
複雜度是 O(n2)\mathcal{O}(n^2)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <typename T>
vector<T> BerlekampMassey(vector<T> a) {
    auto scalarProduct = [](vector<T> v, T c) {
        for (T &x: v) x *= c;
        return v;
    };
    vector<T> s, best;
    int bestPos = 0;
    for (size_t i = 0; i < a.size(); i++) {
        T error = a[i];
        for (size_t j = 0; j < s.size(); j++) error -= s[j] * a[i-1-j];
        if (error == 0) continue;
        if (s.empty()) {
            s.resize(i + 1);
            bestPos = i;
            best.push_back(1 / error);
            continue;
        }
        vector<T> fix = scalarProduct(best, error);
        fix.insert(fix.begin(), i - bestPos - 1, 0);
        if (fix.size() >= s.size()) {
            best = scalarProduct(s, - 1 / error);
            best.insert(best.begin(), 1 / error);
            bestPos = i;
            s.resize(fix.size());
        }
        for (size_t j = 0; j < fix.size(); j++)
            s[j] += fix[j];
    }
    return s;
}

BM 演算法的應用!

方便起見,接下來會用SLR簡稱「最短遞迴式」。
終於來到我最想分享的部份。前面code可以直接當模板抄啦,就跟Dinic一樣神秘。
但是重點在於怎麼活用BM演算法。BM演算法對於矩陣,尤其是稀疏矩陣的相性特別好。
這裡大部分都是IOI 2019中國國家隊的論文來的www

向量/矩陣的SLR

一些 nn 維向量 v0,v1,…,vn−1 \\{ v_0, v_1, \dots, v _ {n-1} \\} 的SLR怎麼求?答案是隨機生成一個向量 uu 跟他做內積,改求 uTv0,uTv1,…,uTvt−1 \\{ u^T v_0, u^T v_1, \dots, u^T v _ {t-1} \\} 的 SLR。可以證明乘上 uu 之後,SLR不變的機率至少是 1−np1 - \frac{n}{p} (pp 是模的質數)。類似的,一些 m×nm \times n 矩陣 A0,A1,…,At−1 \\{ A_0, A_1, \dots, A _ {t-1} \\} 的 SLR 求法就是生成隨機向量 u,vu, v 拿去左邊右邊乘,改求 uTA0v,uTA1v,…,uTAt−1v \\{ u^T A_0 v, u^T A_1 v, \dots, u^T A _ {t-1} v \\} 的SLR。正確的機率似乎至少是 1−n+mp1 - \frac{n+m}{p} 。
證明用到了一個我完全不會的 Schwartz-Zippel 引理。哪那麼衰XD

稀疏線性方程組

求解 Ax=bAx = b 即是想得到 x=A−1bx = A^{-1}b 。考慮 A0b,A1b,A2b…, \\{ A^0b, A^1b, A^2b \dots, \\} 的SLR,可以得知
Akb=∑i=0k−1sk−1−iAib A^kb = \sum _ {i=0} ^ {k-1} s _ {k-1-i} A^ib 。兩邊同乘 A−1A^{-1} 並移項之後可以得出

A−1b=−1sk−1(Ak−1b−∑i=1k−1sk−1−iAi−1b) A^{-1}b = -\frac{1}{s _ {k-1}}(A^{k-1}b - \sum _ {i=1} ^ {k-1} s _ {k-1-i} A^{i-1}b)


注意到 sk−1≠0s _ {k-1} \neq 0,否則他就不可能是最短。
如果 AA 是稀疏的,裡面有 ee 個非零元素,那麼依序推出 A0b,A1b,…,A2n−1b A^0b, A^1b, \dots, A^{2n-1}b 就只需要 O(n(n+e))\mathcal{O}(n(n+e))。
IOICAMP似乎有一題可以用這個搶topcoder

稠密的轉移矩陣

在前面某一段有提到,轉移長度如果不是很小的常數的話,常常會寫成像是 vi=Aiv0v_i = A^iv_0 的形式,其中 AA 是 nn 階方陣而 viv_i 是 nn 維向量,我們想求的是 vkv_k。
直接利用矩陣快速冪的方法是 n3log⁡kn^3 \log k 的。不過,我們只需要求出前 2n2n 項的SLR,再利用前述的 fast linear recurrence 就能得到第 kk 項。(不過可能要稍微改寫一下求出xkmod  fx^k \mod f之後帶進GG的部份)可以依序求出 v0,v1,…,v2n−1v_0, v_1, \dots, v _ {2n-1},複雜度 O(n3)\mathcal{O}(n^3),後面的fast linear recurrence複雜度大概是 O(n2log⁡k)\mathcal{O}(n^2\log k)之類的。
例題可以去試試TIOJ 1892 owo

矩陣的最小多項式

一個 n×nn\times n 方陣 AA 的最小多項式是 degree 最小的 pp 使得 p(A)=0p(A) = 0 。By Cayley Hamilton theorem,特徵多項式正好也是一個多項式 pp 使得 p(A)=0p(A) = 0 ,所以最小多項式的 degree 至多是 nn。
如何求?最小多項式 p(A)=∑ciAip(A) = \sum c_i A^i ,可以發現只要求 A0,A1,A2,… \\{ A^0, A^1, A^2, \dots \\} 的 SLR 就能得到最小多項式。又,最小多項式至多 nn 項,故取前 2n2n 項計算即可得到答案。

稀疏矩陣行列式

給定一個稀疏矩陣AA想求det⁡(A)\det(A)。事實上只要求得特徵方程式的常數項就可以得到行列式。
根據 wiki 上 Cayley Hamilton 定理的頁面,最小多項式是特徵多項式的因式,但他們可能不相等,不過他們的根集合是相同的,問題只是特徵多項式裡可能有重根。因此我們把AA乘上一個隨機的對角矩陣BB,可以證明ABAB沒有重根(特徵多項式與最小多項式相等)的機率至少是 1−2n2−np1 - \frac{2n^2 - n}{p}。似乎同樣又是Schwartz-Zippel。
總之,求得 ABAB 的特徵多項式 pp ,並設 pp 的領導係數是 11 (如果不是就除掉),那麼 det⁡(AB)\det(AB) 就是 (−1)∣A∣⋅p(0)(-1)^{|A|} \cdot p(0) 也就是 pp 的常數項差一個正負號。 det⁡(A)=det⁡(AB)/det⁡(B)\det(A) = \det(AB) / \det(B) 就完成了。
這個是之前打BambooFoxCTF google and copy過別人的XD現在才來完整了解作法

Reference

https://codeforces.com/blog/entry/61306
https://www.cnblogs.com/zzqsblog/p/6877339.html
https://github.com/enkerewpo/OI-Public-Library/blob/master/IOI%E4%B8%AD%E5%9B%BD%E5%9B%BD%E5%AE%B6%E5%80%99%E9%80%89%E9%98%9F%E8%AE%BA%E6%96%871999-2019/%E5%9B%BD%E5%AE%B6%E9%9B%86%E8%AE%AD%E9%98%9F2019%E8%AE%BA%E6%96%87%E9%9B%86.pdf

註:我遞迴、遞推等等用詞都搞不太清楚,不過我猜不重要吧XD
越來越毒的感覺了,該把路矯正回來喇ww
不知道會有多少人仔細看?抓typo不要鞭太用力QQ