Unverified Commit 6aa2519d authored by FFjet's avatar FFjet Committed by GitHub
Browse files

Update euclidean-like.md

使语言更加严谨,修正了一些公式排版,使其更为正式。
parent d632929d
Loading
Loading
Loading
Loading
+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$ 取模以简化问题:

@@ -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
@@ -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}
@@ -83,7 +85,9 @@ $$

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

## 问题二
容易发现时间复杂度为 $\mathcal O(\log n)$ 。

## 扩展

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

@@ -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
@@ -150,7 +159,7 @@ $$
\end{split}
$$

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

$$
\begin{split}
@@ -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)
@@ -173,9 +182,9 @@ $$

## 模板与实现

在计算的时侯,因为 3 个函数各有交错递归,因此可以考虑三个一起整体递归来求,否则有很多项会被多次计算。或者采用记忆化
在计算的时侯,因为 $3$ 个函数各有交错递归,因此可以考虑三个一起整体递归,同步计算,否则有很多项会被多次计算。这样实现的复杂度是 $\mathcal O(\log n)$ 的

模板:Luogu5170
模板:[luogu5170](https://www.luogu.org/problemnew/show/P5170)

```cpp
#include <bits/stdc++.h>
@@ -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;
@@ -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);
@@ -225,4 +245,3 @@ signed main() {
	return 0;
}
```