万能欧几里得算法学习笔记
万能欧几里得算法
万能欧几里得算法用于解决一类与
我们在平面直角坐标系中作出一条直线
我们注意到这条直线与平行于坐标轴的直线有若干个交点。与横线的交点(紫色),与竖线的交点(红色),如果同时与横线和竖线相交,我们约定先与横线相交再与竖线相交,这么做的原因我们等会会看到。
我们要求解的和式就可以看做,从初始状态开始,遇到横线做一种操作,遇到竖线做另一种操作最终得到答案。因为遇到横线意味着
以开头提到的
初始时
对三元组形式化的理解是,
我们继续把上述操作用标准化的语言描述,定义三元组的乘法,
有了上面的储备,接下来就来看看怎么样加速求解的过程。方便起见,我们用
-
case0:对于
的处理。
由于我们只关心交点到来的顺序以及交点的种类,因此将函数上下平移整数个单位是不影响答案的。我们把 修改为 ,后面我们都默认 。 -
case1:
在这种情况下,我们可以发现:这说明在遇到一根竖线之前我们至少会遇到
条横线。这样就可以把这 个 操作和一个 操作合并成一个新的 操作。这样每条竖线之前会有 条横线被合并因此
-
case2:
我们可以想到把坐标轴翻转,把 变为 同时交换 这样就可以回归到case1中去了。但翻转过后我们发现三个小问题,第一个是对于同时和横线竖线相交的点,翻转之后变成了先做 再做 这与我们的期望不符,第二个是直线在 轴上的截距变成了负的;第三个是原先我们是以竖线结尾的,现在变成以横线结尾。这些问题导致处理起来会有麻烦。(下面的推到过程并不适用于 的情况,但我们之后会看到用下面推到过程得出的算法对于 确是正确的)
我们先来解决第一个问题,因此我们可以将原函数向左平移
个单位再反转,这并不影响答案,同时又解决了同时和横线竖线相交的点。直线变成
再来看剩下的两个问题。事实上,第二个问题对应翻转前 的情况,第三个问题对应翻转前 的情况。我们可以掐头去尾把这两部分单独拿出来考虑。这样我们只需要专注于
内的交点即可。为了与最初的问题适配,我们把直线向左平移一个单位,并计算 内的交点。直线变成
因此 -
case3:
我们再来看 的情况, 时,当 取到整数时, 也必然是整数,操作过程是规律性的 条竖线 条横线, 条竖线 条横线, 在case2的算法中我们掐头时,少算了经过 这个整点的一条竖线,然而在经过其他整点时由于横线竖线顺序是反的,刚好把这个误差补了回来,在去尾时,多算了经过 时的一条竖线,刚好又把最后的误差给补足了。所以虽然case2的推导过程对 不适用,但导出的算法恰好适合 。真是太奇妙了。
现在我们完成了整个万能欧几里得算法,他和传统欧几里得算法的过程类似,时间复杂度是合并两个操作的时间复杂度乘上
code:万能欧几里得
#include <cstdio>
#include <cstring>
#include <iostream>
#define int long long
using namespace std;
const int mo = 998244353;
struct mat {
int a[25][25];
int n;
void init(int n) {
memset(a, 0, sizeof(a));
this->n = n;
}
void read() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
scanf("%lld", &a[i][j]);
}
}
}
void setI(int n) {
this->n = n;
memset(a, 0, sizeof(a));
for (int i = 1; i <= n; i++) {
a[i][i] = 1;
}
}
mat operator+(const mat& o) const {
mat ans;
ans.init(n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
ans.a[i][j] += o.a[i][j];
ans.a[i][j] += a[i][j];
ans.a[i][j] %= mo;
}
}
return ans;
}
mat operator*(const mat& o) const {
mat ans;
ans.init(n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
ans.a[i][j] += 1ll * a[i][k] * o.a[k][j] % mo;
ans.a[i][j] %= mo;
}
}
}
return ans;
}
void print() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j < n; j++) {
printf("%lld ", a[i][j]);
}
printf("%lld\n", a[i][n]);
}
}
} A, B;
struct pp {
mat a;
mat b;
mat ans;
int n;
pp(int n) {
a.setI(n);
b.setI(n);
ans.init(n);
this->n = n;
}
pp(mat o, mat p, mat q, int n) {
a = o;
b = p;
ans = q;
this->n = n;
}
pp setA() {
a.init(n);
b.setI(n);
a = ans = A;
return *this;
}
pp setB() {
a.setI(n);
b.init(n);
ans.init(n);
b = B;
return *this;
}
pp fix() {
for (int i = 1; i <= n; i++) {
ans.a[i][i]++;
ans.a[i][i] %= mo;
}
return *this;
}
pp operator*(const pp& o) const {
return pp(a * o.a, b * o.b, ans + a * o.ans * b, n);
}
pp pow(int p) {
pp yu = pp(n);
pp o = *this;
while (p) {
if (p & 1) yu = yu * o;
o = o * o;
p >>= 1;
}
return yu;
}
};
int p, q, r, l, n;
pp slove(int p, int q, int r, int l, pp s0, pp s1) {
r = r % q;
if (p >= q) return slove(p % q, q, r, l, s0, s0.pow(p / q) * s1);
int m = ((__int128)p * l + r) / q;
if (m == 0) return s1.pow(l);
return (s1.pow((q - r - 1) / p) * s0 *
slove(q, p, q - r - 1, m - 1, s1, s0) *
s1.pow(l - ((__int128)m * q - r - 1) / p));
}
signed main() {
scanf("%lld%lld%lld%lld%lld", &p, &q, &r, &l, &n);
A.init(n);
A.read();
B.init(n);
B.read();
pp s0 = pp(n).setB();
pp s1 = pp(n).setA();
pp ans = pp(n);
ans.ans = s0.pow((p+r)/q).b;
ans = ans * s0.pow((p + r) / q) * slove(p, q, p + r, l - 1, s0, s1);
(A * ans.ans).print();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下