万能欧几里得算法学习笔记

万能欧几里得算法

万能欧几里得算法用于解决一类与px+rq有关的和式求解问题,例如类欧几里得算法中提到的和式就属于此类问题。但万能欧几里得算法从几何意义出发能处理更广泛的和式类型。例如x=0lAxBpx+rqA,BRn×n

我们在平面直角坐标系中作出一条直线y=px+rq。以y=5x+34为例

6

我们注意到这条直线与平行于坐标轴的直线有若干个交点。与横线的交点(紫色),与竖线的交点(红色),如果同时与横线和竖线相交,我们约定先与横线相交再与竖线相交,这么做的原因我们等会会看到。

6

我们要求解的和式就可以看做,从初始状态开始,遇到横线做一种操作,遇到竖线做另一种操作最终得到答案。因为遇到横线意味着px+rq会增加1,遇到竖线意味着x会增加1

以开头提到的x=0lAxBpx+rqA,BRn×n为例,我们设定一个三元组(C,D,ans)来记录答案。

初始时C=D=Inans=Brq,我们只需要关注(0,l]范围内的交点,然后每经过一根横线,我们需要将D右乘BCans不变。每经过一条竖线,我们需要将C左乘ADans+ACDansnew

对三元组形式化的理解是,C表示当前操作序列Ax的值,D表示当前操作序列Bpx+rq的值,ans表示当前操作序列对答案的贡献。这里我们可以明白一开始约定碰到与横线竖线同时相交的点要视作先碰横线再碰竖线,因为在碰到竖线的时候意味着x增加了1,需要更新答案了,先要把px+rq更新到最新。

我们继续把上述操作用标准化的语言描述,定义三元组的乘法,(a,b,c)(d,e,f)=(ad,be,c+afb)a,b,c,d,e,fRn×na,d是矩阵A的幂,b,e是矩阵B的幂,碰上横线进行的操作是乘三元组(In,B,On),碰上竖线进行的操作是乘三元组(A,In,A)。容易证明这个三元组的乘法具有结合律,事实上这也是万能欧几里得算法能解决问题的必要条件。

有了上面的储备,接下来就来看看怎么样加速求解的过程。方便起见,我们用s0表示碰到横线需要执行的操作,s1表示碰到竖线需要执行的操作。用solve(p,q,r,l,s0,s1)描述问题的求解过程。

  • case0:对于r的处理。
    由于我们只关心交点到来的顺序以及交点的种类,因此将函数上下平移整数个单位是不影响答案的。我们把r修改为r%q,后面我们都默认r<q

  • case1:pq
    在这种情况下,我们可以发现:

    p(x+1)+rqpx+rq=px+(p%q)+rqpx+rq+pqpq

    这说明在遇到一根竖线之前我们至少会遇到pq条横线。这样就可以把这pqs0操作和一个s1操作合并成一个新的s1操作。这样每条竖线之前会有pqx条横线被合并

    px+rqpqx=(p%q)x+rq

    因此solve(p,q,r,l,s0,s1)=solve(p%q,q,r,l,s0,s0pqs1)

  • case2:p<q
    我们可以想到把坐标轴翻转,把y=px+rq变为y=qxrp同时交换s0,s1这样就可以回归到case1中去了。但翻转过后我们发现三个小问题,第一个是对于同时和横线竖线相交的点,翻转之后变成了先做s1再做s0这与我们的期望不符,第二个是直线在y轴上的截距变成了负的;第三个是原先我们是以竖线结尾的,现在变成以横线结尾。这些问题导致处理起来会有麻烦。(下面的推到过程并不适用于p=1的情况,但我们之后会看到用下面推到过程得出的算法对于p=1确是正确的)
    我们先来解决第一个问题,

    yNqyr0(modp)qyrpqyrp=qyrqyrppp=(qyr)%pp1p

    因此我们可以将原函数向左平移1p个单位再反转,这并不影响答案,同时又解决了同时和横线竖线相交的点。直线变成y=qxr1p
    再来看剩下的两个问题。事实上,第二个问题对应翻转前y<1的情况,第三个问题对应翻转前ypl+rq的情况。我们可以掐头去尾把这两部分单独拿出来考虑。

    y=px+rq<10<xqr1py=px+rqpl+rqpl+rqqr1p+1xl

    这样我们只需要专注于(1,pl+rq]内的交点即可。为了与最初的问题适配,我们把直线向左平移一个单位,并计算(0,pl+rq1]内的交点。直线变成y=qx+qr1p
    因此solve(p,q,r,l,s0,s1)=s1qr1ps0solve(q,p,qr1,pl+rq1)s1lpl+rqqr1p

  • case3:p=1
    我们再来看p=1的情况,p=1时,当y取到整数时,x也必然是整数,操作过程是规律性的q条竖线1条横线,q条竖线1条横线, 在case2的算法中我们掐头时,少算了经过y=1这个整点的一条竖线,然而在经过其他整点时由于横线竖线顺序是反的,刚好把这个误差补了回来,在去尾时,多算了经过y=pl+rq时的一条竖线,刚好又把最后的误差给补足了。所以虽然case2的推导过程对p=1不适用,但导出的算法恰好适合p=1真是太奇妙了

现在我们完成了整个万能欧几里得算法,他和传统欧几里得算法的过程类似,时间复杂度是合并两个操作的时间复杂度乘上O(log2p)

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;
}
posted @   clapp  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示