万能欧几里得 学习笔记

模拟赛又双叒叕被卡科技了,学个技能防身/yun(tzc 附体)


考虑这样的问题:将平面内所有 \(x=c\)\(y=c\)(其中 \(c\in\Z\))的直线标记出来。考虑一条直线 \(y=\dfrac{px+r}q\)(其中 \(p,r\in\N,q\in\N_+\),而作为 \(r\),模 \(q\) 的同余类显然是等价的,所以还可以保证 \(r<q\)),令这条直线上的一个动点在横坐标区间 \((0,L]\)\(L\in\N\))内从左往右移动,每次遇到竖线就写下一个字符 R,遇到横线就写 U(如果同时遇到,即遇到整点,规定先 UR),最终得到一个 RU 串。再考虑一个幺半群(你可以自己定义)\(\mathbb G\),里面有两个给定的特殊的元素 \(R,U\in\mathbb G\),希望快速求出这个 RU 串的值。事实证明,很多类似 \(\sum\limits_{i=1}^Lf\!\left(i,\left\lfloor\dfrac{pi+r}{q}\right\rfloor\right)\) 的和式求值问题都可以归约到这个问题。

思路是类似欧几里得算法的递归:若 \(p\geq q\) 则令 \(p\gets p\bmod q\),否则交换 \(p,q\)\(p\geq q\) 的情况(相对)比较简单。注意到当横坐标增大 \(1\) 个单位时,纵坐标增大 \(\dfrac pq\) 个单位,在这个矩形内显然恰好会有一个 R,在该 R 之前会有 \(\left\lfloor\dfrac pq\right\rfloor\sim\left\lfloor\dfrac pq\right\rfloor+1\)U。于是我们考虑将这至少的 \(\left\lfloor\dfrac pq\right\rfloor\)U 砍掉,那么就相当于白送了这么多个 U 然后把纵坐标的步长改为 \(\left\{\dfrac pq\right\}\)(表示小数部分)。这也就相当于令 \(R\gets L^{\left\lfloor\frac pq\right\rfloor}R\) 并令 \(p\gets p\bmod q\)(想一想,为什么)

接下来考虑第二部分,交换 \(p,q\)\(R,U\) 的地位(细节比较毒瘤)。如果直接关于 \(y=x\) 翻转坐标系,那么会出现三个问题:

  1. 由于 \(r\geq 0\),我们递归到的问题的直线在第一象限中必须要与纵轴有交点,而直接翻转会变成和横轴有交点。
  2. 翻转之后 \(L\) 不是整数了。
  3. 原本遇到整点先 UR,翻转之后就变成先 RU 了。

我们一个个来解决这些问题。先看第一个,考虑找到第一个遇到的 U(也就是交于直线 \(y=1\) 处),先算上在这之前的贡献,然后砍掉纵坐标区间 \((0,1]\),再翻转上面的区域,截距就为正了。前面的贡献是多少?此处我们算出第 \(b\)U 之前有多少个 R,一劳永逸。

答案显然是最大的整数 \(x\) 使 \(\left\lfloor\dfrac{px+r}q\right\rfloor<b\)。由于 \(b\) 是整数,即 \(\dfrac{px+r}q<b\)\(x<\dfrac{qb-r}p\),即 \(x\leq\dfrac{qb-r-1}p\)。所以答案就是 \(\left\lfloor\dfrac{qb-r-1}p\right\rfloor\)

继续上面的讨论。带入 \(b=1\),前面的贡献显然是 \(R^{\left\lfloor\frac{q-r-1}p\right\rfloor}U\)(由于 \(p<q\),显然不可能在此处遇到整点,故无需担心后面还有一个 \(R\))。而砍掉下面翻转上面后,新的直线解析式的 \(p,q\) 交换,截距是多少?就是原直线在 \(y=1\) 的交点横坐标,容易解出为 \(\dfrac{q-r}p\),故新的 \(r\)\(q-r\)

考虑第二个问题。解决方案是找到最后一个 U,对其下方的区域进行递归,上面的贡献特殊考虑。显然一共有 \(L_0=\left\lfloor\dfrac{pL+r}{q}\right\rfloor\)U(此时其实注意到解决第一个问题时没有考虑无 U 的情况,现在可以通过 \(L_0=0\) 判断,如果确实无 U 那显然返回 \(R^L\)),并且之前砍掉了一个 U,所以新的 \(L\) 等于 \(L_0-1\)。上面的贡献是多少?显然只有 R,总共有 \(L\)R,减去最后一个 U 前面的 R 数量 \(\left\lfloor\dfrac{qL_0-r-1}p\right\rfloor\)(通过带入 \(b=L_0\) 得到),所以贡献是 \(R^{L-\left\lfloor\frac{qL_0-r-1}{p}\right\rfloor}\)

最后一个问题:如何处理翻转之后整点处顺序变了?方法非常巧妙:只要令 \(r\gets r-1\) 即可。这样直线往下移了一点点,原来的整点 \(px\equiv 0\pmod q\) 处确实变成先 RU 了,而原来的 \(px\equiv 1\pmod q\) 变成了现在的整点,但人家本来就是先 UR。所以新的 \(r\) 其实等于旧的 \(q-r-1\),最后为了保证 \(r\) 在模意义范围内,还要 \(\bmod p\)

到此为止这个算法就讲完啦,分析一下复杂度。跟 gcd 一样,每两次令 \(p\gets p\bmod q\),分 \(q\leq\dfrac p2\) 与否两种情况讨论发现都有 \(p\) 至少减半,所以递归次数是 \(\mathrm O(\log(p+q))\)。但里面有快速幂,这样就是 2log 吗?错。首先 \(L\) 不增,看作常数。那么每次快速幂的指数都是 \(\mathrm O\!\left(\dfrac qp\right)\),其 log 便为 \(\mathrm O(\log q-\log p)\)。下一次快速幂时,\(q\) 变为了上次的 \(p\)\(p\) 变为了更小的 \(q'\),于是总复杂度加起来就是 \(\log q-\log p+\log p-\log q'+\log q'-\cdots=\mathrm O(\log q)\),还是一个 log 的(很巧妙啊很巧妙)。所以总复杂度就是 \(\mathrm O(T(\log(p+q)+\log L))\),其中 \(T\) 是幺半群 \(\mathbb G\) 一次乘法的复杂度。

code
opr euclid(int p, int q, int r, int L, opr U, opr R) {
	if(p >= q) {
		return euclid(p % q, q, r, L, U, (U ^ p / q) * R);
	} else {
		int L0 = (p * L + r) / q;
		if(!L0) return R ^ L;
		opr res = (R ^ ((q - r - 1) / p)) * U;
		res = res * euclid(q, p, (q - r - 1) % p, L0 - 1, R, U);
		res = res * (R ^ (L - (L0 * q - r - 1) / p));
		return res;
	}
}

例题:P5170 - 【模板】类欧几里得算法

用万欧水类欧,好!

我们只需要自定义幺半群 \(\mathbb G\),其它的交给万欧板子就好。

这个地方的标准做法应该是开一个结构体记录答案和转移要用的 \(\Delta\),只需要 6 个变量,但是我做的时候没想到,使用了最无脑的做法——记录操作,使用了 11 个变量(好劣啊)……那就讲一下我的方法吧,反正标准方法在下一道例题里会提到。

考虑记录一个结构体,里面描述一个操作集合。设当前横坐标下取整为 \(I\),纵坐标下取整为 \(V\),三问的答案分别为 \(A,B,C\),考虑记录 \(A,B,C\) 分别加上关于 \(I,V\) 的一个什么东西,然后 \(I,V\) 分别加上多少。那么是加上什么东西呢?初始情况(即 \(R,U\) 内部)显然是直接加上 \(V,V^2,IV,1,1\), 但这样合并不满足封闭性。经尝试,如果我们记录 \(A,B,C\) 分别加上的是 \(V\) 的一次函数、\(V\) 的二次函数、\(I,V\) 的多项式,那么是封闭的。于是我们设操作集合为(按顺序地):

  1. \(A\gets A+aV+b\)
  2. \(B\gets B+cV^2+dV+e\)
  3. \(C\gets C+fVI+gV+hI+i\)
  4. \(V\gets V+j\)
  5. \(I\gets I+k\)

记录 \(a\sim k\) 这 11 个量。合并就不说了!!因为既简单又复杂(矛 盾 文 学 奖)。\(U\)\(j=1\),其余量为 \(0\)\(R\)\(a = c = f = g = k = 1\),其余量为 \(0\)。注意到题面里 \(i\)\(0\) 开始,所以前两问需要加上 \(i=0\) 的贡献。以及万欧算法需要 \(r<q\),而初始没有保证,所以要在前面乘上 \(U^{\left\lfloor\frac rq\right\rfloor}\),然后令 \(r\gets r\bmod q\)。最后计算答案时,由于初始时 \(I=V=0\),所以只要取常数项 \(b,e,i\)

code
struct opr {
	int a, b;
	int c, d, e; 
	int f, g, h, i;
	int j, k; 
	opr(int o = 0) {
		memset(this, 0, sizeof(*this));
		if(o == 1) j = 1;
		else if(o == 2) a = c = f = g = k = 1;
	}
	void mod() {
		a %= P, b %= P, c %= P, d %= P, e %= P, f %= P, g %= P, h %= P, i %= P, j %= P, k %= P;
	}
	friend opr operator*(opr x, opr y) {
		opr z;
		z.a = x.a + y.a;
		z.b = x.b + y.a * x.j + y.b;
		z.c = x.c + y.c;
		z.d = x.d + 2 * y.c * x.j + y.d;
		z.e = x.e + y.c * x.j % P * x.j + y.d * x.j + y.e;
		z.f = x.f + y.f;
		z.g = x.g + y.f * x.k + y.g;
		z.h = x.h + y.f * x.j + y.h;
		z.i = x.i + y.f * x.j % P * x.k + y.g * x.j + y.h * x.k + y.i;
		z.j = x.j + y.j;
		z.k = x.k + y.k;
		z.mod();
		return z;
	}
	friend opr operator^(opr x, int y) {
		opr res;
		while(y) {
			if(y & 1) res = res * x;
			x = x * x;
			y >>= 1;
		} return res;
	}
};

opr euclid(int p, int q, int r, int L, opr U, opr R) {
	if(p >= q) {
		return euclid(p % q, q, r, L, U, (U ^ p / q) * R);
	} else {
		int L0 = (p * L + r) / q;
		if(!L0) return R ^ L;
		opr res = (R ^ ((q - r - 1) / p)) * U;
		res = res * euclid(q, p, (q - r - 1) % p, L0 - 1, R, U);
		res = res * (R ^ (L - (L0 * q - r - 1) / p));
		return res;
	}
}

void mian() {
	int n = read(), p = read(), r = read(), q = read();
	opr res = (opr(1) ^ r / q) * euclid(p, q, r % q, n, opr(1), opr(2));
	prt(add(res.b, r / q % P)), pc(' '), prt(add(res.e, (r / q) * (r / q) % P)), pc(' '), prt(res.i), pc('\n');
}

例题:loj6440 - 万能欧几里得

loj 上的模板题。(终于有属于我们万欧自己的模板题啦!!!)

考虑在结构体里维护答案:即维护假装是从一开始就执行该 RU 串得到的答案 \(ans\)。那么考虑两个 RU 串合并,为了算出合并后的 \(ans\) 还需要什么信息。那显然新的 \(ans\) 等于左边串的 \(ans\) 加上经过左边的 RU 作用后的右边串的 \(ans\)。幸运的是,后者是可求的,设左边有 \(i\)\(R\)\(j\)\(U\),那么 \(\sum\limits_{i=1}^kA^{f(i)}B^{g(i)}\) 经过这些作用之后就是 \(A^i\!\left(\sum\limits_{d=1}^kA^{f(d)}B^{g(d)}\right)B^j\)。于是我们只要再维护 RU 串执行完毕后的 \(A^i\)\(B^j\) 的值即可,总共维护三个矩阵。

\(R\)\(A^i=A,B^j=I,ans=A\)\(U\)\(A^i=I,B^j=B,ans=O\),依然要在之前乘上 \(U^{\left\lfloor\frac rq\right\rfloor}\) 然后令 \(r\gets r\bmod q\)。时间复杂度 \(\mathrm O\!\left(n^3\log\right)\)。有两个细节:一是 ll 乘以 ll 需要 i128(虽然运算结果不会爆,但是过程会爆);以及 1e9 超过了 998244353 需要手动取模/yun

code
constexpr int N = 30;

int n;
struct matrix {
	int a[N][N];
	matrix(int x = 0) {
		memset(a, 0, sizeof(a));
		REP(i, 1, n) a[i][i] = x;
	}
	int *operator[](int x) { return a[x]; }
	friend matrix operator*(matrix f, matrix g) {
		matrix h;
		REP(i, 1, n) REP(j, 1, n) REP(k, 1, n) addto(h[i][j], f[i][k] * g[k][j] % P);
		return h;
	}
	friend matrix operator+(matrix f, matrix g) {
		matrix h;
		REP(i, 1, n) REP(j, 1, n) h[i][j] = add(f[i][j], g[i][j]);
		return h;
	}
	void read() {
		REP(i, 1, n) REP(j, 1, n) a[i][j] = ::read();
	}
	void prt() {
		REP(i, 1, n) REP(j, 1, n) ::prt(a[i][j]), pc(" \n"[j == n]);
	}
} A, B;

struct opr {
	matrix ans, Ai, Bj;
	opr(int o = 0) {
		ans = matrix(), Ai = Bj = matrix(1);
		if(o == 1) Bj = B;
		else if(o == 2) Ai = ans = A;
	}
	friend opr operator*(opr x, opr y) {
		opr z;
		z.ans = x.ans + x.Ai * y.ans * x.Bj;
		z.Ai = x.Ai * y.Ai;
		z.Bj = x.Bj * y.Bj;
		return z;
	}
	friend opr operator^(opr x, int y) {
		opr res;
		while(y) {
			if(y & 1) res = res * x;
			x = x * x;
			y >>= 1;
		} return res;
	}
};

opr euclid(int p, int q, int r, int L, opr U, opr R) {
//	debug(mt(p, q, r, L));
	if(p >= q) {
		return euclid(p % q, q, r, L, U, (U ^ p / q) * R);
	} else {
		int L0 = ((i128)p * L + r) / q;
		if(!L0) return R ^ L;
		opr res = (R ^ ((q - r - 1) / p)) * U;
		res = res * euclid(q, p, (q - r - 1) % p, L0 - 1, R, U);
		res = res * (R ^ (L - ((i128)L0 * q - r - 1) / p));
		return res;
	}
}

void mian() {
	int p = read(), q = read(), r = read(), L = read(); n = read();
	A.read(), B.read();
	opr res = (opr(1) ^ r / q) * euclid(p, q, r % q, L, opr(1), opr(2));
	res.ans.prt();
}

例题:loj138 - 类欧几里得算法

万欧,好😍;类欧,不好😫。

这道题的类欧做法好像要推不少式子,然后还要插值,最终复杂度是 \(k^4\)。而用万欧做脑子都不用动,复杂度也一样,常数应该也更小!我们万欧真是太厉害啦😋。

考虑类似洛谷模板题那样维护三个操作,分别表示 \(x\) 加多少,\(f(x)\) 加多少,和答案加多少。答案加的东西怎么定义才能封闭呢?不难发现,定义成 \(x\)\(f(x)\) 的多项式就好,共 \((k_1+1)(k_2+1)\) 项。于是我们维护操作:

  1. \(ans\gets ans+\sum\limits_{i=0}^{k_1}\sum\limits_{j=0}^{k_2}a_{i,j}\)
  2. \(x\gets x+I\)
  3. \(f(x)\gets f(x)+V\)

\(U\)\(V=1\)\(R\) 不仅 \(I=1\),还由于 \((x+1)^{k_1}\) 的二项式展开,有 \(a_{i,k_2}=\dbinom{k_1}{i}\)。如何合并?只讲对系数矩阵 \(a\) 的合并。肯定要累加上左元的矩阵,接下来设 \(I,V\) 为左元的对应项,\(a\) 为右元的 \(a\),那么 \((x+I)^i(f(x)+V)^j\) 对合并后的 \(a\) 的贡献你可以把这两个多项式二项式展开,然后对应项系数乘起来再乘以 \(a_{i,j}\) 累加到对应位置。这样复杂度就是四方的(\(\mathrm O\!\left(Tk^4\log\right)\)),而且跑不满。

code
constexpr int N = 12;

comb_init(N);

int k1, k2;

struct opr {
	int a[N][N], I, V;
	int *operator[](int x) { return a[x]; }
	opr(int o = 0) {
		memset(this, 0, sizeof(*this));
		if(o == 1) V = 1;
		else if(o == 2) {
			I = 1;
			REP(i, 0, k1) a[i][k2] = comb[k1][i];
		}
	}
	friend opr operator*(opr f, opr g) {
		opr h;
		h.I = add(f.I, g.I);
		h.V = add(f.V, g.V);
		REP(i, 0, k1) REP(j, 0, k2) h[i][j] = f[i][j];
		REP(i, 0, k1) REP(j, 0, k2) {
			int l[N], r[N];
			int now = 1;
			REP(k, 0, i) l[i - k] = comb[i][k] * now % P * g[i][j] % P, now = now * f.I % P;
			now = 1;
			REP(k, 0, j) r[j - k] = comb[j][k] * now % P, now = now * f.V % P;
			REP(k, 0, i) REP(o, 0, j) addto(h[k][o], l[k] * r[o] % P);
		}
		return h;
	}
	friend opr operator^(opr x, int y) {
		opr res;
		while(y) {
			if(y & 1) res = res * x;
			x = x * x;
			y >>= 1;
		} return res;
	}
};

opr euclid(int p, int q, int r, int L, opr U, opr R) {
//	debug(mt(p, q, r, L));
	if(p >= q) {
		return euclid(p % q, q, r, L, U, (U ^ p / q) * R);
	} else {
		int L0 = (p * L + r) / q;
		if(!L0) return R ^ L;
		opr res = (R ^ ((q - r - 1) / p)) * U;
		res = res * euclid(q, p, (q - r - 1) % p, L0 - 1, R, U);
		res = res * (R ^ (L - (L0 * q - r - 1) / p));
		return res;
	}
}

void mian() {
	int L = read(), p = read(), r = read(), q = read(); k1 = read(), k2 = read();
	opr res = (opr(1) ^ r / q) * euclid(p, q, r % q, L, opr(1), opr(2));
	int ans = res[0][0];
	addto(ans, qpow(0, k1) * qpow(r / q, k2));
	prt(ans), pc('\n');
}
posted @ 2022-02-24 16:01  ycx060617  阅读(185)  评论(0编辑  收藏  举报