[CodeForces 575A] Fibonotci
题意
给定两个无穷长度的正整数序列 \(\set{s_n}_{n=0}^\infty\) 和 \(\set{f_n}_{n=0}^\infty\):
- \(\set{s_n}_{n=0}^\infty\):给定 \(s_0,s_1,\dots,s_{n-1}\),对于 \(i\ge n\),有 \(m\) 个 \(i\) 的值给定,其余 \(i\) 均满足 \(s_i=s_{i\bmod n}\)。
- \(\set{f_n}_{n=0}^\infty\):\(f_0=0,f_1=1\),对于 \(i\ge 2\),满足 \(f_i=f_{i-1}s_{i-1}+f_{i-2}s_{i-2}\)。
输出 \(f_k\bmod p\) 的值。
数据范围:\(1\le n,m\le 5\times 10^4\),\(0\le k\le 10^{18}\),\(1\le s_i,p\le 10^9\)。
思路
问题简化: 如果 \(m=0\) 怎么做?注意到 \(f\) 为常系数线性递推式,故可表示为矩阵的形式:
从而,可通过如下方式计算 \(f_k\):
由于 \(m=0\),所以 \(s\) 具有周期性。于是,只需要预处理矩阵的前缀积,通过快速幂即可做到 \(O(\log V)\) 查询。
回归原问题: 此时,\(s\) 有个别位置会发生变化,若 \(i\) 位置发生变化,只会影响矩阵乘积中两个矩阵的值。所以,实际上被修改的位置将矩阵的乘积划分成若干段:
- 段中:具有周期性,需维护从上个修改位置快速转移至下个修改位置,也就是这一段中的所有矩阵乘积;
- 端点:只会影响两个矩阵,可暴力更新。
这样的话,就不仅需要维护前缀积,还需要维护后缀积,同时如果左右端点在同一个长度为 \(n\) 的块中,需要区间查询(线段树),细节很多且麻烦。不过,由于本题没有单点修改,用线段树大材小用了。实际上,只需要倍增即可解决。具体而言,令 \(f_{i,j}\) 表示区间 \([i,i+2^j)\) 中所有矩阵的乘积,转移借助周期性:
每次查询一段矩阵的乘积 \([x,y]\),直接采用倍增的方式,跳 \(y-x+1\) 次即可,每次查询时间复杂度为 \(O(\log V)\)。总复杂度为 \(O(n\log V)\)。
代码
这里给出采用倍增的写法,极为简短,细节并不多。注意 \(k\le 1\) 和模数为 \(1\) 的情况。
#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
using i64 = long long;
using mat = array<array<int, 2>, 2>;
#define _(i, j) ((i64)a[i][0] * b[0][j] % p + (i64)a[i][1] * b[1][j] % p) % p
i64 p, k;
void e(mat &a) { a = {1, 0, 0, 1}; }
mat operator* (mat a, mat b) { return {_(0, 0), _(0, 1), _(1, 0), _(1, 1)}; }
const int maxn = 5E4;
int n, m, s[maxn];
pair<i64, int> t[maxn];
mat f[maxn][60];
map<i64, int> vis;
mat trans(i64 x, i64 y) {
mat res; e(res); i64 k = y - x;
for (int i = 59; i >= 0; i --)
if (k >> i & 1) {
res = res * f[x % n][i];
x += 1ll << i;
}
return res;
}
int get(i64 x) { return vis.count(x) ? vis[x] : s[x % n]; }
mat mt(int a, int b) { return {0, a, 1, b}; }
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> k >> p >> n;
for (int i = 0; i < n; i ++) cin >> s[i];
for (int i = 0; i < n; i ++) f[i][0] = mt(s[(i + n - 1) % n], s[i]);
for (int i = 1; i < 60; i ++)
for (int j = 0; j < n; j ++)
f[j][i] = f[j][i - 1] * f[((1ll << i - 1) + j) % n][i - 1];
cin >> m;
for (int i = 0; i < m; i ++)
cin >> t[i].fi >> t[i].se, vis[t[i].fi] = t[i].se;
if (k <= 1) return cout << k % p << '\n', 0;
sort(t, t + m);
i64 cur = 1; mat res; e(res);
for (int i = 0; i < m; i ++) {
if (t[i].fi >= k) break;
if (cur < t[i].fi) res = res * trans(cur, t[i].fi), cur = t[i].fi;
if (cur <= t[i].fi) res = res * mt(get(t[i].fi - 1), t[i].se), cur ++;
if (cur == k) break;
res = res * mt(t[i].se, get(t[i].fi + 1)), cur ++;
}
if (cur < k) res = res * trans(cur, k);
cout << res[1][1] << '\n';
return 0;
}
总结与反思
本题思路不难,主要是写法上有一些技巧:并未想到可以采用倍增,来维护模意义下的静态区间。而是,采用了线段树的方式,细节繁琐且麻烦。而倍增可以简单解决这类问题,码量会少很多。同时,矩阵没必要开一个 struct,需要多维护很多信息,使用 array<array<int, 2>, 2> 更佳!因为 array 是可以重载乘法的,而且自带很多东西(比如说,构造函数)。当 \(2\times 2\) 或 \(3\times 3\) 这样的小矩阵时,可以不写循环,直接手动模拟乘法过程,写出来会简洁很多。
总结:
- 模意义下静态区间查询:倍增(不是模意义下,用倍增也会方便很多),本质就是 \(O(\log n)\) 查询的元素不允许多次维护 ST 表。
- 矩阵乘法:
array会比开struct方便,\(2\times 2\) 优秀写法如下:
using mat = array<array<int, 2>, 2>;
#define _(i, j) a[i][0] * b[0][j] + a[i][1] * b[1][j]
void e(mat &a) { a = {1, 0, 0, 1}; }
mat operator* (mat a, mat b) { return {_(0, 0), _(0, 1), _(1, 0), _(1, 1)}; }

浙公网安备 33010602011771号