【学习笔记】斐波那契数列的简单性质
(公式可能渲染得比较慢,这时候,你只需要,放在那边加载一会就好了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)\) 级别的,这也是时间复杂度的级别。