0x33_同余

0x33同 余

定义

若整数 aa 和整数 bb 除以正整数 mm 的余数相等,则称 a,ba, bmm 同余,记为 ab(modm)a \equiv b \pmod{m}

同余类与剩余系

对于 a[0,m1]\forall a\in [0,m - 1] ,集合 {a+km}(kZ)\{a + km\} (k\in \mathbb{Z}) 的所有数模 mm 同余,余数都是 aa 。该集合称为一个模 mm 的同余类,简记为 a\overline{a}

mm 的同余类共有 m1m - 1 个,分别为 0,1,2,,m1\overline{0},\overline{1},\overline{2},\dots ,\overline{m - 1} 。它们构成 mm 的完全剩余系。

1m1 \sim m 中与 mm 互质的数代表的同余类共有 φ(m)\varphi(m) 个,它们构成 mm 的简化剩余系。例如,模 10 的简化剩余系为 {1,3,7,9}\{\overline{1}, \overline{3}, \overline{7}, \overline{9}\}

简化剩余系关于模 mm 乘法封闭。这是因为若 a,b(1a,bm)a, b (1 \leq a, b \leq m)mm 互质,则 aba * b 也不可能与 mm 含有相同的质因子,即 aba * b 也与 mm 互质。再由余数的定义即

可得到 abmodma * b \bmod m 也与 mm 互质,即 abmodma * b \bmod m 也属于 mm 的简化剩余系。

费马小定理

pp 是质数,则对于任意整数 aa ,有 apa(modp)a^p \equiv a \pmod{p}

欧拉定理

若正整数 a,na, n 互质,则 aφ(n)1(modn)a^{\varphi(n)} \equiv 1 \pmod{n} ,其中 φ(n)\varphi(n) 为欧拉函数。

证明:

nn 的简化剩余系为 {a1,a2,,aφ(n)}\{\overline{a_1},\overline{a_2},\dots ,\overline{a_{\varphi(n)}}\} 。对 ai,aj\forall a_i,a_j ,若 aaiaaj(modn)a*a_{i}\equiv a*a_{j}(\mathrm{mod} n)a(aiaj)0a*(a_i - a_j)\equiv 0 。因为 a,na,n 互质,所以 aiaj0a_{i} - a_{j}\equiv 0 ,即 aiaja_{i}\equiv a_{j} 。故当 aiaja_{i}\neq a_{j} 时, aai,aajaa_{i},aa_{j} 也代表不同的同余类。

又因为简化剩余系关于模 nn 乘法封闭,故 aai\overline{aa_i} 也在简化剩余系集合中。因此,集合 {a1,a2,,aφ(n)}\{\overline{a_1},\overline{a_2},\dots ,\overline{a_{\varphi(n)}}\} 与集合 {aa1,aa2,,aaφ(n)}\{\overline{aa_1},\overline{aa_2},\dots ,\overline{aa_{\varphi(n)}}\} 都能表示 nn 的简化剩余系。综上所述:

aφ(n)a1a2aφ(n)(aa1)(aa2)(aaφ(n))a1a2aφ(n)(modn)a ^ {\varphi (n)} a _ {1} a _ {2} \dots a _ {\varphi (n)} \equiv (a a _ {1}) (a a _ {2}) \dots \left(a a _ {\varphi (n)}\right) \equiv a _ {1} a _ {2} \dots a _ {\varphi (n)} (\mathrm {m o d} n)

因此 aφ(n)1a^{\varphi (n)}\equiv 1 (mod nn )。

pp 是质数时, φ(p)=p1\varphi(p) = p - 1 ,并且只有 pp 的倍数与 pp 不互质。所以,只要 aa 不是 pp 的倍数,就有 ap11(modp)a^{p - 1} \equiv 1 \pmod{p} ,两边同乘 pp 就是费马小定理。若 aapp 的倍数,费马小定理显然成立。

证毕。

欧拉定理的推论

若正整数 a,na, n 互质,则对于任意正整数 bb ,有 ababmodφ(n)(modn)a^b \equiv a^b \mod \varphi(n) \pmod{n}

证明:

b=qφ(n)+rb = q*\varphi (n) + r ,其中 0r<φ(n)0\leq r < \varphi (n) ,即 r=bmodφ(n)r = b\bmod \varphi (n) 。于是:

abaqφ(n)+r(aφ(n))qar1qararabmodφ(n)(modn)a ^ {b} \equiv a ^ {q * \varphi (n) + r} \equiv \left(a ^ {\varphi (n)}\right) ^ {q} * a ^ {r} \equiv 1 ^ {q} * a ^ {r} \equiv a ^ {r} \equiv a ^ {b \mod \varphi (n)} (\mod n)

证毕。

许多计数类的题目要求我们把答案对一个质数 pp 取模后输出。面对 a+b,ab,aba + b, a - b, a * b 这样的算式,可以在计算前先把 a,ba, bpp 取模。面对乘方算式,根据欧拉定理的推论,可以先把底数对 pp 取模、指数对 φ(p)\varphi(p) 取模,再计算乘方。

【例题】The Luckiest Number POJ3696

给定一个正整数 LLL2109L\leq 2*10^{9}

问至少多少个8连在一起组成的正整数是 LL 的倍数?

xx 个8连在一起组成的正整数可写作 8(10x1)/98(10^{x} - 1) / 9 。题目就是让我们求出一个最小的 xx ,满足 L8(10x1)/9L\mid 8(10^{x} - 1) / 9 。设 d=gcd(L,8)d = \gcd (L,8)

L8(10x1)99L8(10x1)9Ld10x110x1(mod9Ld)L \mid \frac {8 (1 0 ^ {x} - 1)}{9} \Leftrightarrow 9 L \mid 8 (1 0 ^ {x} - 1) \Leftrightarrow \frac {9 L}{d} \mid 1 0 ^ {x} - 1 \Leftrightarrow 1 0 ^ {x} \equiv 1 (\mathrm {m o d} \frac {9 L}{d})

引理:

若正整数 a,na, n 互质,则满足 ax1(modn)a^x \equiv 1 \pmod{n} 的最小正整数 x0x_0φ(n)\varphi(n) 的约数。

证明:

反证法。假设满足 ax1(modn)a^x \equiv 1 \pmod{n} 的最小正整数 x0x_0 不能整除 φ(n)\varphi(n)

φ(n)=qx0+r\varphi(n) = qx_0 + r ( 0<r<x00 < r < x_0 )。因为 ax01(modn)a^{x_0} \equiv 1 \pmod{n} ,所以 aqx01(modn)a^{qx_0} \equiv 1 \pmod{n} 。根据欧拉定理,有 aφ(n)1(modn)a^{\varphi(n)} \equiv 1 \pmod{n} ,所以 ar1(modn)a^r \equiv 1 \pmod{n} 。这与 x0x_0 最小矛盾。故假设不成立,原命题成立。

证毕。

根据以上引理,我们只需求出欧拉函数 φ(9L/d)\varphi(9L / d) ,枚举它的所有约数,用快速幂逐一检查是否满足条件即可。时间复杂度为 O(LlogL)O\left(\sqrt{L} \log L\right)

扩展欧几里得算法

Bézout定理

对于任意整数 a,ba, b ,存在一对整数 x,yx, y ,满足 ax+by=gcd(a,b)ax + by = \gcd(a, b)

证明:

在欧几里得算法的最后一步,即 b=0b = 0 时,显然有一对整数 x=1,y=0x = 1, y = 0 ,使得 a1+00=gcd(a,0)a * 1 + 0 * 0 = \gcd(a, 0)

b>0b > 0 ,则 gcd(a,b)=gcd(b,amodb)\gcd (a,b) = \gcd (b,a\bmod b) 。假设存在一对整数 x,yx,y ,满足 bx+(amodb)y=gcd(b,amodb)b*x+(a\bmod b)*y = \gcd (b,a\bmod b) ,因为 bx+(amodb)y=bx+(aba/b)y=ayb(xa/by)bx + (a\bmod b)y = bx + (a - b\lfloor a / b\rfloor)y = ay - b(x - \lfloor a / b\rfloor y) ,所以令 x=y,y=xa/byx' = y, y' = x - \lfloor a / b\rfloor y ,就得到了 ax+by=gcd(a,b)ax' + by' = \gcd (a,b)

对欧几里得算法的递归过程应用数学归纳法,可知Bézout定理成立。

证毕。

Bézout 定理是按欧几里得算法的思路证明的,且上述证明同时给出了整数 xxyy 的计算方法。这种计算方法被称为扩展欧几里得算法。

int exgcd(int a, int b, int &x, int &y) { if (b == 0) { x = 1, y = 0; return a; } }

int d = exgcd(b, a%b, x, y);
int z = x; x = y; y = z - y * (a / b);
return d;
}

定义变量 d,x0,y0d, x_0, y_0 ,调用 d=exgcd(a,b,x0,y0)\mathrm{d} = \mathrm{exgcd}(a, b, x_0, y_0) 。注意在上述代码中, x0,y0x_0, y_0 是以引用的方式传递的。上述程序求出方程 ax+by=gcd(a,b)ax + by = \gcd(a, b) 的一组特解 x0,y0x_0, y_0 ,并返回 a,ba, b 的最大公约数 dd

对于更为一般的方程 ax+by=cax + by = c ,它有解当且仅当 dcd|c 。我们可以先求出 ax+by=dax + by = d 的一组特解 x0,y0x_0, y_0 ,然后令 x0,y0x_0, y_0 同时乘上 c/dc / d ,就得到了 ax+by=cax + by = c 的一组特解 (c/d)x0,(c/d)y0(c / d)x_0, (c / d)y_0

事实上,方程 ax+by=cax + by = c 的通解可以表示为:

x=cdx0+kbd,y=cdy0kad(kZ)x = \frac {c}{d} x _ {0} + k \frac {b}{d}, \quad y = \frac {c}{d} y _ {0} - k \frac {a}{d} \quad (k \in \mathbb {Z})

其中 kk 取遍整数集合, d=gcd(a,b)d = \gcd(a, b)x0,y0x_0, y_0ax+by=gcd(a,b)ax + by = \gcd(a, b) 的一组特解。乘法逆元

若整数 b,mb, m 互质,并且 bab \mid a ,则存在一个整数 xx ,使得 a/bax(modm)a / b \equiv a * x \pmod{m} 。称 xxbb 的模 mm 乘法逆元,记为 b1(modm)b^{-1} \pmod{m}

因为 a/bab1a/bbb1(modm)a / b\equiv a*b^{-1}\equiv a / b*b*b^{-1}(\mathrm{mod} m) ,所以 bb11(modm)b*b^{-1}\equiv 1(\mathrm{mod}m)

如果 mm 是质数(此时我们用符号 pp 代替 mm )并且 b<pb < p ,根据费马小定理, bp11(modp)b^{p - 1} \equiv 1 \pmod{p} ,即 bbp21(modp)b * b^{p - 2} \equiv 1 \pmod{p} 。因此,当模数 pp 为质数时, bp2b^{p - 2} 即为 bb 的乘法逆元。

如果只是保证 b,mb, m 互质,那么乘法逆元可通过求解同余方程 bx1(modm)b * x \equiv 1 \pmod{m} 得到。下个部分我们就来介绍线性同余方程及其求解方法。

有了乘法逆元,我们在计数类问题中即使遇到 a/ba / b 这样的除法算式,也可以先把 a,ba, b 各自对模数 pp 取模,再计算 ab1modpa * b^{-1} \bmod p 作为最终的结果。当然,前提是必须保证 b,pb, p 互质(当 pp 是质数时,等价于 bb 不是 pp 的倍数)。

到此为止,我们在模 pp 运算下对加、减、乘、除、乘方运算都已经有了适当的处理方法,请读者回顾并加以思考。

【例题】Sumdiv POJ1845

ABA^B 的所有约数之和 mod 9901 (1A,B5107)(1 \leq A, B \leq 5 * 10^7)

0×030 \times 03 节中,我们已经用递归和分治算法解决了取模乘法下等比数列的求和问题。在本题中我们将直接借助乘法逆元计算等比数列的求和公式。

AA 分解质因数,表示为 p1c1p2c2pncnp_1^{c_1} * p_2^{c_2} * \dots * p_n^{c_n} ,由算术基本定理的“约数和”推论, ABA^B 的所有约数之和为: (1+p1+p12++p1Bc1)(1+p2+p22++p2Bc2)(1+pn+pn2++pnBcn)\left(1 + p_1 + p_1^2 + \dots + p_1^{B*c_1}\right) * \left(1 + p_2 + p_2^2 + \dots + p_2^{B*c_2}\right) * \dots * \left(1 + p_n + p_n^2 + \dots + p_n^{B*c_n}\right)

上式的每一项都是一个等比数列。以第一项为例, 1+p1+p12++p1Bc1=(p1Bc1+11)/(p11)1 + p_1 + p_1^2 + \dots + p_1^{B*c_1} = (p_1^{B*c_1 + 1} - 1) / (p_1 - 1) 。可以先用快速幂计算分子 (p1Bc1+11)mod9901\left(p_1^{B*c_1 + 1} - 1\right) \mod 9901 和分母 (p11)mod9901(p_1 - 1) \mod 9901 。因为9901是质数,只要 p11p_1 - 1 不是9901的倍数,就只需计算 p11p_1 - 1 的乘法逆元 invinv ,用乘 invinv 代替除以 (p11)(p_1 - 1) ,直接计算出等比数列求和公式的结果。

特别地,若 p11p_1 - 1 是9901的倍数,此时乘法逆元不存在,但是 p1mod9901=1p_1 \mod 9901 = 1 ,所以 1+p1+p12++p1Bc11+1+12++1Bc1Bc1+1(mod9901)1 + p_1 + p_1^2 + \dots + p_1^{B*c_1} \equiv 1 + 1 + 1^2 + \dots + 1^{B*c_1} \equiv B*c_1 + 1 \pmod{9901}

int a, b, m, ans = 1, mod = 9901;  
int p[20], c[20];  
void divide(int n) {  
    m = 0;  
    for (int i = 2; i * i <= n; i++) {  
        if (n % i == 0) {  
            p[++m] = i, c[m] = 0;  
        }  
    }  
    if (n > 1)  
        p[++m] = n, c[m] = 1;  
}  
int power(int a, long long b) {  
    int c = 1;  
    for (; b; b >= 1) {  
        if (b & 1) c = (long long)c * a % mod;  
        a = (long long)a * a % mod;  
    }  
    return c;  
}  
int main() {  
    cin >> a >> b;
divide(a); //分解质因数  
for (int i = 1; i <= m; i++) {  
    if ((p[i] - 1) % mod == 0) { //没有逆元时,特判  
        ans = ((long long)b * c[i] + 1) % mod * ans % mod;  
        continue;  
    }  
    int x = power(p[i], (long long)b * c[i] + 1); //分子  
    x = (x - 1 + mod) % mod;  
    int y = p[i] - 1; //分母  
    y = power(y, mod - 2); //根据费马小定理求乘法逆元  
    ans = (long long)ans * x % mod * y % mod;  
}  
cout << ans << endl;

\spadesuit 线性同余方程

给定整数 a,b,ma, b, m ,求一个整数 xx 满足 axb(modm)a * x \equiv b \pmod{m} ,或者给出无解。因为未知数的指数为1,所以我们称之为一次同余方程,也称线性同余方程。

axb(modm)a * x \equiv b \pmod{m} 等价于 axba * x - bmm 的倍数,不妨设为 y-y 倍。于是,该方程可以改写为 ax+my=ba * x + m * y = b

根据Bézout定理及其证明过程,线性同余方程有解当且仅当 gcd(a,m)b\gcd (a,m)|b

在有解时,先用欧几里得算法求出一组整数 x0,y0x_0, y_0 ,满足 ax0+my0=gcd(a,m)a * x_0 + m * y_0 = \gcd(a, m) 。然后 x=x0b/gcd(a,m)x = x_0 * b / \gcd(a, m) 就是原线性同余方程的一个解。

方程的通解则是所有模 m/gcd(a,m)m / \gcd (a,m)xx 同余的整数。

【例题】同余方程 NOIP2012/CODEVS1200

求关于 xx 的同余方程 ax1(modb)a*x \equiv 1 \pmod{b} 的最小正整数解。输入数据保证一定有解。

由上述线性同余方程的知识可得, ax1(modb)a*x\equiv 1(\mathrm{mod}b) 有解当且仅当 gcd(a,b)=1\gcd (a,b) = 1 。方程可改写为 ax+by=1a*x + b*y = 1 ,用欧几里得算法求出一组特解 x0,y0x_0,y_0 ,则 x0x_0 就是原方程的一个解,通解为所有模 bbx0x_0 同余的整数。通过取模操作把解的范围移动到 1b1\sim b 之间,就得到了最小正整数解。

long long a, b, x, y;
long long exgcd(long long a, long long b,

long long &x, long long &y) { if (!b) { x=1, y=0; return a; } long long d = exgcd(b, a%b, x, y); long long z = x; x = y; y = z - y*(a/b); return d; } int main() { cin >> a >> b; exgcd(a, b, x, y); cout << (x % b + b) % b << endl; }

中国剩余定理

m1,m2,,mnm_{1}, m_{2}, \dots, m_{n} 是两两互质的整数, m=i=1nmim = \prod_{i=1}^{n} m_{i}Mi=mmiM_{i} = \frac{m}{m_{i}}tit_{i} 是线性同余方程 Miti1(modmi)M_{i} t_{i} \equiv 1 \pmod{m_{i}} 的一个解。对于任意的 nn 个整数 a1,a2,,ana_{1}, a_{2}, \dots, a_{n} ,方程组

{xa1(modm1)xa2(modm2)xan(modmn)\left\{ \begin{array}{l} x \equiv a _ {1} (\mathrm {m o d} m _ {1}) \\ x \equiv a _ {2} (\mathrm {m o d} m _ {2}) \\ \dots \\ x \equiv a _ {n} (\mathrm {m o d} m _ {n}) \end{array} \right.

有整数解,解为 x=i=1naiMitix = \sum_{i=1}^{n} a_i M_i t_i

证明:

因为 Mi=m/miM_{i} = m / m_{i} 是除 mim_{i} 之外所有模数的倍数,所以 ki,aiMiti\forall k\neq i,a_{i}M_{i}t_{i}\equiv 0 (mod mk)m_{k}) 。又因为 aiMitiaia_{i}M_{i}t_{i}\equiv a_{i} (mod mi)m_{i}) ,所以代入 x=i=1naiMitix = \sum_{i = 1}^{n}a_{i}M_{i}t_{i} ,原方程组成立。

证毕。

中国剩余定理给出了模数两两互质的线性同余方程组的一个特解。方程组的通解可以表示为 x+km(kZ)x + km (k \in \mathbb{Z}) 。有些题目要求我们求出最小的非负整数解,只需把 xxmm 取模,并让 xx 落在 0m10 \sim m - 1 的范围内即可。

另外,即使模数不满足两两互质,我们也有方法判断线性同余方程组是否有解,并求出方程组的解。

【例题】Strange Way to Express Integers POJ2891

给定 2n2n 个正整数 a1,a2,,ana_1, a_2, \dots, a_nm1,m2,,mnm_1, m_2, \dots, m_n ,求一个最小的正整数 xx ,满足 i[1,n]\forall i \in [1, n]xai(modmi)x \equiv a_i \pmod{m_i} ,或者给出无解。

本题中 mim_{i} 不一定两两互质,中国剩余定理不再适用。可以考虑使用数学归纳法,

假设已经求出了前 k1k - 1 个方程构成的方程组的一个解 xx 。记 m=i=1k1mim = \prod_{i=1}^{k-1} m_i ,则 x+im(iZ)x + i * m (i \in \mathbb{Z}) 是前 k1k - 1 个方程的通解。

考虑第 kk 个方程,求出一个整数 tt ,使得 x+tmak(modmk)x + t * m \equiv a_k \pmod{m_k} 。该方程等价于 mtakx(modmk)m * t \equiv a_k - x \pmod{m_k} ,其中 tt 是未知量。这就是一个线性同余方程,可以用扩展欧几里得算法判断是否有解,并求出它的解。若有解,则 x=x+tmx' = x + t * m 就是前 kk 个方程构成的方程组的一个解。

综上所述,我们使用 nn 次扩展欧几里得算法,就求出了整个方程组的解。

\spadesuit 高次同余方程

关于高次同余方程,有 axb(modp)a^x \equiv b (\bmod p)xab(modp)x^a \equiv b (\bmod p) 两类问题。不过后者超出了我们的探讨范围,学有余力的读者可以查阅“原根”“阶”“指标”的相关资料。我们重点来解决前者。

问题:

给定整数 a,b,pa, b, p ,其中 a,pa, p 互质,求一个非负整数 xx ,使得 axb(modp)a^x \equiv b (\bmod p)

Baby Step, Giant Step 算法

因为 a,pa, p 互质,所以可以在模 pp 意义下执行关于 aa 的乘、除法运算。

x=itjx = i * t - j ,其中 t=p,0jt1t = \left\lceil \sqrt{p} \right\rceil, 0 \leq j \leq t - 1 ,则方程变为 aitjb(modp)a^{i * t - j} \equiv b (\bmod p) 。即 (at)ibaj(modp)(a^t)^i \equiv b * a^j (\bmod p)

对于所有的 j[0,t1]j \in [0, t - 1] ,把 bajmodpb * a^j \mod p 插入一个 Hash 表。

枚举 ii 的所有可能取值,即 i[0,t]i \in [0, t] ,计算出 (at)imodp(a^t)^i \bmod p ,在 Hash 表中查找是否存在对应的 jj ,更新答案即可。时间复杂度为 O(p)O(\sqrt{p})

下面的参考程序实现了 BabyStep, GiantStep 算法,计算同余方程 axb(modp)a^x \equiv b (\bmod p) 的最小非负整数解,无解时返回 -1。

int baby_step_giant_step(int a, int b, int p) {
    map<int, int> hash;
    hash.clear();
    b % = p;
    int t = (int)sqrt(p) + 1;
    for (int j = 0; j < t; j++) {
        int val = (long long)b * power(a, j, p) % p; // b*a^j
        hash[val] = j;
    }
    a = power(a, t, p); // a^t
if  $(a == 0)$  return  $b == 0$  ? 1 : -1;  
for (int i = 0; i <= t; i++) {  
    int val = power(a, i, p); // (a^t)^i  
    int j = hash.find(val) == hash.end() ? -1 : hash[val];  
    if (j >= 0 && i * t - j >= 0) return i * t - j;  
}  
return -1;

0×340 \times 34 矩阵乘法

一个 nmn*m 的矩阵可看作一个 nmn*m 的二维数组。矩阵的加法和减法就是把两个矩阵对应位置上的数相加减,即 C=A+Bi[1,n],j[1,m],Ci,j=Ai,j±Bi,jC = A + B \Leftrightarrow \forall i \in [1,n], \forall j \in [1,m], C_{i,j} = A_{i,j} \pm B_{i,j} ,其中 A,B,CA, B, C 都是 nmn*m 矩阵。

矩阵乘法的定义略微复杂一些。设 AAnmn*m 矩阵, BBmpm*p 矩阵,则 C=ABC = A*Bnpn*p 矩阵,并且 i[1,n],j[1,p]\forall i \in [1,n], \forall j \in [1,p]

Ci,j=k=1mAi,kBk,jC _ {i, j} = \sum_ {k = 1} ^ {m} A _ {i, k} * B _ {k, j}

也就是说,参与矩阵乘法运算的第一个矩阵的列数必须等于第二个矩阵的行数,所得到的结果矩阵的行数、列数分别等于第一个矩阵的行数、第二个矩阵的列数。结果矩阵 CCii 行第 jj 列的数,是由 AA 的第 ii 行的 mm 个数与 BB 的第 jj 列的 mm 个数分别相乘再相加得到的。

矩阵乘法满足结合律,即 (AB)C=A(BC)(A*B)*C = A*(B*C) ,满足分配律,即 (A+B)C=AC+BC(A + B)*C = A*C + B*C ,不满足交换律,即 ABA*B 不一定等于 BAB*A

考虑矩阵乘法中一种特殊的情形, FF1n1*n 矩阵, AAnnn*n 矩阵,则 F=FAF' = F*A 也是 1n1*n 矩阵。 FFFF' 可看作一维数组,省略它们的行下标 1。按照矩阵乘法的定义, j[1,n]\forall j \in [1,n] ,有 Fj=k=1nFkAk,jF'_j = \sum_{k=1}^{n} F_k * A_{k,j} 。它的含义是,经过一次与 AA 的矩阵乘法运算后, FF 数组中的第 kk 个值会以 Ak,jA_{k,j} 为系数累加到 FF' 数组的第 jj 个值上,等价于在一个向量的 k,jk,j 两个状态之间发生了递推。

【例题】Fibonacci POJ3070

在斐波那契数列中, Fib0=0,FIB1=1,Fibn=Fibn1+Fibn2(n>1)Fib_{0} = 0, FIB_{1} = 1, Fib_{n} = Fib_{n - 1} + Fib_{n - 2}(n > 1) 。现给定整数 n,mn,m (0n2109,m=10000)(0\leq n\leq 2*10^{9},m = 10000) ,求 FibnmodmFib_{n}\mod m_{\circ}

直接循环递推计算斐波那契数列,时间复杂度显然为 O(n)O(n) 。不过, FibnFib_{n} 只与

Fibn1F i b_{n - 1}Fibn2F i b_{n - 2} 有关,我们在递推时只需要保存最近的两个斐波那契数,即可得到下一个斐波那契数。

F(n)F(n) 表示一个 121*2 的矩阵, F(n)=[FibnFibn+1]F(n) = [Fib_{n} \quad Fib_{n+1}]

我们希望根据 F(n1)=[Fibn1Fibn]F(n - 1) = [Fib_{n - 1} \quad Fib_n] 计算出 F(n)F(n) 。也就是说,要把 F(n1)F(n - 1) 第2列上的数作为 F(n)F(n) 第1列上的数,并把 F(n1)F(n - 1) 第1、2列上的数都累加到 F(n)F(n) 的第2列上。因此,我们令矩阵 AA 第2行第1列、第1行第2列、第2行第2列的数都是1,第1行第1列的数是0。

F(n)=F(n1)A[FibnFibn+1]=[Fibn1Fibn][0111]F (n) = F (n - 1) * A \Leftrightarrow [ F i b _ {n} \quad F i b _ {n + 1} ] = [ F i b _ {n - 1} \quad F i b _ {n} ] * \left[ \begin{array}{l l} 0 & 1 \\ 1 & 1 \end{array} \right]

请读者根据矩阵乘法的定义对上式进行验证。

上式是一个矩阵乘法递推式,初值为 F(0)=[01]F(0) = [0 \quad 1] ,目标为 F(n)=F(0)AnF(n) = F(0) * A^n 。因为矩阵乘法满足结合律,所以我们可以用快速幂计算 F(0)AnF(0) * A^n ,得到的矩阵第1列上的数就是 FibnFib_n 。算法的时间复杂度为 O(23logn)O(2^3 \log n) ,其中 232^3 是矩阵乘法所消耗的时间。

题目还要求我们对 mm 取模,可以直接在执行矩阵乘法时,对各个整数的加法、乘法运算加入取模操作。详细过程请参阅下面的参考程序:

const int mod = 10000;  
int n;  
void mul(int f[2], int a[2][2]) {  
    int c[2];  
    memset(c, 0, sizeof(c));  
    for (int j = 0; j < 2; j++)  
        for (int k = 0; k < 2; k++)  
            c[j] = (c[j] + (long long)f[k] * a[k][j]) % mod;  
        memcpy(f, c, sizeof(c));  
}  
void mulself(int a[2][2]) {  
    int c[2][2];  
    memset(c, 0, sizeof(c));  
    for (int i = 0; i < 2; i++)  
        for (int j = 0; j < 2; j++)  
            for (int k = 0; k < 2; k++)  
                c[i][j] = (c[i][j] + (long long)a[i][k]*a[k][j])%mod;  
       memcpy(a, c, sizeof(c));  
}  
int main() {
while (cin >> n && n != -1) {  
    int f[2] = {0,1};  
    int a[2][2] = {{0,1}, {1,1}};  
    for (; n; n >= 1) {  
        if (n & 1) mul(f, a);  
            mulself(a);  
    }  
    cout << f[0] << endl;  
}

这道题使我们对“矩阵乘法加速递推”有了初步的认识。一般来说,如果一类问题具有以下特点:

  1. 可以抽象出一个长度为 nn 的一维向量,该向量在每个单位时间发生一次变化。
    2.变化的形式是一个线性递推(只有若干“加法”或“乘一个系数”的运算)。

  2. 该递推式在每个时间可能作用于不同的数据上,但本身保持不变。

  3. 向量变化时间(即递推的轮数)很长,但向量长度 nn 不大。

那么可以考虑采用矩阵乘法进行优化。我们把这个长度为 nn 的一维向量称为“状态矩阵”,把用于与“状态矩阵”相乘的固定不变的矩阵 AA 称为“转移矩阵”。若状态矩阵中的第 xx 个数对下一单位时间状态矩阵中的第 yy 个数产生影响,则把转移矩阵的第 xx 行第 yy 列赋值为适当的系数。

矩阵乘法加速递推的关键在于定义出“状态矩阵”,并根据递推式构造出正确的“转移矩阵”,之后就可以利用快速幂和矩阵乘法完成程序实现了。时间复杂度为 O(n3logT)O(n^{3}\log T) ,其中 TT 为递推总轮数。

【例题】石头游戏 BZOJ2973

石头游戏在一个 nnmm(1n,m8)(1 \leq n, m \leq 8) 的网格上进行,每个格子对应一种操作序列,操作序列至多有10种,分别用 090 \sim 9 这10个数字指明。

操作序列是一个长度不超过6且循环执行、每秒执行一个字符的字符串。每秒钟,所有格子同时执行各自操作序列里的下一个字符。序列中的每个字符是以下格式之一:

  1. 数字 090 \sim 9 :表示拿 090 \sim 9 个石头到该格子。

  2. NWSE:表示把这个格子内所有的石头推到相邻的格子,N表示上方,W表示左方,S表示下方,E表示右方。
    3.D:表示拿走这个格子的所有石头。

给定每种操作序列对应的字符串,以及网格中每个格子对应的操作序列,求石头游戏进行了 tt 秒之后,石头最多的格子里有多少个石头。在游戏开始时,网格是空的。

i[1,n],j[1,m]\forall i\in [1,n],j\in [1,m]num(i,j)=(i1)m+j\mathrm{num}(i,j) = (i - 1)*m + j

把网格看作长度为 nmn * m 的一维向量,定义1行 nm+1n * m + 1 列的“状态矩阵” FF 下标为 0nm0 \sim n * m ,其中 F[num(i,j)]F[\mathrm{num}(i,j)] 记录格子 (i,j)(i,j) 中石头的个数。

特别地,令 F[0]F[0] 始终等于1。

FF 会随着时间的增长而不断变化。设 FkF_{k} 表示 kk 秒之后的“状态矩阵”。在游戏开始时,根据题意和 FF 的定义,有 F0=[1000]F_{0} = \begin{bmatrix} 1 & 0 & 0 & \dots & 0 \end{bmatrix}

注意到操作序列的长度不超过6,而 161\sim 6 的最小公倍数是60,所以每经过60秒,所有操作序列都会重新处于最开始的字符处。我们可以统计出第 kk(1k60)(1\leq k\leq 60) 各个格子执行了什么字符,第 k+60k + 60 秒执行的字符与第 kk 秒一定是相同的。

对于 1601\sim 60 之间的每个 kk ,各个格子在第 kk 秒执行的操作字符可以构成一个“转移矩阵” AkA_{k} ,矩阵行、列下标都是 0nm0\sim n*m 。构造方法如下:

  1. 若网格 (i,j)(i,j)kk 秒的操作字符为“N”,且 i>1i > 1 ,则令 Ak[num(i,j),num(i1,j)]=1A_{k}[\mathrm{num}(i,j),\mathrm{num}(i - 1,j)] = 1 ,表示把石头推到上边的格子。字符“W”“S”“E”类似。

  2. 若网格 (i,j)(i,j)kk 秒的操作字符是一个数字 xx ,则令 Ak[0,num(i,j)]=xA_{k}[0, \mathrm{num}(i,j)] = x

  3. Ak[0,0]=1A_{k}[0,0] = 1

  4. AkA_{k} 的其他部分均赋值为0。

上述构造实际上把“状态矩阵”的第0列作为“石头来源”。 Ak[0,0]=1A_{k}[0,0] = 1 保证了 F[0]F[0] 始终等于1。 Ak[0,num(i,j)]=xA_{k}[0,\mathrm{num}(i,j)] = x 相当于从“石头来源”获取 xx 个石头,放到格子 (i,j)(i,j) 中。使用矩阵乘法加速递推,遇到常数项时,经常需要在“状态矩阵”中添加一个额外的位置,始终存储常数1,并乘上“转移矩阵”中适当的系数,累加到“状态矩阵”的其他位置。

A=i=160AiA = \prod_{i=1}^{60} A_i , t=q60+rt = q * 60 + r ( 0r<600 \leq r < 60 )。根据上面的讨论:

Ft=F0Aqi=1rAiF _ {t} = F _ {0} * A ^ {q} * \prod_ {i = 1} ^ {r} A _ {i}

使用矩阵乘法和快速幂计算该式, FtF_{t}1nm1\sim n*m 列中的最大值即为所求。