倍增和差分

本文介绍了两种降低时间复杂度的方法——倍增和差分。倍增算法主要用于区间最值查询(RMQ),通过预处理达到O(nlogn)的时间复杂度,查询时为O(1)。差分数组则用于区间修改和查询,通过计算差分数组简化操作。文章详细阐述了倍增算法的ST表构建和RMQ查询,以及差分数组的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

两种降时间复杂度的方法

离线查询(做起来容易):CDQ分治

在线查询:树套树

倍增

RMQ

区间最值查询Range Minimum/Maximum Query:给定一个数组,求出给定区间[l,r]最值的下标

倍增最关键的地方是:绝对不会走都是相同的步数(比如说走两步可以到达的地方绝对不会分两个一步走)

其次可以想到:成倍的增长->2(1,2,4,8,16…)

ST(Sparse Table)算法:O(nlogn)预处理,O(1)查询

求最小值

1.预处理(动态规划)

f(i,j)表示区间[i,i+2j-1]

第i个点到第i+2j-1个点的最小值

即f(i,0)表示区间[i,i]的最小值(即这个值)、f(i,1)表示区间[i,i+1]、f(i,2)表示区间[i,i+3]、f(i,3)表示区间[i,i+7]~i+15,i+31+…依次类推

用动态规划的思想,大区间的最小值可以由小区间的最小值转移而来,由此我们可以发现区间[i,i+2j-1]的最小值可以由区间[i,i+2j-1-1]的最小值和[i+2j-1,i+2j-1]的最小值,两者的最小值得来。(取两个长度为2j-1的块取代和更新长度为2j的块)

即最小值(此时起点为i,间断点为i+2j-1(化为f的第二个参数即j-1),两个区间的终点都为j-1(因为分成两块所以终点已经不是j了))

f[i,j]=min(f[i,j-1],f[i+2j-1][j-1])

同理f[i,j]=max(f[i,j-1],f[i+2j-1][j-1])

我的一些思考:
f(1,1)是区间[1,2],f(1,1)=min(f(1,0),f(2,0))即第一个点的区间[1,1]的最小值+第二个点的区间[2,2]的最小值
f(1,2)是区间[1,4],f(1,2)=min(f(1,1),f(3,1))即第一个点的区间[1,2]的最小值+第三个点的区间[3,4]的最小值
(所以要先算出f(1,1),f(2,1),f(3,1)才能知道f(1,2)…所以j要放在外层循环)

代码模板

//应该把j放在外层,因为内层要用到的还没被计算
 for (int j = 1; j < 20; j++)//假设样例n最大为50000,则2的16次方已经可以超过了
	for (int i = 1; i <= n; i++)			
 		if (i + (1 << j) - 1 <= n)//大于n的就不用计算
			 f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);

2.查询(ST算法)

Query(L,R)注意:L和R不一定是2的幂次

假设L+2k到不了R,同理R-2k+1也到不了L,则要找到两个覆盖[L,R]区间的最小幂区间(区间可重复,因为相交没有影响)

区间长度为R-L+1,要保证k最大,则2k<=(R-L+1)->k=log2(R-L+1)

RMQ(L,R)=min(f[L,k],f[R-2k+1,k])

代码模块

//query(a,b)
k = (int)(log((double)(b - a + 1)) / log(2.0));
mi = min(f[a][k], f[b -(1<<k) +1][k]);

板子题:

POJ 3264

RMQ无法求区间和

LCA

最近公共祖先Lowest Common Ancestors

离线:Tarjan算法

O(n+q)

在线:倍增做法

LCA洛谷教程:https://www.luogu.org/problemnew/solution/P3379

原因:直接dfs找祖先会t,所以选择1,2,4,8,16,32…这样跳着寻找->倍增

这里要涉及到链式前向星存图,特来补充一下

建图:

struct node

{

    int weight;

    int end;

    int next;//next[i]表示与第i条边同起点的上一条边的存储位置

}edge[maxn];

int head[maxn];//head[i]表示以i为起点的最后一条边的存储位置

加边:


int head[maxn];//初始化为-1,head[i]表示以i为起点的最后一条边的存储位置

int cnt;//cnt初始化为1

void add(int u, int v, int w)//u是起点,v是终点,w是权重

{

    edge[cnt].weight = w;

    edge[cnt].end = v;

    edge[cnt].next = head[u];

    head[u] = cnt++;

}

遍历:

for(int i=head[start];i!=0;i=edge[i].next)\\start是输入的起点

例子:

带权图

输入:

5 7

1 2 8

1 3 13

1 4 15

2 4 9

3 4 5

3 5 10

4 5 3

我对链式前向星的思考:
edge数组:所有的边都存入里面
edge[1]的权重为8,终点为2,它的next为-1,head[1]=1
edge[2]的权重为13,终点为3,它的next为1(表示第二条边指向第一条边,即把兄弟边都连在一块),head[1]=2//表示1顶点所连的边数
其他的同理可得

注:从大到小找(从小到大会把最近的跳过)

depth数组记录每个节点的深度

fa[i][j]表示节点i的2j级祖先

复杂度:

1.预处理O(nlogn)

dfs遍历每个节点,处理深度数组depth和fa数组

可以思考一下:若是森林应该如何处理?


void dfs(int now, int father)//now表示当前节点,father表示当前节点的父亲节点

{

    depth[now] = depth[father] + 1;

    fa[now][0] = father;

    for (int i = 1; (1 << i) <= n; i++)

        fa[now][i] = fa[fa[now][i - 1]][i - 1];//我的第2^i级爸爸是我的2^(i-1)级爸爸的2^(i-1)级爸爸

    for (int i = head[now]; i != -1; i = edge[i].next)

    {

        if (edge[i].end != father)

            dfs(edge[i].end, now);

    }

}

2.查询O(logn)

query(x,y)

先要控制x,y深度一致,然后再倍增的往上跳

常数优化:

for (int i = 1; i <= n; i++)

        {

            lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);

        }

log(a)(MN)=log(a)(M)+log(a)(N)

为什么要算log2(i)+1=log2(2*i):因为log2(i)不好直接求得(所以后续还要-1)

查询:

long long LCA(int x,int y)

{

    if (depth[x] < depth[y])

        swap(x, y);

    while (depth[x] > depth[y])

    {

        x = fa[x][lg[depth[x] - depth[y]] - 1];

    }

    if (x == y)return x;

    for (int k = lg[depth[x]]-1;k>=0;k--)//向上跳,从大到小

    {

        if (fa[x][k] != fa[x][k])

        {

            x = fa[x][k];

            y = fa[y][k];

        }

    }

    return fa[x][0];

}

不能直接跳到LCA(两个数的LCA,因为不好判断,谁知道跳到的是祖先还是最近祖先呢),所以只能跳到两个数的某一个数的父节点是这两个数的LCA,这就是为什么一开始设的数组是存储的父亲节点。

差分

求出差分数组(后-前)B,L~R的区间修改只需要修改B[L],B[R+1]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值