(公式可能渲染得比較慢,這時候,你只需要,放在那邊加載一會就好了qwq)
1 定義和通項
1.1 定義
遞歸定義斐波那契數列為
特別地,\(F_0=0,F_1=1\)。
1.2 通項公式
令 \(\phi=\frac{1+\sqrt 5}2\),\(\bar\phi=\frac{1-\sqrt 5}2\),那么
考慮利用通項公式計算斐波那契數列的第 \(n\) 項模 \(p\) 意義下的值。
當 \(5\) 是模 \(p\) 的二次剩余時,我們可以考慮直接用 Cipolla's algorithm 算出 \(5\) 的二次剩余;否則我們可以考慮用擴展數域來計算,即因為 \(\sqrt5 \not \in \mathbb F_p\),所以我們類似定義復數,定義擴展數域 \(\mathbb F_p(\sqrt 5)\) 為
這種定義方法在下面研究模 \(p\) 意義下的循環節也是有涉及到的。
擴展數域的加減乘除是很顯然的(用復數類比即可),相對於矩陣乘法,用擴展數域計算的優越性就是常數小。
1.3 通項公式的推導
1.3.1 生成函數法
設生成函數
由遞推公式可以得到
解得
為了將它表示為級數形式,我們考慮利用一個常見的生成函數來進行轉化
假設我們可以將 \(\mathcal F(x)\) 分解為下面的形式,那么利用就可以很容易得得出級數表示,進而直接得出通項
由 \((1)\) 可知轉化成這種形式應當是可行的,具體地,我們可以得到下面的方程組
不難得到其中一組解:
因此將這些量代入 \((3)\),可以得到
利用 \((2)\) 可以得到
即
1.3.2 歸納法
直接代入可以得到
直接代入還可以得到
假設對於 \(0 \leq i \leq n\),都有 \(F_i=\frac 1{\sqrt 5}(\phi^i-\bar\phi^i)\),那么考慮對於 \(n+1\) 就有
2 模意義下的循環節
本部分參考資料:https://www.math.arizona.edu/~ura-reports/071/Campbell.Charles/Final.pdf
我們可以發現,在模意義下,斐波那契的第 \(n+1\) 項 \(F_{n+1}\) 僅取決於 \((F_{n-1}\bmod p,F_{n}\bmod p)\),不難發現這個二元組有 \(p^2\) 種取值。因此,在模意義下,斐波那契數列一定會最終產生循環,即當 \(n\) 足夠大時,一定會出現 \(F_{n}=F_{n-k}\) 的情況。
並且根據遞推式,我們有 \(F_n=F_{n+2}-F_{n+1}\),也就是說,\(F_{n+1}\) 和 \(F_{n+2}\) 的前面一項一定是 \(F_n\)。
通過這兩個事實,我們可以知道,斐波那契數列在模意義下一定會產生循環節,並且一定是純循環的,即循環節的開始一定是 \(F_0\) 和 \(F_1\)。因此,我們就可以定義斐波那契數列模意義下的最小循環節,並探究有關性質。
2.1 定義
定義斐波那契數列模 \(p\) 意義下的最小循環節 \(\pi(p)\) 為
同時不難得到
\(\pi(p)\) 又被稱為皮薩諾周期。
2.2 一些定理
\(\textbf{Theorem 1.}\) 對於質數 \(p\) 和一個正整數 \(a\),若 \(a \equiv 1\pmod p\),則對於任意自然數 \(n\),都有
證明:考慮歸納法證明,這個命題顯然對 \(n=0\) 成立。假設對 \(n\) 有 \(a^{p^n}\equiv 1 \pmod{p^{n+1}}\) 成立,那么我們證明對 \(n+1\) 也有 \(a^{p^{n+1}}\equiv 1 \pmod{p^{n+2}}\) 成立。
因為 \(\binom{p}{i}=\frac{p!}{i!(p-i)!}\),並且因為 \(p\) 是質數,所以 \(p|\binom{p}{i} (1<i<p)\)。所以
因此
證畢。
\(\textbf{Corollary 1.}\) 對於一個質數 \(p\),若 \(m\) 為斐波那契數列模 \(p\) 意義下的一個周期,那么對於任意正整數 \(k\),都有
證明:由於
所以
又因為
所以
故
結合 \(\text{Theorem 1}\),我們可以得到
證畢。
之所以要證明 \(\text{Corollary 1}\),是為了將模數為 \(p^k(p \text{ is a prime})\) 的情況轉化為模數為 \(p\) 的情況,便於處理。
具體地,我們有下面這個定理。
\(\textbf{Theorem 2.}\) 對於質數 \(p\),滿足對於任意正整數 \(k\),都有 \(\pi(p^k)|p^{k-1}\cdot\pi(p)\)。
證明:令 \(m=\pi(p)\),那么容易得到
以及
證畢。
\(\textbf{Theorem 3.}\) 對於任意正整數 \(n\),考慮 \(n\) 的唯一分解形式 \(n=\prod_{i=1}^s p_i^{k_i}\),那么有
同時
證明:設 \(m=\pi(n)\),\(m_i=\pi(p_i^{k_i})\),那么
又由中國剩余定理,可以知道
又因為
由 \((4)\) 可知式 \((5)\) 成立當且僅當 \(\forall i,m_i|m\),那么滿足條件的最小的 \(m\) 即為 \(\mathrm{lcm}(m_1,m_2,\cdots,m_s)\)。
證畢。
2.3 素數情形
前置知識:
通過 \(\text{Theorem 3}\),我們可以將一般的模數轉化為素數來處理,現在就只需要考慮素數的情況。
一個不可避免的問題是,\(\sqrt 5\) 在某些模數下可能是沒有意義的,我們需要進行一些討論。
在下面的討論中,我們默認 \(p \neq 2, p \neq 5\),並且 \(p\) 是素數。這兩個數的情況特殊討論。
在數論中,若存在整數 \(x\) 滿足 \(x^2\equiv a\pmod p\),則稱 \(a\) 是模 \(p\) 的二次剩余,否則是二次非剩余。在本文中,我們只關心 \(5\) 是否是 \(p\) 的二次剩余,這涉及到 \(\sqrt 5\) 是否在模 \(p\) 下有意義。
利用二次互反律,我們可以知道
並且
因此 \(5\) 是模 \(p\) 的二次剩余當且僅當 \(p\equiv \pm 1\pmod 5\),\(5\) 是模 \(p\) 的二次非剩余當且僅當 \(p\equiv \pm 2\pmod 5\)。
下面我們就分這兩種情況,給出兩個定理,解決斐波那契數列模質數意義下的循環節問題。
\(\textbf{Theorem 4.}\) 若 \(p\) 為奇素數並且 \(p\equiv \pm 1\pmod 5\),那么 \(\pi(p)|m-1\)。
證明:因為 \(p\equiv \pm 1\pmod 5\),所以 \(5\) 是模 \(p\) 的二次剩余,因此 \(\phi,\bar\phi\in \mathbb F_p\)。應用費馬小定理,我們可以得到
所以
根據 \((4)\) 我們得到 \(\pi(p)|m-1\)。證畢。
\(\textbf{Theorem 5.}\) 若 \(p\) 為奇素數並且 \(p\equiv \pm 2\pmod 5\),那么 \(\pi(p)|2m+2\),並且 \(\frac{2m+2}{\pi(p)}\) 是奇數。
證明:因為 \(p\equiv \pm 2\pmod 5\),所以 \(5\) 是模 \(p\) 的非二次剩余,因此 \(\phi,\bar\phi\not \in \mathbb F_p\),為了方便,我們在一個擴展的數域計算,使得 \(\phi,\bar\phi\) 有意義。具體地,我們有如下擴展的數域
因為 \(5^{\frac{p-1}2}\equiv -1\pmod p\),所以在模意義下
同理 \(\bar\phi^p=\phi\)。
因此我們有
因此由 \((4)\) 可知 \(\pi(p)\nmid p+1\)。並且我們有
因此由 \((4)\) 可知 \(\pi(p)|2p+2\),並且因為 \(\pi(p)\nmid p+1\),所以 \(\frac{2m+2}{\pi(p)}\) 是奇數。證畢。
2.4 一般情形
綜合上述定理,對於任意模數 \(n(n>1)\),考慮 \(n\) 的唯一分解形式 \(n=\prod_{i=1}^s p_i^{k_i}\)。那么斐波那契數列模 \(n\) 意義下的最小循環節
其中對於素數 \(p\)
其中 \(2\) 和 \(5\) 的情況比較特殊,是另外討論的結果。
如果要求最小周期,可以先算出 \(\mathrm{lcm}\left(g(p_1)p_1^{k_1-1},g(p_2)p_2^{k_2-1},\cdots,g(p_s)p_s^{k_s-1}\right)\),這個復雜度是質因數分解的復雜度。然后再枚舉約數就可以知道最小周期了,驗證可以通過矩陣乘法。
在一般應用中,通常只需要求出一個循環節,\(\mathrm{lcm}\left(g(p_1)p_1^{k_1-1},g(p_2)p_2^{k_2-1},\cdots,g(p_s)p_s^{k_s-1}\right)\) 這個值在多數情況下已經夠用了。
2.5 循環節的上界
具體證明請見:Pisano_period
只考慮 \(\mathrm{lcm}\left(g(p_1)p_1^{k_1-1},g(p_2)p_2^{k_2-1},\cdots,g(p_s)p_s^{k_s-1}\right)\) 的上界,我們發現,上界主要取決於 \(g(p)\),因此我們觀察 \(g(p)\)。發現這個循環節變大的關鍵在於 \(2\) 和 \(5\),出現質因子 \(2\) 帶來的貢獻是 \(\times \frac{3}{2}\),出現質因子 \(5\) 的貢獻是 \(\times 4\)。
因此,我們發現形如 \(n=2\times 5^r(r \in \mathbb N^*)\) 的循環節均為 \(6n\),並且可以證明
具體證明在此不展開,根據 \(g(p)\) 的式子分類討論也可得出這個結論。
2.6 求循環節的一些其他方法
2.6.1 矩陣 BSGS
這種做法是比較常見的,應用也十分廣泛,但是前提是確定了循環節的上界,以及轉移矩陣可逆才有辦法使用。
考慮矩陣乘法求斐波那契數列第 \(n\) 項的過程,用矩陣表示可以寫成
記 \(T=\begin{pmatrix}1 & 1\\1 & 0\\\end{pmatrix}\),那么求循環節相當於求模意義下,滿足 \(T^m\equiv I \pmod p\) 的正整數 \(m\)。
套用 \(\mathrm{BSGS}\) 算法的流程,在轉移矩陣 \(T\) 可逆的前提下,我們設 \(m=iS-j(i\geq 1,0<j \leq S)\),於是方程可以寫成
即
假設確定的循環節上界為 \(q\),那么取 \(S=\sqrt q\),預處理其中一邊,存到哈希表內,然后查詢即可。時間復雜度 \(\mathcal O(\sqrt q)\)。
2.6.2 隨機化
以斐波那契數列為例,同樣我們需要知道循環節的上界才有辦法處理。因為 \(\pi(p) \leq 6p\),可以認為是 \(\mathcal O(p)\) 的。
如果每次隨機兩個數 \(0 \leq i,j \leq 6p\),然后判斷是否有 \((F_i,F_{i+1})\equiv (F_j,F_{j+1})\pmod p\),如果有的話,那么循環節就為 \(\lvert i-j\rvert\) 的約數。但是這樣做的期望次數是 \(\mathcal O(p)\) 的,沒有什么優勢。
我們考慮每次隨機兩個的概率太小,但是假設我們不斷隨機,每次隨機三個、四個……概率就比較大了。更形式化地,假設我們在一個 \(n\) 個元素的集合內每次隨機選取一個元素,一共隨機 \(m\) 次,存在兩次選取的元素相同的概率隨 \(m\) 的增加是呈平方級別增加的,具體的分析可以看 Birthday Problem——Wikipedia。
如果我們改變一下算法流程,每次新隨機一個數 \(x\),將 \((F_x\bmod p,F_{x+1}\bmod p)\) 的信息存到哈希表中,並查詢哈希表內之前有沒有相同的 \((F_y\bmod p,F_{y+1}\bmod p)\)。可以證明,這樣做的期望次數是 \(\mathcal O(\sqrt p)\) 的。
實際情況需要取上界為 \(12p\) 來保證期望次數。代碼可以看參考博文。
2.7 一些應用
2.7.1 Luogu4000 斐波那契數列
求 \(F_n\bmod p\),其中 \(F_n\) 是斐波那契數列的第 \(n\) 項。
\(1 \leq p<2^{31}\),\(n \leq 10^{3\cdot 10^7}\)
時空限制:\(\texttt{1s/512MB}\)
直接用 \(\mathcal O(\log n)\) 級別的做法都是不可行的,一般的解決方法就是找到循環節,然后計算 \(n\) 在模循環節長度意義下的值,時間復雜度就可以優化到 \(\mathcal O(\log p)\) 了:
- 應用前面推導的結論,直接質因數分解計算模 \(p\) 意義下的循環節。
- 矩陣 BSGS(可能會 TLE)
- 隨機化計算循環節
2.7.2 FJWC2020 Day4T1
來源:FJWC2020 Day4T1
定義 \(g_0=a,g_1=b\),\(g_n=3g_{n-1}-g_{n-2}(n\geq 2)\)。
定義 \(f_{n,0}=n\),\(f_{n,k}=f_{g_n,k-1}\)。
給定 \(a,b,n,k,p\),請你求出 \(f_{n,k}\bmod p\)。
\(n,p\leq 10^9\),\(k \leq 100\),數據組數 \(T \leq 1000\)。
時空限制:\(\texttt{1s/512MB}\)
可以觀察到 \(g_n=F_{2n}b-F_{2(n-1)}a\),其中 \(F\) 是斐波那契數列,特別地,我們定義 \(F_{-1}=1,F_{-2}=-1\)。
顯然 \(g_n\) 的增長速度極快,要求 \(g_{g_{g_{\dots g_n}}}\)(共 \(k\) 個 \(g\)),顯然不能用 \(g_n\) 的真實值計算。可以考慮用計算循環節的方式來處理。在每一層都計算一下當前模數的循環節,那么下面一層的模數就用計算的循環節來代替。
矩陣 BSGS 的時間復雜度無法承受,考慮利用前面推出的結論
可以證明,不斷迭代下去,循環節的大小不會很大,具體地,我們考慮當前循環節長度中質因子 \(2,3,5\) 的次數,若它們的次數都足夠,那么根據
我們發現循環節就不會再增長了。
於是每一層計算一次循環節,然后遞歸下去即可。
#include <bits/stdc++.h>
template <class T>
inline void read(T &x)
{
static char ch;
while (!isdigit(ch = getchar()));
x = ch - '0';
while (isdigit(ch = getchar()))
x = x * 10 + ch - '0';
}
template <class T>
inline void putint(T x)
{
static char buf[25], *tail = buf;
if (!x)
putchar('0');
else
{
for (; x; x /= 10) *++tail = x % 10 + '0';
for (; tail != buf; --tail) putchar(*tail);
}
}
typedef long long s64;
s64 mod;
int a, b, n, K, p;
inline s64 plus(s64 x, s64 y)
{
x += y;
return x >= mod ? x - mod : x;
}
typedef long double ld;
inline s64 qmul(s64 a, s64 b)
{
s64 res = a * b - (s64)((ld)a / mod * b + 1e-8) * mod;
return res < 0 ? res + mod : res;
}
struct mat
{
int r, c;
s64 a[2][2];
mat(){}
mat(int _r, int _c):
r(_r), c(_c) {memset(a, 0, sizeof(a));}
inline mat operator * (const mat &rhs) const
{
mat res(r, rhs.c);
for (int i = 0; i < r; ++i)
for (int k = 0; k < c; ++k)
for (int j = 0; j < rhs.c; ++j)
res.a[i][j] = plus(res.a[i][j], qmul(a[i][k], rhs.a[k][j]));
return res;
}
inline mat operator ^ (s64 p) const
{
mat res(r, c), x = *this;
res.init();
for (; p; p >>= 1, x = x * x)
if (p & 1)
res = res * x;
return res;
}
inline void init()
{
for (int i = 0; i < r; ++i)
a[i][i] = 1;
}
}T(2, 2), F0(1, 2);
inline s64 calc_G(s64 n, s64 p)
{
if (n == 0)
return a % p;
else if (n == 1)
return b % p;
mod = p;
T.a[0][0] = 3 % mod, T.a[0][1] = 1 % mod;
T.a[1][0] = mod - 1, T.a[1][1] = 0;
F0.a[0][0] = b % mod, F0.a[0][1] = a % mod;
return (F0 * (T ^ (n - 1))).a[0][0];
}
const int MaxN = 1e6 + 5;
int pri[MaxN], n_pri;
inline s64 lcm(s64 a, s64 b)
{
return a / std::__gcd(a, b) * b;
}
inline void sieve_init(int n = 1000000)
{
static bool sie[MaxN];
for (int i = 2; i <= n; ++i)
{
if (!sie[i])
pri[++n_pri] = i;
for (int j = 1; j <= n_pri && pri[j] * i <= n; ++j)
{
sie[i * pri[j]] = true;
if (i % pri[j] == 0)
break;
}
}
}
inline s64 getp(s64 p)
{
if (p == 2)
return 3;
else if (p == 3)
return 8;
else if (p == 5)
return 20;
else if (p % 5 == 1 || p % 5 == 4)
return p - 1;
else
return 2 * p + 2;
}
inline s64 find_period(s64 p)
{
s64 x = p;
s64 res = 1;
for (int i = 1; x > 1 && i <= n_pri && 1LL * pri[i] * pri[i] <= p && pri[i] <= x; ++i)
if (x % pri[i] == 0)
{
x /= pri[i];
s64 cur = getp(pri[i]);
while (x % pri[i] == 0)
x /= pri[i], cur *= pri[i];
res = lcm(res, cur);
}
if (x > 1)
res = lcm(res, getp(x));
return res;
}
inline s64 calc(s64 n, int k, s64 p)
{
if (k == 0)
return n % p;
return calc_G(calc(n, k - 1, find_period(p)), p);
}
int main()
{
sieve_init();
int orzcx;
read(orzcx);
while (orzcx--)
{
read(a), read(b), read(n), read(K), read(p);
if (p == 1)
{
puts("0");
continue;
}
putint(calc(n, K, p));
putchar('\n');
}
return 0;
}
3 一些性質
3.1 卡西尼恆等式
有一個比較簡潔的證明:
3.2 附加性質
不妨設 \(F_n=a\),\(F_{n+1}=b\),那么可以歸納證明 \(F_{n+m}=F_{m-1}a+F_{m}b\),原式得證。
注意到將 \(m=n\) 代入原式可以得到另外一個美妙的性質
廣義斐波那契數列
注意到我們可以擴展斐波那契數列,對於 \(F_n(n<0)\),我們可以將遞推公式反向 \(F_n=F_{n+2}-F_{n+1}\)。因此該性質沒有限制 \(n,m\) 的正負性。我們稱這種數列叫廣義斐波那契數列。
並且有一個很有意思的性質
由歸納法很容易可以證明,這里就不展開。
類斐波那契數列
這個附加性質啟發我們定義類斐波那契數列,對於一個數列 \(G\),若 \(G_0=a,G_1=b\),並且數列滿足遞推關系式
則稱 \(G\) 是類斐波那契數列,並且有
證明可以考慮歸納法。類斐波那契數列也有部分斐波那契數列的性質,具體情況可以具體分析。
3.3 求和性質
3.3.1 直接求和
首先我們有個前綴和公式
證明方法:\(\sum_{i=1}^nF_i=\sum_{i=1}^n(F_{i+2}-F_{i+1})=F_{n+2}-1\)。
還有一些類似的性質,比如偶數項求和、奇數項求和
證明也是寫成差的形式。
3.3.2 平方和
有一個非常巧妙的幾何證明,可以看下面這張圖:

當然也可以歸納:
-
顯然對於 \(n=1\) 結論成立。
-
假設有 \(\sum_{i=1}^{n-1}F_i^2=F_{n-1}F_n\) 成立,那么
\[\sum_{i=1}^nF_i^2=F_{n-1}F_n+F_n^2=F_nF_{n+1} \]
3.4 和數論有關的性質
3.4.1 相鄰項互質
證明也可以歸納:
-
顯然對於 \(n=1\) 結論成立。
-
假設有 \(\mathrm{gcd}(F_{n-1},F_n)=1\) 成立,那么
\[\mathrm{gcd}(F_n,F_{n+1})=\mathrm{gcd}(F_n,F_{n+1}-F_n)=\mathrm{gcd}(F_n,F_{n-1})=1 \]
3.4.2 最大公約數性質
這個性質的證明需要用到輾轉相除法的一些性質:
-
不妨假設 \(n<m\),由 \((7)\) 可以推出
\[\begin{aligned} \mathrm{gcd}(F_n,F_m)&=\mathrm{gcd}(F_n,F_{n+(m-n)})\\ &=\mathrm{gcd}(F_n,F_{m-n-1}F_n+F_{m-n}F_{n+1})\\ &=\mathrm{gcd}(F_n,F_{m-n}F_{n+1}) \end{aligned} \] -
由 \((11)\) 可知 \(\mathrm{gcd}(F_n,F_{n+1})=1\),所以
\[\mathrm{gcd}(F_n,F_m)=\mathrm{gcd}(F_n,F_{m-n})(m>n) \tag{12} \] -
根據 \((12)\) 遞歸下去就是輾轉相減法,最終會得到的形式應當就是
\[\mathrm{gcd}(F_n,F_m)=F_{\mathrm{gcd}(n,m)} \]
這個性質還可以導出其他性質:
證明只要套用 \((11)\) 即可。
3.5 其他性質
代入 \(F_{n+1}=F_{n}+F_{n-1}\) 和 \(F_{n-2}=F_{n}-F_{n-1}\) 可以輕松驗證。這個性質在某些題目中會有些用。
3.6 應用
3.6.1 一道樹鏈剖分題
給定一棵 \(n\) 個結點的有根樹,每個點有點權,初始值為 \(0\),接下來有 \(m\) 次操作,每次操作為下面兩種之一:
- 給定 \(x,k\),將屬於 \(x\) 子樹的每個點 \(y\) 的點權加上 \(F_{k+dis(x,y)}\),其中 \(F\) 是斐波那契數列。
- 給定 \(x,y\),詢問路徑 \((x,y)\) 上的點權和 \(\bmod 10^9+7\)。
\(n,m \leq 10^5,0 \leq k\leq 10^{18}\)。
時空限制:\(\texttt{1s/512MB}\)。
根據 \((7)\) 可以得到
用樹鏈剖分+線段樹打標記解決,注意 \(k-\mathrm{dep}_x\) 可能是負數,根據廣義斐波那契數列的定義,預處理這個范圍的 \(F_x\) 即可。
可以處理出樹鏈剖分完每個前綴的 \(\sum_{i=1}^xF_{\mathrm{dep}_x-1}\) 和 \(\sum_{i=1}^xF_{\mathrm{dep}_x}\),便於打標記。
時間復雜度為 \(\mathcal O(n\log^2n)\)。
#include <bits/stdc++.h>
typedef long long s64;
template <class T>
inline void read(T &x)
{
static char ch;
static bool opt;
while (!isdigit(ch = getchar()) && ch != '-');
x = (opt = ch == '-') ? 0 : ch - '0';
while (isdigit(ch = getchar()))
x = x * 10 + ch - '0';
if (opt) x = ~x + 1;
}
inline bool getopt()
{
static char ch;
while ((ch = getchar()) != 'U' && ch != 'Q');
return ch == 'U';
}
const int MaxNV = 1e5 + 5;
const int MaxNE = MaxNV << 1;
const int MaxNode = MaxNV << 2;
const int mod = 1e9 + 7;
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
if (x < 0)
x += mod;
}
struct matrix
{
int r, c;
int a[3][3];
matrix(){}
matrix(const int &x, const int &y):
r(x), c(y)
{
memset(a, 0, sizeof(a));
}
inline void clear()
{
memset(a, 0, sizeof(a));
}
inline void init()
{
memset(a, 0, sizeof(a));
for (int i = 1; i <= r; ++i)
a[i][i] = 1;
}
inline matrix operator * (const matrix &rhs) const
{
matrix res(r, rhs.c);
for (int i = 1; i <= r; ++i)
for (int j = 1; j <= rhs.c; ++j)
for (int k = 1; k <= c; ++k)
add(res.a[i][j], 1LL * a[i][k] * rhs.a[k][j] % mod);
return res;
}
inline matrix operator ^ (s64 p) const
{
matrix x = *this, res(r, c);
res.init();
for (; p; p >>= 1, x = x * x)
if (p & 1)
res = res * x;
return res;
}
}A(1, 2), T(2, 2);
int ect, adj[MaxNV], to[MaxNE], nxt[MaxNE];
int n, m, f_neg[MaxNV], f_pos[MaxNV], dis1[MaxNV], dis2[MaxNV];
int fa[MaxNV], son[MaxNV], sze[MaxNV], dep[MaxNV], pos[MaxNV], idx[MaxNV], top[MaxNV], totpos;
int tag1[MaxNode], tag2[MaxNode];
int sum1[MaxNode], sum2[MaxNode];
#define lc (x << 1)
#define rc (x << 1 | 1)
#define trav(u) for (int e = adj[u], v; v = to[e], e; e = nxt[e])
inline void addEdge(const int &u, const int &v)
{
nxt[++ect] = adj[u], adj[u] = ect, to[ect] = v;
}
inline int getfib(const s64 &x)
{
if (x <= 0)
return f_neg[-x];
if (x < MaxNV)
return f_pos[x];
return (A * (T ^ (x - 1))).a[1][1];
}
inline void dfs1(const int &u)
{
dep[u] = dep[fa[u]] + 1;
sze[u] = 1;
// son[u] = top[u] = 0;
trav(u) if (v != fa[u])
{
fa[v] = u;
dfs1(v);
sze[u] += sze[v];
if (sze[v] > sze[son[u]])
son[u] = v;
}
}
inline void dfs2(const int &u)
{
if (son[u])
{
idx[pos[son[u]] = ++totpos] = son[u];
top[son[u]] = top[u];
dfs2(son[u]);
}
trav(u)
if (v != son[u] && v != fa[u])
{
idx[pos[v] = ++totpos] = v;
top[v] = v;
dfs2(v);
}
}
inline void upt(const int &x)
{
add(sum1[x] = sum1[lc], sum1[rc]);
add(sum2[x] = sum2[lc], sum2[rc]);
}
inline void add_node(const int &x, const int &l, const int &r, const int &val1, const int &val2)
{
// printf(":%d %d %d %d %d:%d\n", x, l, r, val1, val2, dis1[r] - dis1[l - 1]);
add(sum1[x], 1LL * (dis1[r] - dis1[l - 1]) * val1 % mod);
add(tag1[x], val1);
add(sum2[x], 1LL * (dis2[r] - dis2[l - 1]) * val2 % mod);
add(tag2[x], val2);
}
inline void dnt(const int &x, const int &l, const int &r)
{
if (tag1[x] || tag2[x])
{
int mid = l + r >> 1;
add_node(lc, l, mid, tag1[x], tag2[x]);
add_node(rc, mid + 1, r, tag1[x], tag2[x]);
tag1[x] = tag2[x] = 0;
}
}
inline void modify(const int &x, const int &l, const int &r, const int &u, const int &v, const int &val1, const int &val2)
{
if (u <= l && r <= v)
{
add_node(x, l, r, val1, val2);
return;
}
dnt(x, l, r);
int mid = l + r >> 1;
if (u <= mid)
modify(lc, l, mid, u, v, val1, val2);
if (v > mid)
modify(rc, mid + 1, r, u, v, val1, val2);
upt(x);
}
inline int query(const int &x, const int &l, const int &r, const int &u, const int &v)
{
if (u <= l && r <= v)
{
int res = sum1[x] + sum2[x];
if (res >= mod)
res -= mod;
return res;
}
dnt(x, l, r);
int mid = l + r >> 1, res = 0;
if (u <= mid)
add(res, query(lc, l, mid, u, v));
if (v > mid)
add(res, query(rc, mid + 1, r, u, v));
return res;
}
inline int path_query(int u, int v)
{
int res = 0;
while (top[u] != top[v])
{
if (dep[top[u]] < dep[top[v]])
std::swap(u, v);
add(res, query(1, 1, n, pos[top[u]], pos[u]));
u = fa[top[u]];
}
if (dep[u] > dep[v])
std::swap(u, v);
add(res, query(1, 1, n, pos[u], pos[v]));
return res;
}
int main()
{
A.a[1][1] = T.a[1][1] = T.a[1][2] = T.a[2][1] = 1;
read(n), read(m);
for (int i = 2; i <= n; ++i)
{
int u, v;
read(u), read(v);
addEdge(u, v);
addEdge(v, u);
}
dfs1(1);
pos[1] = idx[1] = top[1] = totpos = 1;
dfs2(1);
f_neg[0] = 0, f_neg[1] = 1;
for (int i = 2; i <= n; ++i)
add(f_neg[i] = f_neg[i - 2], -f_neg[i - 1]);
f_pos[1] = f_pos[2] = 1;
for (int i = 3; i < MaxNV; ++i)
add(f_pos[i] = f_pos[i - 1], f_pos[i - 2]);
for (int i = 1; i <= n; ++i)
{
add(dis1[i] = dis1[i - 1], getfib(dep[idx[i]]));
add(dis2[i] = dis2[i - 1], getfib(dep[idx[i]] - 1));
}
bool opt;
int x;
s64 y;
while (m--)
{
opt = getopt(), read(x), read(y);
if (opt)
modify(1, 1, n, pos[x], pos[x] + sze[x] - 1, getfib(y - dep[x] + 1), getfib(y - dep[x]));
else
printf("%d\n", path_query(x, y));
}
return 0;
}
3.6.2 一道簡單題
給定 \(n,k\),令 \(A(i_1,i_2,\cdots ,i_k)=F_{1+\sum_{i=1}^k(i_k-1)}\),其中 \(F\) 是斐波那契數列,求
\[\sum_{1 \leq i_1,i_2,\cdots,i_k \leq n}A(i_1,i_2,\cdots,i_k) \bmod 10^9+7 \]\(n,k \leq 10^9\),數據組數 \(T \leq 100\)。
時空限制:\(\texttt{1s/512MB}\)。
先從 \(k=1\) 開始考慮,根據 \((8)\) 我們可以得到 \(\sum_{i=1}^nF_i=F_{n+2}-F_2\),因此 \(k=1\) 的答案可以表示為斐波那契數列的兩項相減的形式。
再考慮 \(k=2\),這時候相當於算這個式子
類似斐波那契數列前綴和的推導,我們發現這個式子可以寫成
不難驗證數列 \(F'_n=F_{n+\alpha+2}-F_{\alpha+2}\)(其中 \(\alpha\) 是常數)是一個類斐波那契數列(即滿足斐波那契數列遞推式的數列,但對前兩項沒有要求)。因此這個式子還可以寫成
以此類推,也就是說,前 \(k\) 維的答案就可以直接寫成一個類斐波那契數列的第 \(n+2\) 項和第 \(2\) 項相減的形式,這啟發我們求出這個數列具體的形式。定義 \(G_k(n)\) 表示在計算第 \(k\) 維時對應的類斐波那契數列,形式化地,我們有
因為 \(G_k(n)\) 是類斐波那契數列,所以可以證明 \(G_k(n)\) 的本質其實是考慮前 \(k\) 維,並且 \(i_k=n\) 的答案。
也就是說,當我們要計算前 \(k\) 維的答案 \(S_k\) 時,其實就只要考慮
那么現在的問題就很明顯了,我們只需要求出 \(G_k(n+2)\) 和 \(G_k(2)\)。具體地,我們考慮到
考慮到類斐波那契數列的一個性質 \(G_k(n)=F_{n-1}G_k(0)+F_nG_k(1)\),那么上式可以改寫成
需要用到的斐波那契數可以用矩乘預處理,上式也可以用矩乘來遞推 \(G_k(0),G_k(1)\)。
那么本題就做完了,時間復雜度就是 \(\mathcal O(T\log n)\)。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int n, K;
struct mat
{
int r, c;
int a[5][5];
mat(){}
mat(int n, int m):
r(n), c(m) {memset(a, 0, sizeof(a));}
inline void init() //初始化單位矩陣
{
for (int i = 1; i <= r; ++i)
a[i][i] = 1;
}
inline mat operator * (const mat &rhs) const //矩陣乘法
{
mat res(r, rhs.c);
for (int i = 1; i <= r; ++i)
for (int k = 1; k <= c; ++k)
for (int j = 1; j <= rhs.c; ++j)
res.a[i][j] = (res.a[i][j] + 1LL * a[i][k] * rhs.a[k][j]) % mod;
return res;
}
inline mat operator ^ (int p) const //矩陣快速冪
{
mat res(r, c), x = *this;
res.init();
for (; p; p >>= 1, x = x * x)
if (p & 1)
res = res * x;
return res;
}
};
int main()
{
int TAT;
std::cin >> TAT;
while (TAT--)
{
scanf("%d%d", &n, &K);
mat T(4, 4);
T.a[1][1] = T.a[1][3] = T.a[2][2] = 1;
T.a[2][4] = T.a[3][1] = T.a[4][2] = 1;
mat F1 = mat(1, 4);
F1.a[1][2] = 1, F1.a[1][3] = 1;
mat F2 = F1;
F1 = F1 * (T ^ n);
F2 = F2 * (T ^ (n + 1));
int a1 = F1.a[1][1], b1 = F1.a[1][2];
int a2 = F2.a[1][1], b2 = F2.a[1][2];
//預處理類斐波拉契的n+1,n+2項對應的系數
mat F(1, 2);
F.a[1][2] = 1;
T = mat(2, 2);
T.a[1][1] = a1, T.a[1][2] = a2 ? a2 - 1 : mod - 1;
T.a[2][1] = b1 ? b1 - 1 : b1, T.a[2][2] = b2 ? b2 - 1 : b2;
//通過得到的系數得出轉移矩陣
F = F * (T ^ K);
printf("%d\n", F.a[1][2]);
}
fclose(stdin);
fclose(stdout);
return 0;
}
3.6.3 一道 LCT 題
給定一棵 \(n\) 個結點的有根樹,保證父親標號小於兒子,點 \(i\) 有點權 \(a_i\),進行 \(m\) 次操作,共有 \(4\) 種類型:
給定 \(u,v\),將 \(u\) 的父親改為 \(v\),保證 \(v<u\)。
給定 \(u,v,x\),將 \((u,v)\) 路徑上的點權全部改成 \(x\)。
給出 \(u\),詢問 \(F(a_u)\bmod 998244353\)。
給定 \(u,v\),對於路徑 \(u\to v\),假設路徑上的點權值分別是 \(b_1,b_2,\cdots,b_k\),求
\[\sum_{i=1}^k\sum_{j=i}^kF_{\sum_{p=i}^jb_p} \bmod 998244353 \]其中 \(F\) 是斐波那契數列。\(n,m \leq 10^5,1 \leq a_i,x\leq 10^9\)。
時空限制:\(\texttt{2s/512MB}\)。
考慮用通項公式處理斐波那契數列,在擴域 \(\{a+b\sqrt 5\arrowvert a,b\in\mathbb F_p\}\) 下計算。
那么
下面只考慮維護區間的所有子區間 \([l,r]\) 的 \(\prod_{i=l}^r\phi^{b_i}\) 之和,\(\bar\phi\) 的處理是類似的。
考慮用 LCT 實現時合並兩個區間的操作,如下圖:
修改父親直接用 LCT 實現就可以了,考慮鏈覆蓋的操作,我們需要在 LCT 上打標記。
只考慮覆蓋一整條鏈的操作對維護的信息的影響,如下圖:
因為要快速冪,所以時間復雜度為 \(\mathcal O(n\log^2n)\)。
代碼暫時沒有。
3.6.4 一道數學題
給定一個長度為 \(n\) 的序列 \(a_1,a_2,\cdots,a_n\),求
\[\mathrm{lcm}(F_{a_1},F_{a_2},\cdots,F_{a_n})\bmod 10^9+7 \]其中 \(F\) 是斐波那契數列。\(n \leq 5\times 10^4,a_i\leq 10^6\)。
時空限制:\(\texttt{3s/128MB}\)
我們記這個序列構成的集合為 \(S\),對每個質因子的冪進行 \(\text{min-max}\) 容斥可以得到
並且根據 \((11)\),我們可以得到
Solution 1
這個做法是從這里學的:
這是一種比較巧妙的做法。
直接做還是不太好做,對於帶 \(\mathrm{gcd}\) 的這種式子,考慮反演會比較方便,假設我們能構造一個序列 \(\{g_n\}\) 滿足
那么原式就可以寫成
注意 \(g_d\) 上面指數的含義。注意到一個非空集合的大小為奇數的子集個數,和大小為偶數的子集個數是相等的(包括空集),因此指數部分可以寫成
因此原式就可以寫成
序列 \(\{g_n\}\) 可以直接硬算
那么這題就做完了,時間復雜度 \(\mathcal O(n+m\log m)\),其中 \(m=\max\{a_i\}\)。
#include <bits/stdc++.h>
template <class T>
inline void read(T &x)
{
static char ch;
while (!isdigit(ch = getchar()));
x = ch - '0';
while (isdigit(ch = getchar()))
x = x * 10 + ch - '0';
}
template <class T>
inline void relax(T &x, const T &y)
{
if (x < y)
x = y;
}
const int mod = 1e9 + 7;
const int MaxN = 5e4 + 5;
const int MaxM = 1e6 + 5;
int n, m;
int a[MaxN];
bool vis[MaxM];
int f[MaxM], g[MaxM], t[MaxM];
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline int qpow(int x, int y)
{
if (x == 1)
return 1;
int res = 1;
for (; y; y >>= 1, x = 1LL * x * x % mod)
if (y & 1)
res = 1LL * res * x % mod;
return res;
}
int main()
{
read(n);
for (int i = 1; i <= n; ++i)
{
read(a[i]);
relax(m, a[i]);
vis[a[i]] = true;
}
f[1] = 1;
for (int i = 2; i <= m; ++i)
add(f[i] = f[i - 1], f[i - 2]);
for (int i = 1; i <= m; ++i)
t[i] = 1;
int ans = 1;
for (int i = 1; i <= n; ++i)
{
g[i] = 1LL * f[i] * qpow(t[i], mod - 2);
bool flg = false;
if (vis[i])
flg = true;
for (int j = i + i; j <= m; j += i)
{
t[j] = 1LL * t[j] * g[i] % mod;
if (vis[j])
flg = true;
}
if (flg)
ans = 1LL * ans * g[i] % mod;
}
std::cout << ans << std::endl;
return 0;
}
Solution 2
這是一種比較套路的做法。
可以直接套用莫比烏斯反演,比較推得硬核一些:
把指數部分拿出來
那么指數部分可以直接枚舉倍數算,這題就做完了。
這種做法的代碼暫時沒有。
4 斐波那契表示法
參考:
- Zeckendorf's theorem——Wikipedia
- 《具體數學》(第2版,人民郵電出版社)P248~249
- LOJ 3184: 「CEOI2018」斐波那契表示法——Pinkrabbit
4.1 齊肯多夫定理
\(\textbf{Zeckendorf's theorem:}\) 任何正整數 \(n\) 都可以被表示為若干項不同且不相鄰的斐波那契數之和,且表示方法是唯一的(不包括 \(F_0\) 和 \(F_1\))。形式化地,對於任意正整數 \(n\),有且僅有一個滿足 \(c_1\geq 2\) 且 \(c_i \geq c_{i-1}+2(i>1)\) 的正整數序列 \(\{c_k\}\),使得
並且我們稱這種唯一的表示方法為齊肯多夫表示(Zeckendorf representation)。
證明:這個定理分為兩部分:存在一種齊肯多夫表示,並且表示方法是唯一的。
-
首先證明對於任意正整數 \(n\),存在一種齊肯多夫表示。我們可以考慮歸納證明,假設這個結論對於滿足 \(n <F_k\) 的正整數 \(n\) 成立,那么接下來我們證明對於滿足 \(F_{k}\leq n < F_{k+1}\) 的正整數 \(n\) 也是成立的。
我們考慮在 \(n\) 的齊肯多夫表示中加上 \(F_k\),那么考慮 \(n-F_k<F_{k+1}-F_{k}=F_{k-1}\),因此 \(n-F_k\) 的齊肯多夫表示中不包含 \(F_{k-1}\),我們直接用 \(n-F_{k}\) 的齊肯多夫表示加上 \(F_k\),就能得到一個符合條件的表示。
-
接下來我們證明這個齊肯多夫表示是唯一的。需要先有一個引理。
\(\textbf{Lemma.}\) 對於滿足最大的斐波那契數為 \(F_i\) 的一種齊肯多夫表示,這種表示的所有斐波那契數之和嚴格小於 \(F_{i+1}\)。
證明:引理的證明比較簡單,只要對 \(F_i\) 進行歸納,刪去 \(F_i\) 后可以得到比它小的那些數的和嚴格小於 \(F_{i-1}\),進而加上 \(F_i\) 就嚴格小於 \(F_{i+1}\)。接下來采用反證法,我們假設有兩個不連續的斐波那契數的集合 \(S,T(S\neq T)\),滿足 \(S\) 中的斐波那契數之和等於 \(T\) 中的斐波那契數之和。去掉它們中的相同元素,分別得到 \(S'=S\setminus (S\cap T)\),\(T'=T\setminus(S\cap T)\),根據 \(S\neq T\) 以及集合中的數都是正的,\(S',T'\) 應當均非空,並且因為去掉的是相同元素,兩個集合中的元素之和也應當相等。
假設 \(S'\) 中的最大元素為 \(F_s\),而 \(T'\) 中的最大元素為 \(F_t\)。不失一般性地,我們假設 \(F_s>F_t\),那么就有 \(T'\) 中的元素之和 \(<F_{t+1}\leq F_s\),因此就有兩個集合中的元素之和不相等,產生矛盾,故假設不成立。
結合兩個證明,我們就能證明齊肯多夫定理。
齊肯多夫定理的證明過程也告訴了我們如何構造一個正整數的齊肯多夫表示:每次找到當前小於等於 \(n\) 的最大斐波那契數,然后從 \(n\) 里面減掉這個斐波那契數,重復執行這個過程直到 \(n=0\)。
4.2 斐波那契數系
齊肯多夫定理告訴我們,任意正整數 \(n\) 都有一個唯一的齊肯多夫表示法。任何有唯一性的表示方法都是是個數系,這樣一來,齊肯多夫定理就引導出斐波那契數系,我們可以將任何非負整數 \(n\) 用 \(0\) 和 \(1\) 的一個序列表示,記
並且 \(b\) 中不存在兩項 \(b_i\) 和 \(b_{i+1}\) 同為 \(1\)。
4.3 應用
4.3.1 「BJOI2012」最多的方案
給定一個正整數 \(n\),求 \(n\) 能寫成多少種不同的斐波那契數之和(不包括 \(F_0\) 和 \(F_1\))。
\(n \leq 10^{18}\)
時空限制:\(\texttt{0.5s/128MB}\)
我們考慮先求出 \(n\) 的齊肯多夫表示,這樣方便我們處理。
可以證明,一種斐波那契表示(即若干不同的斐波那契數之和)一定可以用齊肯多夫表示通過若干次如下操作達到:假設當前的斐波那契數集合為 \(S\),找到一個滿足 \(F_i\in S\land F_{i-1},F_{i-2}\not\in S\) 的 \(F_i\),然后令新的集合為 \((S\setminus\{F_i\})\cup\{F_{i-1},F_{i-2}\}\)。
證明只需要反過來考慮即可,我們考慮每次在當前的斐波那契表示 \(T\) 中找到一個滿足 \(F_i,F_{i+1}\in T,F_{i+2}\not \in T\) 的 \(F_i\),然后令新的集合為 \((T\setminus\{F_i,F_{i-1}\})\cup\{F_{i+2}\}\)。容易發現若干次這樣的操作過后就能使集合不存在連續的兩個斐波那契數,得到齊肯多夫表示,並且這個操作是上面的操作的反操作,所以結論成立。
因此我們不妨這么想,考慮 \(n\) 的在斐波那契數系下的表示 10001000100
,我們可以將其分段為 [1000][1000][100]
。因為我們要求,將集合 \(S\) 中的 \(F_i\) 分解成 \(F_{i-1}+F_{i-2}\) 時,必須要有 \(F_{i-1},F_{i-2}\not \in S\),因此若 10000
變成了 01100
,后面只能分解較小的那個斐波那契數,即分解為 01010
。
因此我們發現,每一段之間大致是獨立的,我們可以考慮分階段 DP。但是有一種特殊情況,[1000][1000][100]
如果最小的那段先分解成 [1000][1000][011]
,那么中間這段可以利用空出來的空位,使得多分解一個變成 [1000][0101][111]
。
因此我們設 \(f_{i,0/1}\) 表示考慮了齊肯多夫表示的前 \(i\) 個斐波那契數,第 \(i\) 個是否有分解的方案數。可以發現,每一段內,能分解的總是當前最小的那個 1
,還要考慮 \(i-1\) 是否分解帶來的空位影響。因此我們有
邊界條件是 \(c_0=0,f_{0,0}=1,f_{0,1}=0\),注意本題我們需要將 \(F_1=1,F_2=2\)。
4.3.2 「CEOI2018」斐波那契表示法
本題中的斐波那契數列的前兩項為 \(F_1=1,F_2=2\)。
令 \(X(p)\) 表示把 \(p\) 表示為若干個不同的斐波那契數的和的表示法數,兩種表示法不同當且僅當有一個斐波那契數是其中一個的項,而不是另一個的項。
給定一個 \(n\) 項正整數序列 \(a_1,a_2,\cdots,a_n\),請你對於每個 \(1 \leq k \leq n\),求出 \(X\left(\sum_{i=1}^kF_{a_i}\right)\bmod 10^9+7\)。
\(n \leq 10^5\),\(a_i \leq 10^9\)
時空限制:\(\texttt{4s/256MB}\)
考慮沿用上一題的 DP 思路,不過本題需要支持動態維護齊肯多夫表示,因此我們需要考慮動態 DP。
考慮在上一題中,我們可以將轉移方程寫成矩陣的形式
其中 \(d_i=c_i-c_{i-1}\),即表示差分數組。假設齊肯多夫表示中有 \(m\) 個數,答案即為 \(f_{m,0}+f_{m,1}\),其中
如果我們能維護差分數組和對應的矩陣乘積,就可以解決這題了。
那么現在的問題是考慮加入一個 \(F_x\) 后會有什么影響,需要注意我們一定要保證當前的齊肯多夫表示中不包含相同或連續的兩個斐波那契數。
首先我們先考慮若當前 \(x-1,x,x+1\) 三個位置均為空,我們直接將 \(x\) 插入即可。否則就會使其他位置也產生變化,需要仔細考慮。
發現產生的影響與 \(0,1\) 的交替段密切相關,假設涉及的段的最低位的 \(1\) 的位置為 \(l\),最高位的 \(1\) 的位置為 \(r\)(\(l-1 \leq x\leq r+1\)),需要分情況討論:(左邊低位,右邊高位)
- \(x=r+1\),這種情況我們令 \(F_{r}\) 和 \(F_{r+1}\) 合並為 \(F_{r+2}\) 即可。注意到添加 \(F_{r+2}\) 可能也會影響后面的段,我們考慮遞歸下去處理。
- \(x\leq r\land x\not \equiv l\pmod 2\),這種情況我們一直重復 \(F_i+F_{i+1}=F_{i+2}\) 的操作,如下圖,最終使 \(x\sim r\) 的 \(1\) 全部變成 \(0\),並且加入 \(F_{r+1}\)。這時候新加入的 \(1\) 可能會產生交替段的合並,同樣往下遞歸。
- \(x\leq r\land x\equiv l\pmod 2\),這種情況,我們重復使用 \(2F_i=F_{i+1}+F_{i-2}\),可以得到下圖的結果。\(x\sim r\) 的 \(1\) 全部變為 \(0\),並且加入 \(r+1\)。 \(l\sim x-2\) 的 \(1\) 全部右移一位,並且加入 \(l-2\)。這時候加入的兩個可能會產生一些影響,同樣往下遞歸。
上述分類討論的依據都是 \(F_i=F_{i-1}+F_{i-2}\) 和 \(2F_i=F_{i+1}+F_{i-2}\)。
注意到上述討論存在遞歸操作,但是我們可以證明,遞歸操作的次數是常數次:我們只考慮三種情況之間的遞歸,如果遞歸到 \(x-1,x,x+1\) 都為空這種情況顯然沒關系。第 \(1\) 種情況只可能遞歸到第 \(2\) 種情況,第 \(3\) 種情況只可能遞歸到第 \(1\) 種,而第 \(2\) 種情況又不可能遞歸到其他兩種。因此遞歸是常數次的。
因此可以用 \(\text{std::set}\) 維護極大的交替段,用 \(\text{Splay}\) 維護差分序列、矩陣乘積。不難發現每次的操作次數都是均攤常數的,需要注意每個操作可能產生的交替段的合並、刪除、分裂等細節。
在 \(\text{Splay}\) 上面維護的時候有一些細節,如果用差分數組前綴和來表示每個結點的位置編號是比較方便的,但是插入刪除的時候需要注意一些細節。或者直接存下位置編號,但是一整段編號加一的操作可能要用打標記實現。
時間復雜度 \(\mathcal O(n \log n)\),用數組展開、循環展開等方式處理矩陣可以大幅減小常數。
4.3.3 「POI2012」斐波那契表示法
給定正整數 \(k\),求用斐波那契數的和或差表示 \(k\) 所需要的斐波那契數數量最小值。
\(k \leq 4\times 10^7\)
時空限制:\(\texttt{1s/64MB}\)
這題的貪心做法是這樣的:每次找到一個離 \(k\) 最近的斐波那契數 \(F_i\),令 \(k\leftarrow|k-F_i|\),重復若干次直到 \(k=0\)。(即每次令 \(k \leftarrow \min|k-F_i|\))
但是我在網上一直找不到比較好的證明,非常自閉QAQ。
首先有幾個性質:
-
存在最優方案,不會選擇重復的一項。
證明:因為我們有 \(2F_i=F_{i+1}+F_{i-2}\)。
-
存在最優方案,不會選擇相鄰的兩項。
證明:通過討論可以知道
\[\begin{cases} +F_i+F_{i+1}=+F_{i+2}\\ +F_i-F_{i+1}=-F_{i-1}\\ -F_i+F_{i+1}=+F_{i-1}\\ -F_i-F_{i+1}=-F_{i+2} \end{cases} \] -
若當前 \(F_i \leq k \leq F_{i+1}\),那么存在最優方案,一定包含了 \(F_i\) 或 \(F_{i+1}\)。
證明:反證法。假設不包含 \(F_i\) 和 \(F_{i+1}\)。那么根據不選相鄰和重復的原則,我們可以證明其他部分的斐波那契數通過加減一定無法湊到 \([F_i,F_{i+1}]\) 內的數。具體地,我們有 \(F_{i-1}+F_{i-3}+\cdots<F_i\) 成立,這是因為 \(F_1+F_3+\cdots +F_{2n-1}=F_{2n}-1\) 和 \(F_2+F_4+\cdots +F_{2n}=F_{2n+1}-1\)(為方便設 \(F_1=1,F_2=2\))。
這樣一來,\(F_1\sim F_{i-1}\) 的數里選出來的和 \(S<F_i\)。同樣我們如果用比 \(F_{i+1}\) 大的數拿去減,也會遇到這種的情況: \(F_{i+2}-S>F_{i+1}\) ,並且因為不能選相鄰的,\(F_{i+4}-F_{i+2}>F_{i+3}\) 也是沒用的。
因此我們就證明了,存在最優方案,一定包含了 \(F_i\) 或 \(F_{i+1}\)。
那么接下來,我們就得到了,若當前 \(F_{i}\leq k \leq F_{i+1}\),我們一定要從 \(F_i,F_{i+1}\) 中選一個。
接下來我們歸納證明,一定是選較近的那個斐波那契數:
- 顯然對於滿足 \(k < F_3\) 的 \(k\),結論成立。
- 假設對於滿足 \(k < F_i\) 的 \(k\),結論是成立的,那么接下來考慮證明對於滿足 \(F_i\leq k<F_{i+1}\) 的 \(k\),有結論成立。不妨假設 \(k-F_i=a\),\(F_{i+1}-k=b\),不失一般性地,設 \(a<b\),對於 \(a>b\) 同理。
- 接下來證明令 \(k\leftarrow a\) 的策略不比 \(k \leftarrow b\) 的策略劣。首先有 \(a+b=F_{i+1}-F_{i}=F_{i-1}\)。根據 \(a<b\) 我們有 \(b\in (\frac{F_{i-1}}2,F_{i-1}]\),並且因為 \(F_{i-3}<\frac{F_{i-1}}{2}<F_{i-2}<F_{i-1}\),而 \(\frac{F_{i-1}}2=\frac{F_{i-2}+F_{i-3}}2\),因此離 \(b\) 最近的斐波那契數一定是 \(F_{i-2},F_{i-1}\) 之一。
- 因為 \(b \leq F_{i-1}<F_i\) 滿足一定選離 \(b\) 較近的斐波那契數,因此我們有 \(b\) 的最優表示中一定有 \(F_{i-2},F_{i-1}\) 之一。那么 \(a=F_{i-1}-b\),所以 \(a\) 可以由 \(b\) 的表達轉化過來,並且 \(F_{i-1}\) 可以和 \(b\) 的表示中的 \(F_{i-2}\) 或是 \(F_{i-1}\) 合並/抵消,因此 \(a\) 均能得到一種不劣於 \(b\) 的表達方式。
至此我們證明了貪心策略的正確性。
顯然,\(k\) 每次至少減少一半,所以答案是 \(\mathcal O(\log k)\) 級別的,這也是時間復雜度的級別。