倍增&RMQ&LCA

昨天晚上的事情就不提了,不要在意这些细节,我今天下午800m明天上午1500m给她跑个成绩出来。mad
今天上午,本来我是真的很想很想很想去看比赛的,我小学4年、初中2年的运动会从来不缺我给同学喊加油。要不是昨天晚上那一出戏,我现在就在安安心心看比赛。mad
我就是很看重运动会,运动会我从来都是很浪的,再说我不是停课了吗,结果事情闹得这么大,我一年就浪这么一次好吧??mad
好吧,那就安心待在办公室吧,昨天晚上我自己犯下的错,自己承担责任,今天上午就把昨天晚上落下的补起来。
今天上午看了倍增,思想什么的我也不太清楚,但至少倍增的思路我是搞懂了。先来看一道模板题:

luogu P1816 忠诚

题目描述

老管家是一个聪明能干的人。他为财主工作了整整10年,财主为了让自已账目更加清楚。要求管家每天记k次账,由于管家聪明能干,因而管家总是让财主十分满意。但是由于一些人的挑拨,财主还是对管家产生了怀疑。于是他决定用一种特别的方法来判断管家的忠诚,他把每次的账目按1,2,3…编号,然后不定时的问管家问题,问题是这样的:在a到b号账中最少的一笔是多少?为了让管家没时间作假他总是一次问多个问题。

输入输出格式

输入格式:
输入中第一行有两个数m,n表示有m(m<=100000)笔账,n表示有n个问题,n<=100000。

第二行为m个数,分别是账目的钱数

后面n行分别是n个问题,每行有2个数字说明开始结束的账目编号。

输出格式:
输出文件中为每个问题的答案。具体查看样例。

输入输出样例

输入样例#1:
10 3
1 2 3 4 5 6 7 8 9 10
2 7
3 9
1 10
输出样例#1:
2 3 1

这种题目就是给你一个数组,再给你几个区间,让你求每个区间中的最大值或者最小值。这里,我们开一个f二维数组,f[i][j]表示从第i个数开始的 2j 个数(这个区间)的最大值。反正复杂度是log级别的。然后,在求f[i][j]的时候,我们已经保证了f[i][j-1]是求过的,所以状态转移方程是这样的:

f[i][j]=max(f[i][j1],f[i+2j1][j1])

这里需要理解一下,先翻译一下:从第i个数开始的 2j 个数的最大值=max(从第i个数开始的 2j1 个数的最大值,从第 i+2j1 位置开始的 2j1 个数的最大值)。因为 2j=(2j1)+(2j1) ,所以说这个最大值求的时候是没有遗漏的;而为什么是 i+2j1 也就很好理解了:刚刚的f[i][j-1]是前半个区间的最大值,所以后半个区间的起点自然是这样了。
先看一下代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,i,j,k,a[100000],f[100000][30];
inline int read()
{
    int num=0,flag=1;
    char c=getchar();
    for (;c<'0'||c>'9';c=getchar())
    if(c=='-') flag=-1;
    for (;c>='0'&&c<='9';c=getchar())
    num=(num<<3)+(num<<1)+c-48;
    return num*flag;
}
void init()
{
    m=read();
    n=read();
    for (i=1;i<=m;++i)
    a[i]=read();
}
void rmq()
{
    for (i=1;i<=m;++i)
    f[i][0]=a[i];
    for (j=1;j<=20;++j)
    for (i=1;i>=m;++i)
    if (i+(1<<j)-1<=m)
    f[i][j]=min(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
void work()
{
    for (int ii=1;ii<=n;++ii)
    {
        i=read();
        j=read();
        k=log(j-i+1)/log(2);
        cout<<min(f[i][k],f[j-(1<<k)+1][k])<<" ";
    }
}
int main()
{
    init();
    rmq();
    work();
    return 0;
}

主体是两个:①预处理rmq;②查询。
RMQ的状态转移方程刚刚讲过了,这里还有循环需要注意。为什么j循环在i循环外面??老师跟我说,让我想一下合并石子——合并石子的循环最外面是len枚举了长度。这里的最外层循环j也是枚举了区间长度,只不过它是幂级的。而我后来又是这么想的:既然我刚刚说到在求f[i][j]的时候保证了f[i][j-1]是已经求过的,那可是 f[i+2j1][j1] 没有保证j-1的 i+2j1 是求过的,所以你可以这么想:把i循环放里面,你只有当把这一层所有的i循环都求过了,才可以保证这一点。
接下来就是查询,这里 k=log(ji+1)/log2 ,首先这是一个换底公式,所以其实是 k=log2(ji+1) ,k表示这个区间长度,但是因为f数组的右下标是幂级的,所以先用一个log,到时候平方了就正常了。输出的答案是 min(f[i][k],f[j2k+1][k]) 就是包含了整个区间(或许还有重叠),i是起点开始数k个数, j2k+1 是从终点倒推k个数。就这样。

下面是LCA模板题:
题目描述
给定n个点的树(1是根),m次询问,每次询问两点的LCA;

输入格式
第一行两个整数,n,m;

接下来n-1行,给定正整数a,b,表示a,b间有边;

接下来m行,给定整数a,b,表示询问a,b;

输出格式
对于每个询问,输出它们的LCA;

样例数据
input

3 2
1 2
2 3
1 2
2 3
output

1
2
数据规模与约定
保证所有数据n=m=100000

先上代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,xx,yy,temp,k,link[200010],father[100010][40],depth[100010];
struct alca
{
    int y,next;
}
a[200010];
inline int read()
{
    int num=0,flag=1;
    char c=getchar();
    for (;c<'0'||c>'9';c=getchar())
    if(c=='-') flag=-1;
    for (;c>='0'&&c<='9';c=getchar())
    num=(num<<3)+(num<<1)+c-48;
    return num*flag;
}
void insert(int ss,int ee)
{
    a[++temp].y=ee;
    a[temp].next=link[ss];
    link[ss]=temp;
}
void init()
{
    n=read();
    m=read();
    for (int i=1;i<n;++i)
    {
        xx=read();
        yy=read();
        insert(xx,yy);
        insert(yy,xx);
    }
}
void dfs(int t)
{
    for (int i=link[t];i;i=a[i].next)
    {
        k=a[i].y;
        if (k==father[t][0]) continue;
        depth[k]=depth[t]+1;
        father[k][0]=t;
        dfs(k);
    }
}
void work()
{
    for (int j=1;j<=20;++j)
    for (int i=1;i<=n;++i)
    if ((1<<j)<=n)
    father[i][j]=father[father[i][j-1]][j-1];
}
void lca()
{
    for (int i=1;i<=m;++i)
    {
        xx=read();
        yy=read();
        if (depth[xx]>depth[yy]) swap(xx,yy);
        k=depth[yy]-depth[xx];
        for (int j=0;j<=log(n)/log(2);++j)
        if ((1<<j)&k) yy=father[yy][j];
        if (xx!=yy)
        {
            for (int j=log(n)/log(2);j>=0;--j)
            if (father[xx][j]!=father[yy][j])
            {
                xx=father[xx][j];
                yy=father[yy][j];
            }
            yy=father[yy][0];
        }
        printf("%d\n",yy);
    }
}
int main()
{
    init();
    dfs(1);
    work();
    lca();
    return 0;
}

这里,结构还是比较清晰的,我们一段一段看:
①init和insert:输入处理,输入n个结点,m次查询,底下n-1行树的边,底下m行查询。我们用邻接表存储这颗树,无向图。
②dfs:从根节点开始往下遍历,如果碰到当前这个结点是它的直接父亲结点,那么跳出本次循环,否则求出每一个结点的深度和直接父亲结点,然后继续往下遍历。
③work:预处理,和RMQ思想一样——j循环枚举往上蹦的层数,i循环枚举所有结点,第i个结点往上蹦 2j 层的父亲就是第i个结点往上蹦 2j1 的父亲往上再蹦 2j1 层的父亲。
④lca:查询,这里又分两步:第一步,由深的那个点往上移动到和浅的那个点深度一样;第二步,由深度相同的两个点同时移动直到找到lca。第一步,先使yy成为较深的那个点,然后求出两点的深度差,底下那个循环就是把深度差转换成二进制位然后往上蹦(有点快速幂的思想),yy就到达了跟xx一样的深度。这时候判断一下,xx和yy是不是同一个点,如果是,那么就直接求出了LCA;如果不是,那就要第二步,照样是枚举二进制位,从大到小,往上蹦,如果它们的直接父亲结点不是同一个,那么往上蹦。然后这个时候还没有求出LCA,下一步就是让yy等于它的直接父亲,这样就求出了LCA。
最后的时候,我0分,是因为我用了cout,TLE。。。以后要用printf,谨记!!

这边有篇dalao的blog(LCA)可以参考一下:
http://blog.csdn.net/qq_39670434/article/details/78445833

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值