常见思想——倍增

倍增真的好难理解啊WSL

No.1 倍增RMQ:P3865 【模板】ST表

题目描述

给定一个长度为\(N\)的数列,和\(M\)次询问,求出每一次询问的区间内数字的最大值。

输入格式

第一行包含两个整数\(N,M\),分别表示数列的长度和询问的个数。

第二行包含\(N\)个整数\((\)记为 \(a_i)\),依次表示数列的第\(i\)项。

接下来\(M\)行,每行包含两个整数\(l_i,r_i\),表示查询的区间为 $[ l_i, r_i] $

\(1≤N≤10^5,1≤M≤10^6,a_i∈[0,10^9],1≤l_i≤r_i≤N\)

输出格式

输出包含\(M\)行,每行一个整数,依次表示每一次询问的结果。

解法

  1. 线段树

emmmm,数据范围,请.

  1. ST表(重头戏)

倍增思想的重要应用,我们设\(f[i][j]\)表示\(i\)开始,连续\(2^j\)个数中的最大(小)值.(我是真的难转过这个弯来,这样我们就可以发现一些事情:

  1. \(f[i][0]=a[i]\)(定义)
  2. \(f[i][j] = max(f[i][j - 1], f[i + (1 <<(j - 1))][j - 1])\)

第二条是因为什么呢?

首先我们要知道,\(f\)数组的处理顺序是先确定一个左端点,然后一直往右扩展\((\)固定\(i\)增加\(j)\).因此每当我们把j+1,当前区间都会增长一倍,这时候区间\([i,j]\)无非只有两种情况:

  1. 它在左半部分\(([i,i+2^{j-1}])\)
  2. 它在右半部分\(([i+2^{j-1},i+2^{j}])\)

这就是上式的意思啦.因为我们在处理\(j\)时,\(j-1\)的情况一定已经处理好了,所以可以做到递推.

代码:

for (int j = 1; j <= 20; ++j)
    for (int i = 1; i + (1 << j) - 1 <= n; ++i)
        f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);

接下来就是询问了.

当我们在询问时,肯定不能保证区间长度恰好是\(2\)的次幂,也就是和\(f[l][k]\)不一定重合.因此我们将区间分成两个相交的部分,比如区间是\([l,r]\),那么我们就把区间分成两部分,但是这两部分是相交的:\([l,l+2^k],[r-2^k+1,r]\),其中\(k\)是最大的满足\(2^k\le\)区间长度的正整数.这样取一下max就可以了.

但是我们注意到,k并不能用暴力来求,否则会当场去世.所以我们还要处理一个k的数组.我们注意到,我们需要\(2^k\le r-l+1\),这意味着,\(k(k\in N_+)\)应该等于\(\lfloor log_2(r-l+1)\rfloor\).我们需要处理一个\(log_2\)数组,(以及我们可以顺便初始化一下\(f\)数组).

LOG[0] = -1;
for (int i = 1; i <= n; ++i)
    f[i][0] = a[i], LOG[i] = LOG[i >> 1] + 1;

学过对数函数的自然能看明白

现在我们可以放心大胆的求解了

代码:

for (int i = 1; i <= m; ++i) {
    read(q1), read(q2);
    int k = LOG[q2 - q1 + 1];
    printf("%d\n", max(f[q1][k], f[q2 - (1 << k) + 1][k]));
}

No.2 倍增求LCA

序列和树链总是相通的——某位哲人

通常求\(LCA\)的方式有树剖,奇怪的我不会的神仙\(O(1)\)做法,以及本文要讲的倍增法.

一切都是从暴力开始的——某位哲人

当我们试图暴力求\(LCA\)的时候,画风将会是下面这个样子:

inline int LCA(int x, int y) {
    while (x != y) {
        if (dep[x] < dep[y])
            swap(x, y);
        x = father[x];
    }
    return x;
}

数据范围大起来以后时间复杂度显然是\(O(\)跑不过\()\),只能另想别的办法.

还记得上一道题吗?如果暴力在序列上找,同样会\(TLE\),而这个暴力找\(LCA\)的过程也是一样的复杂度.因此我们可以尝试相似的优化方法.如果我们一次跳一大段,肯定能省很大的复杂度,甚至还可以让两个点一起跳.

我们这样来考虑:

为了保证相遇,我们通常令深度较大的一个点向上跳.倍增和暴力的最大区别是一次跳的步数和跳时两个点是否同步.仍然设\(f[i][j]\)表示从点\(i\)往上连续\(2^j\)的点.这次不再需要\(LOG\)数组,只要\(f\)数组就够了.当我们在寻找\(x,y\)\(LCA\)时,先选中深度较大的一个点,然后不断往上跳,直到与另一个点深度相同.这是如何实现的呢?众所周知,任何正整数都可以进行二进制拆分,这和我们维护的第二维恰好很契合.根本上来说,我们就是把初始时两个点的深度的差\(K\)进行二进制拆分,在每个\("1"\)位代表的数处跳一次.操作上来说,就是倒序循环\(20\)\(1\),如果跳一步后还比另一个点深,就跳.或者说是从\(K\)可能的最高位往下匹配,如果匹配到1就跳.不过为了防止在\("0"\)的位置仍然得出"能跳"的结论从而导致最后无法恰好一样深,一定要倒序哦.

现在\(x,y\)一样深了,接下来,我们就在它们相遇之前,不断一起往上跳.同样是二进制思想.最终,他们之差一点就相遇了,这时候只需要随便选一个当代表往上跳一步就可以了,这就是\(LCA\)!

代码:

int lca(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);
    for (int i = 20; i >= 0; i--) {
        if (dep[f[x][i]] >= dep[y]) 
            x = f[x][i];
        if (x == y) 
            return x;
    }
    for (int i = 20; i >= 0; --i)
        if (f[x][i] != f[y][i])
            x = f[x][i], y = f[y][i];
    return f[x][0];
}

转载于:https://www.cnblogs.com/i-cookie/p/11384032.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值