RMQ与LCA
这章真的是题目难,代码量还大,动不动就快200行了,呜呜呜。
LCA:
树上问题通过dfs序转化成区间问题(一般都是dfn序)
P3379 【模板】最近公共祖先(LCA) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意:LCA模板题
第一种做法:dfs序将LCA问题转换为RMQ问题,我们先用dfn序给每个结点标上序号,再用dfs求出树的欧拉序列,然后保存下每个数第一次出现在欧拉序列中的位置,两个数的LCA即为这两个数第一次出现在欧拉欧拉序列中的位置之间的区间最小值对应的结点。这样问题就从一颗二维的树变成了一个区间最小值问题,而区间最小值我们有三种做法:暴力,线段树,st表(分析如上),其中st表的预处理是O(nlogn),询问是O(1),因此选用st表比较快。
那什么叫st表呢?首先我们先把欧拉序列摆出来,而我们要做的是随机访问这个序列的区间最小值,而一个区间又可以表示为左端点以及长度,所以我们如果知道了这个序列每一个点作为左端点并且给定长度的区间最小值,那么就能做到O(1)的查询,然而,这个问题乍一看需要O(n*n)的暴力循环,但是我们思考一下这个区间长度的问题,是不是可以采用二进制拆分的做法把一个区间拆分成若干个2的整数次方的长度的区间,这样一来我们对于每个左端点来说,需要维护的只有logn个区间最小值,就可以解决这个问题了。
还有一点:在st表查询的时候,对于一个区间最值问题来说,我重复地询问一段区间对于这个区间最值是没有影响的,比如我现在要查询[1,7]这个区间,如果按照二进制拆分我们应该查询[1,4],[5,6],[7,7],这样的话查询的复杂度也会提高到O(n),但是我们其实只要查询[1,4],[4,7],这两个区'
间中的最小值就是整个区间的最小值,虽然4被访问了两次但是不影响结果。
#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
struct node{
int ne,to;
};
node edge[maxn<<1];
int head[maxn];
int cnt=0;
void addedge(int a,int b){
edge[cnt].to=b;
edge[cnt].ne=head[a];
head[a]=cnt++;
}
int dfn[maxn<<1];//欧拉序列
int mp[maxn];//每一个dfs编号对应的原值
int fi[maxn];//每一个结点第一次出现在欧拉序列中的位置
int num=0;//dfs序中的编号
void dfs(int x,int fa){
num++;
int tem=num;
dfn[++cnt]=num;
mp[num]=x;
fi[x]=cnt;
for(int i=head[x];i!=-1;i=edge[i].ne){
int son=edge[i].to;
if(son==fa){
continue;
}
dfs(son,x);
dfn[++cnt]=tem;
}
}
int st[21][maxn<<1];
void rmq_st(){
for(int i=1;i<=cnt;i++){
st[0][i]=dfn[i];
}
int k=(int)(log(cnt*1.0)/log(2.0));
for(int i=1;i<=k+1;i++){
int len=(1<<i);
for(int j=1;j<=cnt-len+1;j++){
st[i][j]=min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
}
}
}
int rmq_find(int l,int r){
int len=r-l+1;
int i=(int)(log(1.0*len)/log(2.0));
return min(st[i][l],st[i][r-(1<<i)+1]);
}
int LCA(int a,int b){
if(fi[a]>fi[b]){
swap(a,b);
}
return mp[rmq_find(fi[a],fi[b])];
}
int main(){
memset(head,-1,sizeof(head));
int n,m,r;
cin>>n>>m>>r;
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
addedge(a,b);
addedge(b,a);
}
cnt=0;
dfs(r,0);
rmq_st();
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
cout<<LCA(a,b)<<endl;
}
return 0;
}
好题:线段树+dfn序+思维
题意:给你一颗树,有两种操作:1-选择一个节点给他加上val,但对每个节点来说当父亲被加上val后,自己要加上-val,即隔层加减,具有传递性。2-询问某个点的值
思路:我们注意到这是一个树上问题,直接操作不太方便,因此我们考虑转化成区间问题,注意到所有的操作都是有关于子树的,这就让我们想到dfn序列,在dfn序列中,每个子树在这个序列上都是连续的一个区间,这样树上问题就被转化成了一个区间修改,单点查询的问题,但是问题在于他是各层操作,所以我们可以建两颗线段树,分别代表奇数层和偶数层,这样我们每次只需要分别对两颗线段树进行修改即可。
#include <bits/stdc++.h>
using namespace std;
#define visit _visit
#define next _next
#define pb push_back
#define fi first
#define se second
#define endl '\n'
#define fast ios::sync_with_stdio(0), cin.tie(0)
#define int long long
#define ll long long
#define pint pair<int,int>
const int mod = 998244353;
const int maxn = 200001;
const int INF = 0x3f3f3f3f;
void read(int &x){
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-