【算法学习笔记】类欧几里得算法

一个基础的数论问题。

试求 i=0nai+bc 的值,其中:a,b0n,c>0

在Atcoder的AC库中有这样一个函数可以在 O(lg(n+c+a+b)) 的时间内解决问题。

函数代码 ↓

using ll = long long;
ll floor_sum(ll n, ll m, ll a, ll b) {
    ll ans = 0;
    if (a >= m) {
        ans += (n - 1) * n * (a / m) / 2;
        a %= m;
    }
    if (b >= m) {
        ans += n * (b / m);
        b %= m;
    }

    ll y_max = (a * n + b) / m, x_max = (y_max * m - b);
    if (y_max == 0) return ans;
    ans += (n - (x_max + a - 1) / a) * y_max;
    ans += floor_sum(y_max, a, m, (a - x_max % a) % a);
    return ans;
}

好奇它的证明过程,然后在 OI wiki 上找到相应文档,这个算法名为:类欧几里德算法

个人证明

另外补上个人证明:

acbc 时,

f(a,b,c,n)=n(n+1)2ac+(n+1)bc+f(a mod c,b mod c,c,n).

a<c 并且 b<c 时,

m=an+bc

f(a,b,c,n)=mnf(c,cb1,a,m1).

然后递归至 a=0 即可.

例题

luoguP5171 Earthquake

数形结合, 把式子稍微简单转换一下, 套用类欧几里得算法即可.

引入

f(a,b,c,n)=i=0nai+bc

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

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

如果说 ac 或者 bc,意味着可以将 a,bc 取模以简化问题:

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)

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

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

i=0nai+bc=i=0nj=0ai+bc11

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

=j=0an+bc1i=0n[j<ai+bc]

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

j<ai+bcj+1ai+bcj+1ai+bc

然后可以做一些变换

j+1ai+bcjc+cai+bjc+cb1<ai

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

jc+cb1<aijc+cb1a<i

这一步的重要意义在于,我们可以把变量 i 消掉了!具体地,令 m=an+bc,那么原式化为

f(a,b,c,n)=j=0m1i=0n[i>jc+cb1a]=j=0m1njc+cb1a=nmf(c,cb1,a,m1)

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

容易发现时间复杂度为 O(lg(n+m+a+b))

同时关于 类欧几里德算法 有两个函数的拓展

扩展

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

g(a,b,c,n)=i=0niai+bch(a,b,c,n)=i=0nai+bc2

推导 g

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

g(a,b,c,n)=g(amodc,bmodc,c,n)+acn(n+1)(2n+1)6+bcn(n+1)2

接下来考虑 a<c,b<c 的情况,令 m=an+bc。之后的过程我会写得很简略,因为方法和上文略同:

g(a,b,c,n)=i=0niai+bc=j=0m1i=0n[j<ai+bc]i

这时我们设 t=jc+cb1a,可以得到

=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)]

推导 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)

考虑 a<c,b<c 的情况,m=an+bc,t=jc+cb1a.

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

n2=2n(n+1)2n=(2i=0ni)n

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

h(a,b,c,n)=i=0nai+bc2=i=0n[(2j=1ai+bcj)ai+bc]=(2i=0nj=1ai+bcj)f(a,b,c,n)

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

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)

因此

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

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

模板代码实现




posted @   RioTian  阅读(638)  评论(0编辑  收藏  举报
编辑推荐:
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
阅读排行:
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
· 5. Nginx 负载均衡配置案例(附有详细截图说明++)
历史上的今天:
2020-03-26 PTA | 06-图3 六度空间 (30分)
2020-03-26 PTA | 06-图1 列出连通集 (25分)
点击右上角即可分享
微信分享提示

📖目录