ST表

就是一个用倍增法求静态RMQ(区间最值)的算法。

预处理 \(O(nlogn)\),查询 \(O(1)\),远吊打其他算法。

算法

预处理

依次预处理出左端点为 \(l\),右端点为 \(l+2^j(1\le j\le log_2(n))\) 的所有区间最小值,递推式为:

\[d_{i,j}=\min(d_{i,j-1},d_{i+2^{j-1}\ \ ,j-1})
\]

查询

比如说要查询 \([l,r]\) 这个区间。

我们令 \(k=log_2(r-l+1)\ (2^{k}\le r-l+1\le 2^{k+1})\)。

那么这个区间的最小值就是 \(\min(d_{l,k},d_{r-2^k+1,k})\)

由于 \(2^{k}\le r-l+1\le 2^{k+1}\) ,所以这两个区间只会重合但不会超出去。

所以不用怕查询错误了。

关于 log2

上面的讲述都围绕着一个东西 \(log_2(x)\),那么这个重要的东西怎么求呢?

其实很简单,我们使用 C++ 的 STL,直接写(int)(log(x)/log(2))就可以算出 \(log_2(x)\) 了。

Code

预处理

void pre_st(){
int ln=(int)(log(n)/log(2));
_for(i,1,n)d[i][0]=a[i];
_for(j,1,ln)
_for(i,1,n-(1<<j)+1)
d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
}

查询

int query(int l,int r){
int k=(int)(log(r-l+1)/log(2));
return min(d[l][k],d[r-(1<<k)+1][k]);
}

例题

P2880

板子题。

P2048

好题。

我们用 ST 表维护所有区间的最大前缀和,先算出所有以 \(l\) 为左端点时可以的右端点的区间 \([L,R]\) 和最优的右端点 \(t\),我们可以用一个堆来存储全局的最优区间。

但是我们不能只记录最优的右端点,还要找出分布在 \([L,t)\) 和 \((t,R]\) 的它次优的右端点,次次优的右端点……

那么我们就在每次取出当前最优区间的时候把 \([L,t)\) 和 \((t,R]\) 这两个区间的最优右端点也放入堆即可。

点击查看代码
#include <bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=1e6+10,inf=0x3f3f3f3f;
ll n,k,l,r,a[N],b[N],d[N][30],ans;
struct music{
ll le,rl,rr,t;
friend bool operator <(const music& aa,const music& bb){
return b[aa.t]-b[aa.le-1]<b[bb.t]-b[bb.le-1];
}
};
priority_queue<music>q;
inline ll rnt(){
ll x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*w;
}void pre_st(){
ll ln=(ll)(log(n)/log(2));
_for(i,1,n)d[i][0]=i;
_for(j,1,ln){
_for(i,1,n-(1<<j)+1){
ll x=d[i][j-1],y=d[i+(1<<(j-1))][j-1];
if(b[x]>b[y])d[i][j]=x;
else d[i][j]=y;
}
}
}ll query(ll l,ll r){
ll k=(ll)(log(r-l+1)/log(2));
if(b[d[l][k]]>b[d[r-(1<<k)+1][k]])return d[l][k];
else return d[r-(1<<k)+1][k];
}int main(){
n=rnt(),k=rnt(),l=rnt(),r=rnt();
_for(i,1,n)a[i]=rnt(),b[i]=b[i-1]+a[i];
pre_st();
_for(i,1,n+1-l)
q.push(music{i,i+l-1,min(n,i+r-1),query(i+l-1,min(n,i+r-1))});
_for(i,1,k){
music t=q.top();
q.pop();
ans+=b[t.t]-b[t.le-1];
if(t.t>t.rl)
q.push(music{t.le,t.rl,t.t-1,query(t.rl,t.t-1)});
if(t.t<t.rr)
q.push(music{t.le,t.t+1,t.rr,query(t.t+1,t.rr)});
}printf("%lld\n",ans);
return 0;
}

lca

即最近公共祖先。

这里先只讲三种简单的做法。

树上 RMQ

前置知识:欧拉序列

我们每到一个点就记录一下。比如这棵树:

可以得到序列:

dfs序 1 2 3 4 5 6 7 8 9 10 11 12
编号 1 2 4 5 4 2 1 3 6 7 3 1
层数 1 2 3 4 3 2 1 2 3 3 2 1

每个点第一次出现在:

1 2 3 4 5 6 7
1 2 8 3 4 9 10

算法

我们以上面的树为例,我们要查询 \(5\) 和 \(7\) 的 lca。

先找到 \(5\) 第一次出现在位置 \(4\),\(7\) 第一次出现在位置 \(10\)。

然后可以发现序列区间 \([4,10]\) 的最小的层数对应的点就是他们的 lca:\(1\)!

但这是为什么呢?

我们在正常 \(\text{dfs}\) 的过程中,要想从 \(u\) 遍历到 \(v\) 必然要经过它们的 \(\text{lca}\),也就说明它们的 \(\text{lca}\) 至少有两棵子树。

既然至少有两棵子树,就不可能遍历完一棵子树后先回到父亲再去遍历另一棵子树。

所以它们的 \(\text{lca}\) 一定是这个区间内层数最小的!

Code

void dfs(int u,int fa){
jl[u]=1;
c[u]=c[fa]+1;
dfn[++cnt]=u,q[u]=cnt;
int sz=tu[u].size();
_for(i,0,sz-1){
int v=tu[u][i].v;
if(jl[v])continue;
dfs(v,u);
dfn[++cnt]=u;
}
}void pre_st(){
int ln=(int)(log(cnt)/log(2));
_for(i,1,cnt)d[i][0]=dfn[i];
_for(j,1,ln)_for(i,1,cnt+1-(1<<j)){
if(c[d[i][j-1]]<c[d[i+(1<<(j-1))][j-1]])d[i][j]=d[i][j-1];
else d[i][j]=d[i+(1<<(j-1))][j-1];
}
}int query(int l,int r){
int k=(int)(log(r-l+1)/log(2));
if(c[d[l][k]]<c[d[r-(1<<k)+1][k]])return d[l][k];
return d[r-(1<<k)+1][k];
}

离线 Tarjan

算法

首先我们把所有查询都一股脑的给存进来。

对于一个节点 \(u\),我们都先把它当做自己的根,遍历它所有的子树,每个子树遍历完后把这棵子树的根设为 \(u\)。

在遍历某个节点的时候,如果它对应的查询点 \(v\) 被访问过,那么就查询 \(v\) 当前 的祖先 \(u\),这就是它们的 \(\text{lca}\)。

因为如果一个点对应的查询点被访问过,那么就一定是在 \(u\) 的另一棵子树上遍历的,既然不在一棵子树上但在同一个节点的子树们下,那么 \(u\) 就一定是它们的 \(\text{lca}\) 了!

具体实现,只用一个深搜就完成了。

Code

//tu是原来的树,t是每个点查询时对应的另一个点
void tarjan(int u){
v[u]=1;
int s1=tu[u].size(),s2=t[u].size();
_for(i,0,s1-1){
int vv=tu[u][i].v;
if(v[vv])continue;
tarjan(vv);
fa[vv]=u;
}_for(i,0,s2-1){
int vv=t[u][i].v;
if(v[vv])lca[t[u][i].w]=find(vv);
}
}

倍增

算法

我们先预处理出 \(u\) 的第 \(2^i\) 个祖先,递推式为:

\[fa_{u,i}=fa_{fa_{u,i-1}\ \ ,i-1}
\]

即:\(u\) 的第 \(2^i\) 个祖先为 \(u\) 的第 \(2^{i-1}\) 个祖先的第 \(2^{i-1}\) 个祖先。

我们在查询时先让深度深的往上跳,直到跳到同一高度,然后再让两个点一起跳,跳到同一节点时,那个点就是它们的 \(\text{lca}\) 了。

总之:这是一个十分优雅的暴力。

Code

void dfs(int u,int last){
dep[u]=dep[last]+1;
fa[u][0]=last;
int sz=tu[u].size();
for(int i=1;(1<<i)<=dep[u];++i)
fa[u][i]=fa[fa[u][i-1]][i-1];
_for(i,0,sz-1)
if(tu[u][i]!=last)dfs(tu[u][i],u);
}int lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);
while(dep[x]!=dep[y])
x=fa[x][lg[dep[x]-dep[y]]-1];
if(x==y)return x;
for_(i,lg[dep[x]],0)
if(fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}

对比

算法 查询复杂度 代码长度 能否维护附加信息 维护附加信息代码长短
树上 RMQ \(O(1)\) 较长 \
离线 Tarjan \(O(n)\) (全部询问) 简短
倍增 \(O(logn)\) 简短

例题

P3379

板子题。

P2912

对于每个节点 \(u\),我们维护一下它到根节点的路径权值和 \(sum(u)\)。

在找出 \(u\) 和 \(v\) 的 \(\text{lca}\) 后,它们的路径权值和就是:

\[sum(u)+sum(v)-2*sum(lca)
\]
点击查看核心代码

用 \(\text{tarjan}\) 做的。

void pre(){
_for(i,1,n)fa[i]=i;
}int find(int x){
if(fa[x]==x)return x;
return fa[x]=find(fa[x]);
}void tarjan(int u,int w,int f){
sum[u]=sum[f]+w;
v[u]=1;
int s1=tu[u].size(),s2=t[u].size();
_for(i,0,s1-1){
int vv=tu[u][i].v,ww=tu[u][i].w;
if(v[vv])continue;
tarjan(vv,ww,u);
fa[vv]=u;
}_for(i,0,s2-1){
int vv=t[u][i].v;
if(v[vv])lca[t[u][i].w]=find(vv);
}
}

P2245

好题。

首先我们要做一个最小生成树,来尽量降低危险值。

但难点在于:如何去维护路径最小值?

我们选择用倍增做,因为我们在递推 \(u\) 的第 \(2^i\) 个祖先的时候可以顺便递推出\(u\) 与第 \(2^i\) 个祖先的路径上的最小值。

我们在查询 \(u\rightarrow v\) 的最小值时,答案就应该"是 \(u\) 到 \(\text{lca}\) 的最小值"和"\(v\) 到 \(\text{lca}\) 的最小值"两者中的最小值

点击查看全部代码
#include <bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=3e5+10,inf=0x3f3f3f3f;
int n,m,q;
struct bian{int u,v,w;}b[N];
struct tree{int v,w;};
vector<tree>tr[N];
inline int rnt(){
int x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*w;
}bool cmp(bian a,bian b){
return a.w<b.w;
}struct Kru{
int fa[N];
void pre(){
_for(i,1,n)fa[i]=i;
}int find(int u){
if(fa[u]==u)return u;
return fa[u]=find(fa[u]);
}void kru(){
int k=0;
pre();
sort(b+1,b+m+1,cmp);
_for(i,1,m){
int u=b[i].u,v=b[i].v,w=b[i].w;
if(find(u)!=find(v)){
++k;
fa[find(u)]=find(v);
tr[u].push_back(tree{v,w});
tr[v].push_back(tree{u,w});
}if(k==n-1)break;
}
}
}Kru;
struct Lca{
int f[N][30],d[N][30],jl[N],c[N],z[N],root[N];
void dfs(int u,int w,int fa,int r){
jl[u]=1;
c[u]=c[fa]+1;
z[u]=w;
root[u]=r;
f[u][0]=fa,d[u][0]=w;
for(int i=1;(1<<i)<=c[u];++i){
f[u][i]=f[f[u][i-1]][i-1];
d[u][i]=max(d[f[u][i-1]][i-1],d[u][i-1]);
}
int sz=tr[u].size();
_for(i,0,sz-1)if(!jl[tr[u][i].v])dfs(tr[u][i].v,tr[u][i].w,u,r);
}inline int lca(int u,int v){
if(c[u]<c[v])swap(u,v);
int mx=0;
if(root[u]!=root[v])return 0;
if(c[u]!=c[v]){
for_(i,20,0)
if(c[f[u][i]]>=c[v])
mx=max(mx,d[u][i]),u=f[u][i];
}if(u==v)return mx;
for_(i,20,0){
if(f[u][i]!=f[v][i]){
mx=max(mx,max(d[u][i],d[v][i]));
u=f[u][i],v=f[v][i];
}
}mx=max(mx,max(d[u][0],d[v][0]));
return mx;
}
}Lca;
int main(){
n=rnt(),m=rnt();
_for(i,1,m)b[i].u=rnt(),b[i].v=rnt(),b[i].w=rnt();
Kru.kru();
_for(i,1,n)if(!Lca.jl[i])Lca.dfs(i,0,0,i);
q=rnt();
while(q--){
int x=rnt(),y=rnt();
int qwq=Lca.lca(x,y);
if(qwq)printf("%d\n",qwq);
else printf("impossible\n");
}
return 0;
}
\[\Huge{\mathfrak{The\ End}}
\]

「学习笔记」倍增思想与lca的更多相关文章

  1. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  2. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  3. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  4. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  5. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  6. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  7. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  8. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

  9. 「学习笔记」珂朵莉树 ODT

    珂朵莉树,也叫ODT(Old Driver Tree 老司机树) 从前有一天,珂朵莉出现了... 然后有一天,珂朵莉树出现了... 看看图片的地址 Codeforces可还行) 没错,珂朵莉树来自Co ...

随机推荐

  1. SpringBoot之:SpringBoot的HATEOAS基础

    目录 简介 链接Links URI templates Link relations Representation models 总结 简介 SpringBoot提供了HATEOAS的便捷使用方式,前 ...

  2. RPA微信机器人汇总

    一.微信广告PDF对账单数据提取机器人 [机器人详情] 微信广告对账结算单为PDF文件,从每一期对账单文件中提取结算数据,统计成excel表格,便于与腾讯广告业务结算审核 [机器人步骤] 1.启动机器 ...

  3. VisionPro · C# · 加密狗检查程序

    写VisionPro C#项目时,我们需要在程序的启动时加载各种配置文件,以及检查软件授权,以下代码即检查康耐视加密狗在线状态,如查无加密狗,关闭程序启动进程并抛出异常. 1 using System ...

  4. org/apache/poi/POIXMLTypeLoader或者java.lang.NoSuchFieldError: RETURN_NULL_AND_BLANK

    原因是之前我的poi和ooxml版本有点低, 解决方案 将两者版本提高,我是将两者的版本都提高到了3.15

  5. 【RocketMQ】消息的存储

    Broker对消息的处理 BrokerController初始化的过程中,调用registerProcessor方法注册了处理器,在注册处理器的代码中可以看到创建了处理消息发送的处理器对象SendMe ...

  6. HMS Core 机器学习服务打造同传翻译新“声”态,AI让国际交流更顺畅

    2022年6月,HMS Core机器学习服务面向开发者提供一项全新的开放能力--同声传译,通过AI语音技术减少资源成本,加强沟通交流,旨在帮助开发者制作丰富多样的同声传译应用. HMS Core同声传 ...

  7. 记录一次ubuntu安装mysql,远程无法登录问题的解决历程

    进入ubuntu的mysql配置文件 sudo vim debian.cnf [client] host = localhost user = debian-sys-maint password = ...

  8. SpringBoot快速整合通用Mapper

    前言 后端业务开发,每个表都要用到单表的增删改查等通用方法,而配置了通用Mapper可以极大的方便使用Mybatis单表的增删改查操作. 通用mapper配置 1.添加maven: <depen ...

  9. springboot和mybatis 配置多数据源

    主数据源(由于代码没有办法复制的原因,下面图片和文字不一致) package com.zhianchen.mysqlremark.toword.config;import com.zaxxer.hik ...

  10. linux 配置集群需要修改的东西

    1. 服务器主机名 vi /etc/hostname 按Esc,然后:wq! ,保存,然后重启电脑 reboot 2.修改IP和mac,也可以设置成自动的,但一般是固定的 cd /etc/syscon ...