跳到主要内容

类欧几里德算法

定义

类欧几里德算法是洪华敦在 2016 年冬令营营员交流中提出的内容。

其本质可以理解为,使用一个类似辗转相除法的方法来进行函数求和。

引入

f(a,b,c,n)=i=0nai+bcf(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor

其中 a,b,c,na,b,c,n 是常数。需要一个 O(logn)O(\log n) 的算法。

这个式子和我们以前见过的式子都长得不太一样。带向下取整的式子容易让人想到数论分块,然而数论分块似乎不适用于这个求和。但是我们是可以做一些预处理的。

如果说 aca\ge c 或者 bcb\ge c,意味着可以将 a,ba,bcc 取模以简化问题:

f(a,b,c,n)=i=0nai+bc=i=0n(acc+amodc)i+(bcc+bmodc)c=n(n+1)2ac+(n+1)bc+i=0n(amodc)i+(bmodc)c=n(n+1)2ac+(n+1)bc+f(amodc,bmodc,c,n)\begin{aligned} f(a,b,c,n)&=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor\\ &=\sum_{i=0}^n\left\lfloor \frac{\left(\left\lfloor\frac{a}{c}\right\rfloor c+a\bmod c\right)i+\left(\left\lfloor\frac{b}{c}\right\rfloor c+b\bmod c\right)}{c}\right\rfloor\\ &=\frac{n(n+1)}{2}\left\lfloor\frac{a}{c}\right\rfloor+(n+1)\left\lfloor\frac{b}{c}\right\rfloor+ \sum_{i=0}^n\left\lfloor\frac{\left(a\bmod c\right)i+\left(b\bmod c\right)}{c} \right\rfloor\\ &=\frac{n(n+1)}{2}\left\lfloor\frac{a}{c}\right\rfloor +(n+1)\left\lfloor\frac{b}{c}\right\rfloor+f(a\bmod c,b\bmod c,c,n) \end{aligned}

那么问题转化为了 a<c,b<ca<c,b<c 的情况。观察式子,你发现只有 ii 这一个变量。因此要推就只能从 ii 下手。在推求和式子中有一个常见的技巧,就是条件与贡献的放缩与转化。具体地说,在原式 f(a,b,c,n)=i=0nai+bc\displaystyle f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor 中,0in0\le i\le n 是条件,而 ai+bc\left\lfloor \dfrac{ai+b}{c} \right\rfloor 是对总和的贡献。

要加快一个和式的计算过程,所有的方法都可以归约为 贡献合并计算。但你发现这个式子的贡献难以合并,怎么办?将贡献与条件做转化 得到另一个形式的和式。具体地,我们直接把原式的贡献变成条件:

i=0nai+bc=i=0nj=0ai+bc11\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor =\sum_{i=0}^n\sum_{j=0}^{\left\lfloor \frac{ai+b}{c} \right\rfloor-1}1

现在多了一个变量 jj,既然算 ii 的贡献不方便,我们就想办法算 jj 的贡献。因此想办法搞一个和 jj 有关的贡献式。这里有另一个家喻户晓的变换方法,笔者概括为限制转移。具体来说,在上面的和式中 nn 限制 ii 的上界,而 ii 限制 jj 的上界。为了搞 jj,就先把 j 放到贡献的式子里,于是我们交换一下 i,ji,j 的求和算子,强制用 nn 限制 jj 的上界。

=j=0an+bc1i=0n[j<ai+bc]=\sum_{j=0}^{\left\lfloor \frac{an+b}{c} \right\rfloor-1}\sum_{i=0}^n\left[j<\left\lfloor \frac{ai+b}{c} \right\rfloor\right]

这样做的目的是让 jj 摆脱 ii 的限制,现在 i,ji,j 都被 nn 限制,而贡献式看上去是一个条件,但是我们仍把它叫作贡献式,再对贡献式做变换后就可以改变 i,ji,j 的限制关系。于是我们做一些放缩的处理。首先把向下取整的符号拿掉

j<ai+bc    j+1ai+bc    j+1ai+bcj<\left\lfloor \frac{ai+b}{c} \right\rfloor \iff j+1\leq \left\lfloor \frac{ai+b}{c} \right\rfloor \iff j+1\leq \frac{ai+b}{c}

然后可以做一些变换

j+1ai+bc    jc+cai+b    jc+cb1<aij+1\leq \frac{ai+b}{c} \iff jc+c\le ai+b \iff jc+c-b-1< ai

最后一步,向下取整得到:

jc+cb1<ai    jc+cb1a<ijc+c-b-1< ai\iff \left\lfloor\frac{jc+c-b-1}{a}\right\rfloor< i

这一步的重要意义在于,我们可以把变量 ii 消掉了!具体地,令 m=an+bcm=\left\lfloor \frac{an+b}{c} \right\rfloor,那么原式化为

f(a,b,c,n)=j=0m1i=0n[i>jc+cb1a]=j=0m1(njc+cb1a)=nmf(c,cb1,a,m1)\begin{aligned} f(a,b,c,n)&=\sum_{j=0}^{m-1} \sum_{i=0}^n\left[i>\left\lfloor\frac{jc+c-b-1}{a}\right\rfloor \right]\\ &=\sum_{j=0}^{m-1} (n-\left\lfloor\frac{jc+c-b-1}{a}\right\rfloor)\\ &=nm-f\left(c,c-b-1,a,m-1\right) \end{aligned}

这是一个递归的式子。并且你发现 a,ca,c 分子分母换了位置,又可以重复上述过程。先取模,再递归。这就是一个辗转相除的过程,这也是类欧几里德算法的得名。

容易发现时间复杂度为 O(logn)O(\log n)

扩展

理解了最基础的类欧几里德算法,我们再来思考以下两个变种求和式:

g(a,b,c,n)=i=0niai+bcg(a,b,c,n)=\sum_{i=0}^ni\left\lfloor \frac{ai+b}{c} \right\rfloor h(a,b,c,n)=i=0nai+bc2h(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor^2

推导 g

我们先考虑 gg,类似地,首先取模:

g(a,b,c,n)=g(amodc,bmodc,c,n)+acn(n+1)(2n+1)6+bcn(n+1)2g(a,b,c,n) =g(a\bmod c,b\bmod c,c,n)+\left\lfloor\frac{a}{c}\right\rfloor\frac{n(n+1)(2n+1)}{6}+\left\lfloor\frac{b}{c}\right\rfloor\frac{n(n+1)}{2}

接下来考虑 a<c,b<ca<c,b<c 的情况,令 m=an+bcm=\left\lfloor\frac{an+b}{c}\right\rfloor。之后的过程比较简略,因为方法和上文略同:

g(a,b,c,n)=i=0niai+bc=j=0m1i=0n[j<ai+bc]i\begin{aligned} g(a,b,c,n)&=\sum_{i=0}^ni\left\lfloor \frac{ai+b}{c} \right\rfloor\\ &=\sum_{j=0}^{m-1} \sum_{i=0}^n\left[j<\left\lfloor\frac{ai+b}{c}\right\rfloor\right]\cdot i \end{aligned}

这时我们设 t=jc+cb1at=\left\lfloor\frac{jc+c-b-1}{a}\right\rfloor,可以得到

g(a,b,c,n)=j=0m1i=0n[i>t]i=j=0m112(t+n+1)(nt)=12[mn(n+1)j=0m1t2j=0m1t]=12[mn(n+1)h(c,cb1,a,m1)f(c,cb1,a,m1)]\begin{aligned} g(a,b,c,n)&=\sum_{j=0}^{m-1}\sum_{i=0}^n[i>t]\cdot i\\ &=\sum_{j=0}^{m-1}\frac{1}{2}(t+n+1)(n-t)\\ &=\frac{1}{2}\left[mn(n+1)-\sum_{j=0}^{m-1}t^2-\sum_{j=0}^{m-1}t\right]\\ &=\frac{1}{2}[mn(n+1)-h(c,c-b-1,a,m-1)-f(c,c-b-1,a,m-1)] \end{aligned}

推导 h

同样的,首先取模:

h(a,b,c,n)=h(amodc,bmodc,c,n)+2bcf(amodc,bmodc,c,n)+2acg(amodc,bmodc,c,n)+ac2n(n+1)(2n+1)6+bc2(n+1)+acbcn(n+1)\begin{aligned} h(a,b,c,n)&=h(a\bmod c,b\bmod c,c,n)\\ &+2\left\lfloor\frac{b}{c}\right\rfloor f(a\bmod c,b\bmod c,c,n) +2\left\lfloor\frac{a}{c}\right\rfloor g(a\bmod c,b\bmod c,c,n)\\ &+\left\lfloor\frac{a}{c}\right\rfloor^2\frac{n(n+1)(2n+1)}{6}+\left\lfloor\frac{b}{c}\right\rfloor^2(n+1) +\left\lfloor\frac{a}{c}\right\rfloor\left\lfloor\frac{b}{c}\right\rfloor n(n+1) \end{aligned}

考虑 a<c,b<ca<c,b<c 的情况,m=an+bc,t=jc+cb1am=\left\lfloor\dfrac{an+b}{c}\right\rfloor, t=\left\lfloor\dfrac{jc+c-b-1}{a}\right\rfloor.

我们发现这个平方不太好处理,于是可以这样把它拆成两部分:

n2=2n(n+1)2n=(2i=0ni)nn^2=2\dfrac{n(n+1)}{2}-n=\left(2\sum_{i=0}^ni\right)-n

这样做的意义在于,添加变量 jj 的时侯就只会变成一个求和算子,不会出现 ×\sum\times \sum 的形式:

h(a,b,c,n)=i=0nai+bc2=i=0n[(2j=1ai+bcj)ai+bc]=(2i=0nj=1ai+bcj)f(a,b,c,n)\begin{aligned} h(a,b,c,n)&=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor^2\\ &=\sum_{i=0}^n\left[\left(2\sum_{j=1}^{\left\lfloor \frac{ai+b}{c} \right\rfloor}j \right)-\left\lfloor\frac{ai+b}{c}\right\rfloor\right]\\ &=\left(2\sum_{i=0}^n\sum_{j=1}^{\left\lfloor \frac{ai+b}{c} \right\rfloor}j\right) -f(a,b,c,n)\\ \end{aligned}

接下来考虑化简前一部分:

i=0nj=1ai+bcj=i=0nj=0ai+bc1(j+1)=j=0m1(j+1)i=0n[j<ai+bc]=j=0m1(j+1)i=0n[i>t]=j=0m1(j+1)(nt)=12nm(m+1)j=0m1(j+1)jc+cb1a=12nm(m+1)g(c,cb1,a,m1)f(c,cb1,a,m1)\begin{aligned} \sum_{i=0}^n\sum_{j=1}^{\left\lfloor \frac{ai+b}{c} \right\rfloor}j&=\sum_{i=0}^n\sum_{j=0}^{\left\lfloor \frac{ai+b}{c} \right\rfloor-1}(j+1)\\ &=\sum_{j=0}^{m-1}(j+1) \sum_{i=0}^n\left[j<\left\lfloor \frac{ai+b}{c} \right\rfloor\right]\\ &=\sum_{j=0}^{m-1}(j+1)\sum_{i=0}^n[i>t]\\ &=\sum_{j=0}^{m-1}(j+1)(n-t)\\ &=\frac{1}{2}nm(m+1)-\sum_{j=0}^{m-1}(j+1)\left\lfloor \frac{jc+c-b-1}{a} \right\rfloor\\ &=\frac{1}{2}nm(m+1)-g(c,c-b-1,a,m-1)-f(c,c-b-1,a,m-1) \end{aligned}

因此

h(a,b,c,n)=nm(m+1)2g(c,cb1,a,m1)2f(c,cb1,a,m1)f(a,b,c,n)h(a,b,c,n)=nm(m+1)-2g(c,c-b-1,a,m-1)-2f(c,c-b-1,a,m-1)-f(a,b,c,n)

在代码实现的时侯,因为 33 个函数各有交错递归,因此可以考虑三个一起整体递归,同步计算,否则有很多项会被多次计算。这样实现的复杂度是 O(logn)O(\log n) 的。

实现

#include <cstdio>
using namespace std;
constexpr long long P = 998244353;
long long i2 = 499122177, i6 = 166374059;

struct data_t {
data_t() { f = g = h = 0; }

long long f, g, h;
}; // 三个函数打包

data_t calc(long long n, long long a, long long b, long long c) {
long long ac = a / c, bc = b / c, m = (a * n + b) / c, n1 = n + 1,
n21 = n * 2 + 1;
data_t d;
if (a == 0) { // 迭代到最底层
d.f = bc * n1 % P;
d.g = bc * n % P * n1 % P * i2 % P;
d.h = bc * bc % P * n1 % P;
return d;
}
if (a >= c || b >= c) { // 取模
d.f = n * n1 % P * i2 % P * ac % P + bc * n1 % P;
d.g = ac * n % P * n1 % P * n21 % P * i6 % P + bc * n % P * n1 % P * i2 % P;
d.h = ac * ac % P * n % P * n1 % P * n21 % P * i6 % P +
bc * bc % P * n1 % P + ac * bc % P * n % P * n1 % P;
d.f %= P, d.g %= P, d.h %= P;

data_t e = calc(n, a % c, b % c, c); // 迭代

d.h += e.h + 2 * bc % P * e.f % P + 2 * ac % P * e.g % P;
d.g += e.g, d.f += e.f;
d.f %= P, d.g %= P, d.h %= P;
return d;
}
data_t e = calc(m - 1, c, c - b - 1, a);
d.f = n * m % P - e.f, d.f = (d.f % P + P) % P;
d.g = m * n % P * n1 % P - e.h - e.f, d.g = (d.g * i2 % P + P) % P;
d.h = n * m % P * (m + 1) % P - 2 * e.g - 2 * e.f - d.f;
d.h = (d.h % P + P) % P;
return d;
}

long long T, n, a, b, c;

signed main() {
scanf("%lld", &T);
while (T--) {
scanf("%lld%lld%lld%lld", &n, &a, &b, &c);
data_t ans = calc(n, a, b, c);
printf("%lld %lld %lld\n", ans.f, ans.h, ans.g);
}
return 0;
}