Loading

[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\) 为常系数线性递推式,故可表示为矩阵的形式:

\[\begin{bmatrix} f_{n-2} & f_{n-1} \end{bmatrix} \times \begin{bmatrix} 0 & s_{n-2}\\ 1 & s_{n-1} \end{bmatrix} = \begin{bmatrix} f_{n-1} & f_{n} \end{bmatrix} \]

从而,可通过如下方式计算 \(f_k\)

\[\begin{bmatrix} f_{0} & f_{1} \end{bmatrix} \times \begin{bmatrix} 0 & s_{0}\\ 1 & s_{1} \end{bmatrix} \times \begin{bmatrix} 0 & s_{1}\\ 1 & s_{2} \end{bmatrix} \times \cdots \times \begin{bmatrix} 0 & s_{k-2}\\ 1 & s_{k-1} \end{bmatrix} = \begin{bmatrix} f_{k-1} & f_{k} \end{bmatrix} \]

由于 \(m=0\),所以 \(s\) 具有周期性。于是,只需要预处理矩阵的前缀积,通过快速幂即可做到 \(O(\log V)\) 查询。

回归原问题: 此时,\(s\) 有个别位置会发生变化,若 \(i\) 位置发生变化,只会影响矩阵乘积中两个矩阵的值。所以,实际上被修改的位置将矩阵的乘积划分成若干段:

  • 段中:具有周期性,需维护从上个修改位置快速转移至下个修改位置,也就是这一段中的所有矩阵乘积;
  • 端点:只会影响两个矩阵,可暴力更新。

这样的话,就不仅需要维护前缀积,还需要维护后缀积,同时如果左右端点在同一个长度为 \(n\) 的块中,需要区间查询(线段树),细节很多且麻烦。不过,由于本题没有单点修改,用线段树大材小用了。实际上,只需要倍增即可解决。具体而言,令 \(f_{i,j}\) 表示区间 \([i,i+2^j)\) 中所有矩阵的乘积,转移借助周期性:

\[f_{i,j}=f_{i,j-1}\times f_{(i+2^{j-1})\bmod n,j-1} \]

每次查询一段矩阵的乘积 \([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\) 这样的小矩阵时,可以不写循环,直接手动模拟乘法过程,写出来会简洁很多。

总结:

  1. 模意义下静态区间查询:倍增(不是模意义下,用倍增也会方便很多),本质就是 \(O(\log n)\) 查询的元素不允许多次维护 ST 表。
  2. 矩阵乘法: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)}; }
posted @ 2025-03-17 13:19  Pigsyy  阅读(21)  评论(0)    收藏  举报