Loading docs/math/euclidean-like.md +73 −54 Original line number Diff line number Diff line 类欧几里德算法由洪华敦在 2016 年冬令营营员交流中提出的内容,其本质可以理解为,使用一个类似辗转相除法来做函数求和的过程。我们用几个~喵~不可言的问题引入这个算法。 类欧几里德算法由洪华敦在 2016 年冬令营营员交流中提出的内容,其本质可以理解为,使用一个类似辗转相除法来做函数求和的过程。 ## 问题一 ## 引入 我们考虑一个可爱的求和式子: 设 $$ f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor $$ 其中 $a,b,c,n$ 是常数。这个式子和我们以前见过的式子都长得不太一样。带向下取整的式子容易让人想到数论分块,然而数论分块似乎不适用于这个求和。但是我们是可以做一些预处理的。 其中 $a,b,c,n$ 是常数。需要一个 $\mathcal O(\log n)$ 的算法。 这个式子和我们以前见过的式子都长得不太一样。带向下取整的式子容易让人想到数论分块,然而数论分块似乎不适用于这个求和。但是我们是可以做一些预处理的。 如果说 $a\ge c$ 或者 $b\ge c$,意味着可以将 $a,b$ 对 $c$ 取模以简化问题: Loading @@ -31,9 +33,9 @@ f(a,b,c,n)&=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor\\ \end{split} $$ 那么问题转化为了 $a<c,b<c$ 的情况。观察式子,你发现只有 $i$ 这一个变量。因此要推就只能从 $i$ 下手。在推求和式子中有一个常见的技巧,就是条件与贡献的放缩与转化。具体地说,在原式 $f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor$ 中,$0\le i\le n$ 是条件,而 $\left\lfloor \frac{ai+b}{c} \right\rfloor$ 是对总和的贡献。 那么问题转化为了 $a<c,b<c$ 的情况。观察式子,你发现只有 $i$ 这一个变量。因此要推就只能从 $i$ 下手。在推求和式子中有一个常见的技巧,就是条件与贡献的放缩与转化。具体地说,在原式 $\displaystyle f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor$ 中,$0\le i\le n$ 是条件,而 $\left\lfloor \dfrac{ai+b}{c} \right\rfloor$ 是对总和的贡献。 要加快一个和式的计算过程,所有的方法都可以归约为**贡献合并计算**。但你发现这个式子的贡献难以合并,怎么办?**将贡献与条件做转化**得到另一个形式的和式。具体地说,我们直接把原式的贡献变成条件: 要加快一个和式的计算过程,所有的方法都可以归约为**贡献合并计算**。但你发现这个式子的贡献难以合并,怎么办?**将贡献与条件做转化**得到另一个形式的和式。具体地,我们直接把原式的贡献变成条件: $$ \sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor Loading Loading @@ -69,7 +71,7 @@ $$ jc+c-b-1< ai\Leftrightarrow \left\lfloor\frac{jc+c-b-1}{a}\right\rfloor< i $$ 你发现你把 $i$ 拿出来,把 $j$ 丢进去了。于是就可以把变量 $i$ 消掉了!具体地,令 $m=\left\lfloor \frac{an+b}{c} \right\rfloor$,那么原式化为 这一步的重要意义在于,我们可以把变量 $i$ 消掉了!具体地,令 $m=\left\lfloor \frac{an+b}{c} \right\rfloor$,那么原式化为 $$ \begin{split} Loading @@ -83,7 +85,9 @@ $$ 这是一个递归的式子。并且你发现 $a,c$ 分子分母换了位置,又可以重复上述过程。先取模,再递归。这就是一个辗转相除的过程,这也是类欧几里德算法的得名。 ## 问题二 容易发现时间复杂度为 $\mathcal O(\log n)$ 。 ## 扩展 理解了最基础的类欧几里德算法,我们再来思考以下两个变种求和式: Loading Loading @@ -137,10 +141,15 @@ h(a,b,c,n)&=h(a\bmod c,b\bmod c,c,n)\\ \end{split} $$ 考虑 $a<c,b<c$ 的情况, $m=\left\lfloor\frac{an+b}{c}\right\rfloor, t=\left\lfloor\frac{jc+c-b-1}{a}\right\rfloor$. 考虑 $a<c,b<c$ 的情况, $m=\left\lfloor\dfrac{an+b}{c}\right\rfloor, t=\left\lfloor\dfrac{jc+c-b-1}{a}\right\rfloor$. 这样用到一个技巧。我们先把 $n^2$ 拆一下:$n^2=2\frac{n(n+1)}{2}-n=\left(2\sum_{i=0}^ni\right)-n$. 这样在添加变量 $j$ 的时侯就只会变成一个求和算子,不会出现 $(\sum)\times (\sum)$ 的情况: 我们发现这个平方不太好处理,于是可以这样把它拆成两部分: $$ n^2=2\dfrac{n(n+1)}{2}-n=\left(2\sum_{i=0}^ni\right)-n $$ 这样做的意义在于,添加变量 $j$ 的时侯就只会变成一个求和算子,不会出现 $\sum\times \sum$ 的形式: $$ \begin{split} &h(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor^2 Loading @@ -150,7 +159,7 @@ $$ \end{split} $$ 接下来化一下和式 接下来考虑化简前一部分: $$ \begin{split} Loading @@ -165,7 +174,7 @@ $$ \end{split} $$ 因此原式即为 因此 $$ 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) Loading @@ -173,9 +182,9 @@ $$ ## 模板与实现 在计算的时侯,因为 3 个函数各有交错递归,因此可以考虑三个一起整体递归来求,否则有很多项会被多次计算。或者采用记忆化。 在计算的时侯,因为 $3$ 个函数各有交错递归,因此可以考虑三个一起整体递归,同步计算,否则有很多项会被多次计算。这样实现的复杂度是 $\mathcal O(\log n)$ 的。 模板:Luogu5170 模板:[luogu5170](https://www.luogu.org/problemnew/show/P5170) ```cpp #include <bits/stdc++.h> Loading @@ -183,19 +192,27 @@ $$ using namespace std; const int P = 998244353; int i2 = 499122177, i6 = 166374059; struct data{data(){f=g=h=0;} struct data { data() { f = g = h = 0; } int f, g, h; };// 三个函数打包 data calc(int n,int a,int b,int c) { data calc(int n, int a, int b, int c) { int ac = a / c, bc = b / c, m = (a * n + b) / c, n1 = n + 1, n21 = n * 2 + 1; data d; if(a==0){// 迭代到最底层 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){// 取模 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; Loading @@ -211,13 +228,16 @@ data calc(int n,int a,int b,int c) { data 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; 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; } int T, n, a, b, c; signed main() { signed main() { scanf("%lld", &T); while(T--) { while(T--) { scanf("%lld%lld%lld%lld", &n, &a, &b, &c); data ans = calc(n, a, b, c); printf("%lld %lld %lld\n", ans.f, ans.h, ans.g); Loading @@ -225,4 +245,3 @@ signed main() { return 0; } ``` Loading
docs/math/euclidean-like.md +73 −54 Original line number Diff line number Diff line 类欧几里德算法由洪华敦在 2016 年冬令营营员交流中提出的内容,其本质可以理解为,使用一个类似辗转相除法来做函数求和的过程。我们用几个~喵~不可言的问题引入这个算法。 类欧几里德算法由洪华敦在 2016 年冬令营营员交流中提出的内容,其本质可以理解为,使用一个类似辗转相除法来做函数求和的过程。 ## 问题一 ## 引入 我们考虑一个可爱的求和式子: 设 $$ f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor $$ 其中 $a,b,c,n$ 是常数。这个式子和我们以前见过的式子都长得不太一样。带向下取整的式子容易让人想到数论分块,然而数论分块似乎不适用于这个求和。但是我们是可以做一些预处理的。 其中 $a,b,c,n$ 是常数。需要一个 $\mathcal O(\log n)$ 的算法。 这个式子和我们以前见过的式子都长得不太一样。带向下取整的式子容易让人想到数论分块,然而数论分块似乎不适用于这个求和。但是我们是可以做一些预处理的。 如果说 $a\ge c$ 或者 $b\ge c$,意味着可以将 $a,b$ 对 $c$ 取模以简化问题: Loading @@ -31,9 +33,9 @@ f(a,b,c,n)&=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor\\ \end{split} $$ 那么问题转化为了 $a<c,b<c$ 的情况。观察式子,你发现只有 $i$ 这一个变量。因此要推就只能从 $i$ 下手。在推求和式子中有一个常见的技巧,就是条件与贡献的放缩与转化。具体地说,在原式 $f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor$ 中,$0\le i\le n$ 是条件,而 $\left\lfloor \frac{ai+b}{c} \right\rfloor$ 是对总和的贡献。 那么问题转化为了 $a<c,b<c$ 的情况。观察式子,你发现只有 $i$ 这一个变量。因此要推就只能从 $i$ 下手。在推求和式子中有一个常见的技巧,就是条件与贡献的放缩与转化。具体地说,在原式 $\displaystyle f(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor$ 中,$0\le i\le n$ 是条件,而 $\left\lfloor \dfrac{ai+b}{c} \right\rfloor$ 是对总和的贡献。 要加快一个和式的计算过程,所有的方法都可以归约为**贡献合并计算**。但你发现这个式子的贡献难以合并,怎么办?**将贡献与条件做转化**得到另一个形式的和式。具体地说,我们直接把原式的贡献变成条件: 要加快一个和式的计算过程,所有的方法都可以归约为**贡献合并计算**。但你发现这个式子的贡献难以合并,怎么办?**将贡献与条件做转化**得到另一个形式的和式。具体地,我们直接把原式的贡献变成条件: $$ \sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor Loading Loading @@ -69,7 +71,7 @@ $$ jc+c-b-1< ai\Leftrightarrow \left\lfloor\frac{jc+c-b-1}{a}\right\rfloor< i $$ 你发现你把 $i$ 拿出来,把 $j$ 丢进去了。于是就可以把变量 $i$ 消掉了!具体地,令 $m=\left\lfloor \frac{an+b}{c} \right\rfloor$,那么原式化为 这一步的重要意义在于,我们可以把变量 $i$ 消掉了!具体地,令 $m=\left\lfloor \frac{an+b}{c} \right\rfloor$,那么原式化为 $$ \begin{split} Loading @@ -83,7 +85,9 @@ $$ 这是一个递归的式子。并且你发现 $a,c$ 分子分母换了位置,又可以重复上述过程。先取模,再递归。这就是一个辗转相除的过程,这也是类欧几里德算法的得名。 ## 问题二 容易发现时间复杂度为 $\mathcal O(\log n)$ 。 ## 扩展 理解了最基础的类欧几里德算法,我们再来思考以下两个变种求和式: Loading Loading @@ -137,10 +141,15 @@ h(a,b,c,n)&=h(a\bmod c,b\bmod c,c,n)\\ \end{split} $$ 考虑 $a<c,b<c$ 的情况, $m=\left\lfloor\frac{an+b}{c}\right\rfloor, t=\left\lfloor\frac{jc+c-b-1}{a}\right\rfloor$. 考虑 $a<c,b<c$ 的情况, $m=\left\lfloor\dfrac{an+b}{c}\right\rfloor, t=\left\lfloor\dfrac{jc+c-b-1}{a}\right\rfloor$. 这样用到一个技巧。我们先把 $n^2$ 拆一下:$n^2=2\frac{n(n+1)}{2}-n=\left(2\sum_{i=0}^ni\right)-n$. 这样在添加变量 $j$ 的时侯就只会变成一个求和算子,不会出现 $(\sum)\times (\sum)$ 的情况: 我们发现这个平方不太好处理,于是可以这样把它拆成两部分: $$ n^2=2\dfrac{n(n+1)}{2}-n=\left(2\sum_{i=0}^ni\right)-n $$ 这样做的意义在于,添加变量 $j$ 的时侯就只会变成一个求和算子,不会出现 $\sum\times \sum$ 的形式: $$ \begin{split} &h(a,b,c,n)=\sum_{i=0}^n\left\lfloor \frac{ai+b}{c} \right\rfloor^2 Loading @@ -150,7 +159,7 @@ $$ \end{split} $$ 接下来化一下和式 接下来考虑化简前一部分: $$ \begin{split} Loading @@ -165,7 +174,7 @@ $$ \end{split} $$ 因此原式即为 因此 $$ 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) Loading @@ -173,9 +182,9 @@ $$ ## 模板与实现 在计算的时侯,因为 3 个函数各有交错递归,因此可以考虑三个一起整体递归来求,否则有很多项会被多次计算。或者采用记忆化。 在计算的时侯,因为 $3$ 个函数各有交错递归,因此可以考虑三个一起整体递归,同步计算,否则有很多项会被多次计算。这样实现的复杂度是 $\mathcal O(\log n)$ 的。 模板:Luogu5170 模板:[luogu5170](https://www.luogu.org/problemnew/show/P5170) ```cpp #include <bits/stdc++.h> Loading @@ -183,19 +192,27 @@ $$ using namespace std; const int P = 998244353; int i2 = 499122177, i6 = 166374059; struct data{data(){f=g=h=0;} struct data { data() { f = g = h = 0; } int f, g, h; };// 三个函数打包 data calc(int n,int a,int b,int c) { data calc(int n, int a, int b, int c) { int ac = a / c, bc = b / c, m = (a * n + b) / c, n1 = n + 1, n21 = n * 2 + 1; data d; if(a==0){// 迭代到最底层 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){// 取模 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; Loading @@ -211,13 +228,16 @@ data calc(int n,int a,int b,int c) { data 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; 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; } int T, n, a, b, c; signed main() { signed main() { scanf("%lld", &T); while(T--) { while(T--) { scanf("%lld%lld%lld%lld", &n, &a, &b, &c); data ans = calc(n, a, b, c); printf("%lld %lld %lld\n", ans.f, ans.h, ans.g); Loading @@ -225,4 +245,3 @@ signed main() { return 0; } ```