- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
本文主要面向普及/提高组 OIer 和 ACMer。考虑大多数 OIer 的情况,本文默认读者只会矩阵乘法,不了解矩阵的行列式,矩阵的秩等内容。本文使用 C++ 编写代码示例.
二阶线性递推数列在 OI 界还有个著名的名字:广义斐波那契数列。其所指为如下数列 \(\{a_n\}\):
其中,\(p,q\in \mathbb{R}\),数列前两项为 \(a_0, a_1\).
由于该数列的递推关系是线性的,且每一项都和前两项有关,因此称为二阶线性递推数列.
当前文的数列 \(\{a_n\}\) 中 \(p=q=1,a_0=a_1=1\) 时,该数列就是大名鼎鼎的斐波那契数列 \(\{f_n\}\):
基于这样的递推关系,我们可以写出线性复杂度 \(\mathcal{O}(n)\) 的朴素算法:
int f(int n) { if (n == 0 || n == 1) { return 1; } return f(n - 1) + f(n - 2); }
对于较为简单的情况,这样的算法是可以接受的.
我们可以很容易的推广到更一般的情况,所以对于一般的二阶线性递推的朴素算法就留给读者练习吧.
直接根据递推式计算虽然简单,但是实在是太慢了,我们需要优化。如果你还没有学过矩阵乘法,请移步至 OI Wiki(记得顺便把方阵的逆看看,后面要用)。如果你学过矩阵乘法,应当对下面的式子不陌生(如果你对这个式子有点陌生的话,请尝试手动计算左面的矩阵乘法):
这个式子旨在将斐波那契递推关系用矩阵乘法表示,以进行进一步的操作。其中 \(\begin{bmatrix}f_{n-1} & f_{n-2}\end{bmatrix}\) 被称为状态矩阵(在本例中,它也是个向量),\(\begin{bmatrix} 1 & 1\\ 1 & 0\end{bmatrix}\) 被称为转移矩阵。这样的转移在线性代数中属于线性变换,其中转移矩阵被称为线性算子。我们可以很容易的推广到一般的二阶线性递推上:
据此可以得出使用矩阵乘法表示的二阶线性递推的通项公式:
通过矩阵快速幂算法就可以将算法优化到对数复杂度 \(\mathcal{O}(\log n)\),这在一般情况下已经是最优的了(至于是不是真的是最优的,我也不知道,我也不会证).
代码也是很好实现的,以斐波那契为例:
using vec = array; struct matrix : public array { using array::array; matrix(const vec &a, const vec &b) : array{a, b} { (*this)[0] = a; (*this)[1] = b; } matrix &operator*=(const matrix &other) { matrix res{}; for (int i{0}; i < 2; ++i) for (int j{0}; j < 2; ++j) for (int k{0}; k < 2; ++k) res[i][j] += (*this)[i][k] * other[k][j]; return *this = res; } } e{{1, 0}, {0, 1}}; matrix qpow(matrix x, int n) { auto res{e}; while (n) { if (n & 1) res *= x; x *= x, n >>= 1; } return res; } int fib(int n) { return qpow(matrix{{1, 1}, {1, 0}}, n)[0][0]; }
接下来的这部分内容会有点抽象,如果你学过抽象代数那看这部分内容就再好不过了.
我们可以找到另一种递推的矩阵表示:
如果你乍一眼看不出来,可以计算一下。为了找到这个式子,我思考了一个晚上。注意到在这个式子中所有的矩阵都形如:
每个矩阵都只与两个元素有关,因此我们可以使用一个二元组来表示这类矩阵,并定义二元组上的乘法:
事实上,在抽象代数领域,这是找到了一个特定类型矩阵的同构:
因此矩阵原有的代数性质对于二元组也是成立的,比如最重要的结合律,这是快速幂得以使用的基础。很容易得出使用这种二元组表示的通项公式:
很容易观察到二元组乘法的单位元:
而且不难找到 \((1,0)\) 的逆元:
这样我们就可以方便的实现倒推了.
理论上讲由于这个方法相比矩阵会少算两三次乘法或加法,因此常数会小一点,实际上没什么差距。下面是针对斐波那契数列的代码:
struct phi_tuple : pair { using pair::pair; phi_tuple &operator*=(const phi_tuple &other) { // 这里计算的时候提取了公因式以减少一次乘法 return *this = {first * (other.first + other.second) + second * other.first, first * other.first + second * other.second}; } } e{0, 1}; phi_tuple qpow(phi_tuple x, int n) { auto res{e}; while (n) { if (n & 1) res *= x; x *= x, n >>= 1; } return res; } int fib(int n) { return qpow(phi_tuple{1, 0}, n + 1).first; }
对于二阶线性递推,一种广为人知的求通项公式的方法是特征根法。对于二阶线性递推:
有特征方程:
可以解出该方程的两个复根:
当 \(\phi\neq\psi\) 时(即 \(\Delta\neq0\)),有:
当 \(\phi=\psi\) 时(即 \(\Delta=0\)),有:
接下来只需要将 \(a_0,a_1\) 代入求出 \(s,t\) 即可.
下面我们以斐波那契数列为例。斐波那契数列的特征方程为:
解得:
于是:
将 \(f_0=1,f_1=1\) 代入,会得到一坨很复杂的东西,于是方便起见取 \(f_0=0,f_1=1\):
这样我们就可以得到取 \(f_0=0,f_1=1\) 时的斐波那契通项公式了:
程序实现请继续往后看…… 。
方便起见,如无特殊说明,本节内容均以 \(2\times 2\) 矩阵为例 。
上面的方法有多种证明,在这里我们从线性代数的角度考虑。一种很自然的想法是想办法展开矩阵表示的通项公式:
显然问题的关键在于展开右侧的方阵幂。乍一看会有点没思路,不妨从更简单的对角矩阵考虑。对角矩阵指的是形如下面的方阵:
其中空白的区域表示省略的 \(0\)。观察对焦方阵的平方以及立方:
猜测对角矩阵的幂满足:
很容易通过数学归纳法证明。于是我们找到了计算对角矩阵的幂次的方法,接下来考虑推广到一般的方阵。最直接的想法是将方阵转化为对角矩阵。考虑下面的式子:
注意到:
其中 \(\boldsymbol{P}\) 和 \(\boldsymbol{P}^{-1}\) 成对出现,于是:
接下来只需找到 \(\boldsymbol{A}\) 对应的 \(\boldsymbol{P}\) 和 \(\boldsymbol{\Lambda}\) 即可,这个过程被称为矩阵的相似对角化.
在寻找相似对角化的方法之前,我们先补充一些知识。你也许已经知道线性方程组可以用矩阵乘法等价表示:
基于此,线性方程组均可视为 \(\boldsymbol{Ax}=\boldsymbol{b}\) 的形式。当 \(\boldsymbol{b}=\boldsymbol{0}\) 时,称其为齐次线性方程组,每个方阵都唯一对应一个齐次线性方程组.
另外,你还可能听说过行列式:
方阵 \(\boldsymbol{A}\) 的行列式可以显示方阵的一些性质,比如方阵对应的齐次方程组的解的情况和是否可逆:
因篇幅有限,在此不做证明.
相似对角化需要利用矩阵的特征值 \(\lambda\) 和特征向量 \(\boldsymbol{x}\),其满足:
等号右侧可以乘上单位矩阵 \(\boldsymbol{E}\) 来变形:
由于 \(\boldsymbol{x}\neq\boldsymbol{0}\) 故 。
这其实就是一个关于 \(\lambda\) 的多项式方程,实际上等号左边被称为矩阵的特征多项式。由于本文以二阶方阵为例,因此特征多项式是一元二次的,那么就有两个根 \(\lambda_1,\lambda_2\),这两个根都是矩阵的特征值。将 \(\lambda\) 回代即可得求出特征向量 \(\boldsymbol{x}\),每个 \(\lambda_i\) 均对应无穷多组解 \(k\boldsymbol{x_i},k\in\mathbb{R}\).
回到相似对角化,可以证明,如果一个矩阵是可以相似对角化的,那么必然有下面的相似对角化方案:
其中 \(\boldsymbol{x}_i\) 是任意一个特征值 \(\lambda_i\) 对应的非零特征向量。其中 \(\boldsymbol{P}\) 被称为特征矩阵。当且仅当 \(\lambda_i\) 两两不同时,矩阵 \(\boldsymbol{P}\) 可逆,读者自证不难。解线性方程组和求逆矩阵都可以通过高斯消元法,限于篇幅,这里就不展开了.
接下来仍然以斐波那契数列为例。以 \(f_0=1\) 为首项。有矩阵通项公式(其中 \(\odot\) 表示不重要的元素,下同):
转移矩阵的特征多项式、特征值和特征向量:
相似对角矩阵和特征矩阵:
特征矩阵的逆:
相似对角化求幂次:
于是我们就得到了通项公式:
接下来就是写代码了。方便起见,后文就以 \(f_0=0\) 作为首项……(说实话我也不知道我为啥要在这里绕来绕去,但是懒得改了)我们有通项公式:
如果只是要某一项的值的话,那直接在浮点数环境下计算就可以了。事实上,我们可以发现一个常数对半砍的公式:
其中 \(\mathrm{round}(x)\) 表示四舍五入到整数。这是由于 \(\left|\tfrac{1}{\sqrt{5}}\left(\tfrac{1-\sqrt{5}}{2}\right)^{n}\right|<\tfrac{1}{2}\),因此四舍五入可以忽略这部分影响。更加激进的,根据工程经验(其实是懒得误差分析),由于 \(\tfrac{1}{\sqrt{5}}\approx 0.447,\tfrac{1-\sqrt{5}}{2}\approx 1.618\),下面的公式可以完美作为通项公式:
代码也很好实现,适用于不需要取模的情况:
float qpow(float x, int n) { float r = 1; while (n) { if (n & 1) r *= x; x *= x, n >>= 1; } return r; } int fib(int n) { return round(0.447 * qpow(1.618, n)); }
但是如果你的需求是斐波那契数列取模,因为涉及到 \(\sqrt5\),那就比较复杂了。第一想法是二次剩余,但是 \(5\) 在很多常见模数下没有二次剩余(如果你不知道什么是二次剩余,可以简单的理解为模意义下的平方根,这是数论的内容)。如果你了解过抽象代数或者代数数论,你肯能会想到一个东西——二次整数环.
二次整数环就是所有由整数加上一个特殊无理数后,通过加减乘得到的数所组成的集合。这些数不仅保留了整数环的性质,还可以在更广的范围内进行带余除法等操作。如果你学过复数,那就像复数是由实数和虚数部分组成一样,二次整数环是由整数和一个特定的无理数(如 \(\sqrt5\))的整数倍组合而成的数,这通常表示为 \(\mathbb{Z}[\sqrt{d}]\)。二次整数环 \(\mathbb{Z}[\sqrt{d}]\) 上的运算和二次根式运算是一致的:
由于除法较为复杂而且我们现在用不到,就不做讨论了。在模意义下,每次计算完分别对整数部分和根式部分的系数取模即可,这实际上构成了二次整数环的剩余类环,一般记作 \(\mathbb{Z}[\sqrt{d}]/\{p\}\),\(p\) 是模数。在模意义下二次整数除以一个纯整数依旧是转化为乘上该整数的数论倒数(逆元)。除以二次整数的情况过于复杂且暂时本文用不到,就不做讨论了.
接下来就是愉快的代码实现环节:
using i64 = int64_t; constexpr i64 m = 1e9 + 7; template constexpr num qpow(num a, i64 n) { num r = 1; while (n) { if (n & 1) r *= a; a *= a, n >>= 1; } return r; } struct m64 { i64 x; constexpr m64(const i64 x) : x(((x % m) + m) % m) { } constexpr operator i64() const { return x; } constexpr m64 operator-() const { return m64(-x); } constexpr m64 operator*(const m64 &o) const { return m64((x * o.x) % m); } constexpr m64 &operator*=(const m64 &o) { *this = *this * o; return *this; } constexpr m64 operator/(const m64 &o) const { return m64(*this * qpow(o, m - 2)); } }; struct sr5m64 { m64 x, s; constexpr sr5m64(m64 x, m64 s) : x(x), s(s) { } constexpr sr5m64(i64 x) : x(x), s(0) { } constexpr sr5m64 operator-() const { return sr5m64(-x, -s); } constexpr sr5m64 operator+(const sr5m64 &o) const { return sr5m64(x + o.x, s + o.s); } constexpr sr5m64 operator-(const sr5m64 &o) const { return *this + (-o); } constexpr sr5m64 operator*(const sr5m64 &o) const { return sr5m64(x * o.x + 5 * s * o.s, x * o.s + s * o.x); } constexpr sr5m64 &operator*=(const sr5m64 &o) { *this = *this * o; return *this; } constexpr sr5m64 operator/(const m64 &o) const { return sr5m64(x / o, s / o); } }; constexpr m64 fib(i64 n) { return ((qpow(sr5m64(1, 1) / 2, n) - qpow(sr5m64(1, -1) / 2, n)) * sr5m64(0, 1) / 5).x; }
任何的二阶线性递推在模 \(m\) 下都是具有周期性的。考虑矩阵表示中的状态模 \(m\) 。
显然状态的每一项最多只有 \(m\) 种,因此所有可能的状态最多有 \(m^2\) 种,所以在二阶线性递推充分多次后必然产生两个相同的状态,因此会呈现出周期性。这个结论也可以推广到任意阶线性递推.
由于求解最小正周期需要用到二项式定理等内容,在此不做展开,详情参考皮萨诺周期。一道广为流传的题目可能需要这部分知识.
黄金分割比指的是 。
很显然其是斐波那契数列的一个特征根。本节小标题的意思是:
直接代入通项公式暴力求极限即可:
对于一部分二阶线性递推,也能得出相似的结论.
累了,就先写到这里吧…… 。
最后此篇关于[OI向]深入理解二阶线性递推的文章就讲到这里了,如果你想了解更多关于[OI向]深入理解二阶线性递推的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
当我推/拉存储库时,是否可以详细输出到底发生了什么?目前,我有一个大型存储库,正在将其推送到服务器,大约 15 分钟后。或者这样,它给了我一个错误,但没有告诉我它在这 15 分钟内做了什么。 最佳答案
我不知道我的方法是否有意义,但是,我需要实现如下图的布局: 现在,我只写一个 并用其中的一列表示每个区域,例如 . 没有黄色区域,这工作正常: green red blue
当我查看许多 CSS 网格系统和框架时,它们通常具有标准的列和行设置以及百分比宽度。例如这样的事情: 标准网格列: .col-10 { width: 83.33333%; width: cal
我想使用 git 子模块。 我需要采取的步骤将我的更改推送到我的项目是 add/commit/push from submodule directory add/commit/push from pa
以下为百度站长平台的公告全文: 结合站长对于关键词数据分析的需求,站长平台对流量与关键词工具进行了升级,推出(“关键词影响力”)这一全新概念。关键词影响力算法复杂,涵盖该关键词下百度搜索可以为
我需要一个具有普通按钮和下拉按钮的控件。 例如 类似的控件在 wxRibbonButtonBar 中可用,我无法在简单的 wxPanel 中使用它。 最佳答案 我实现了 SplitButton,它看起
我一直在做一个项目,使用 Bazaar 作为版本控制系统。现在我必须和离岸人员一起工作,而他们只想使用 SVN。 我有什么: 我的 bazaar 分支及其文件和修订版。 一个全新的 subversio
我一直在开发数据流/图表风格的内部 DSP 应用程序(Java 带有 Groovy/Jython/JRuby 的钩子(Hook),通过 OSGi 的插件,大量的 JNI),类似于纯数据和 simuli
我正在尝试使用 THUMB 指令创建一个阶乘方法,我基本上做到了。 我只有一个关于 PUSH/POP 操作码的问题:如果我使用 push 将 r0 的值存储在堆栈中(所以 push {r0} ),我可
在尝试 ZeroMQ Push/Pull (他们称之为 Pipeline)套接字类型时,我很难理解这个图案。它被称为“负载均衡器”。 假设单个服务器将任务发送给多个工作人员,推/拉将在所有客户端之间平
有什么方法可以使用 push() 方法找出我的数据何时保存在数据库中?我写了下面的代码,但它多次保存数据...... db.ref('news').push(opts).then(() => {
我有这个问题,每次推或拉时我都必须把它放进去。我认为这是新的。有什么想法吗? 最佳答案 您可能正在使用 https 网址。切换到 ssh 并确保您的 key 设置正确(如果您的密码短语为空),则不必输
为什么当您将一个值压入堆栈时,ESP 寄存器会减少(而不是增加),而当您弹出一个值时,ESP 寄存器会增加(而不是减少)?在这一点上,这对我来说是违反直觉的。 最佳答案 那是因为堆栈是从上到下“增长”
有什么方法可以使用 push() 方法找出我的数据何时保存在数据库中?我写了下面的代码,但它多次保存数据...... db.ref('news').push(opts).then(() => {
我决定编写一个测试代码来查看 pusher - many pullers bundle 是如何工作的,我的怀疑成真了。 拉取器按照连接的顺序接收消息,例如第一个消息由第一个连接的拉取器接收,第二个由第
我在 CSV 文件中存储了一长串日期。我已经成功地使用 d3.js 加载了这个数据集。现在我想向此数据集添加另一列,其中包含列表中每个日期的随机数。 我相信此数据集已作为对象数组加载。所以我正在使用下
我一直在寻找解决方案。不使用 c++11。 for(int a = 1; a < team1.chan; a++) { team1.nums.push_back(ppb.back())
我打算在布局中构建带有滑动 subview 的 UI。 +--------------+ +--------------+ +--------------+ | view1
Tiêu đề Trên màn hình nhỏ, tôi cần tiêu đề trước rồi mới đến trường văn bản, nhưng trên màn hình trung bình trở lên, tôi cần ngược lại - Tôi đã thử đẩy và kéo nhưng không hiệu quả - Bạn có ý tưởng nào không? Câu trả lời tốt nhất dựa trên Swa
Một số phần của ZeroMQ không hoạt động theo cách có thể dự đoán được. Tôi đang sử dụng VS2013 và zmq 3.2.4. Để không "mất" tin nhắn trong khuôn khổ pubsub của tôi [Ngoài lề: Tôi nghĩ đây là một lỗi thiết kế. Tôi có thể bắt đầu thuê bao của tôi đầu tiên
Tôi là một lập trình viên xuất sắc, rất giỏi!