[做题笔记] lxl 的数据结构选讲(上)
Julia the snail
题目描述
解法
首先把所有询问离线下来,我们扫描 \(y\),维护所有 \(x\) 的答案,每次需要添加若干个 \(r_i=x\) 的线段。
设 \(f(x)\) 表示不经过 \(<x\) 的点,可以到达的最大高度。考虑添加线段 \([l_i,r_i]\) 的效果是:对于 \(i\in[1,l]\),如果 \(f(i)\geq l_i\),那么把 \(f(i)\) 修改成 \(r_i\)
可以直接上势能线段树维护,对于线段树上的每个区间我们维护 \(mx,cx\),分下面几种情况套路:
- 如果 \(mx<l_i\),那么整个区间不可能被修改,直接退出。
- 如果 \(cx<l_i\leq mx\),那么把整个区间为 \(mx\) 的单点修改成 \(r_i\),打标记后退出。
- 否则递归下去。
发现如果递归下去,要么是区间没有被整个包含,要么区间值的种类会减少 \(1\),时间复杂度 \(O(n\log n)\)
总结
注意维护信息的顺序,比如本题扫描 \(x\) 就是不行的,但是扫描 \(y\) 却可以。
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,ans[M];vector<int> g[M];
int mx[M<<2],cx[M<<2],fl[M<<2];
struct node
{
int l,r,id;
bool operator < (const node &b) const
{return r<b.r;}
}q[M];
void add(int i,int c)
{
fl[i]+=c;mx[i]+=c;
}
void down(int i)
{
if(!fl[i]) return ;
int ml=mx[i<<1],mr=mx[i<<1|1];
if(ml>=mr) add(i<<1,fl[i]);
if(ml<=mr) add(i<<1|1,fl[i]);
fl[i]=0;
}
void build(int i,int l,int r)
{
mx[i]=r;
if(l==r) return ;
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
}
void upd(int i,int l,int r,int L,int R,int x)
{
if(L>r || l>R || mx[i]<R) return ;
if(L<=l && r<=R && cx[i]<R)
{add(i,x-mx[i]);return ;}
int mid=(l+r)>>1;down(i);
upd(i<<1,l,mid,L,R,x);
upd(i<<1|1,mid+1,r,L,R,x);
mx[i]=max(mx[i<<1],mx[i<<1|1]);
cx[i]=max(cx[i<<1],cx[i<<1|1]);
if(mx[i<<1]!=mx[i]) cx[i]=max(cx[i],mx[i<<1]);
if(mx[i<<1|1]!=mx[i]) cx[i]=max(cx[i],mx[i<<1|1]);
}
int ask(int i,int l,int r,int x)
{
if(l==r) return mx[i];
int mid=(l+r)>>1;down(i);
if(x<=mid) return ask(i<<1,l,mid,x);
return ask(i<<1|1,mid+1,r,x);
}
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int l=read(),r=read();
g[r].push_back(l);
}
k=read();
for(int i=1;i<=k;i++)
{
int l=read(),r=read();
q[i]=node{l,r,i};
}
sort(q+1,q+1+k);build(1,1,n);
for(int i=1,j=1;i<=n;i++)
{
for(int x:g[i])
upd(1,1,n,1,x,i);
while(j<=k && q[j].r<=i)
ans[q[j].id]=ask(1,1,n,q[j].l),j++;
}
for(int i=1;i<=k;i++)
printf("%d\n",ans[i]);
}
Nastya and CBS
题目描述
解法
相比于分块做法,我写的做法基本没优势,而且更难写,不过可以练一练经典模型。
一个常规想法是开一棵线段树,每个节点维护 }]){[(
这种结构,也就是存在一个分界点使得前面只有左括号,后面只有右括号,如果不满足这样的结构那么判定无解。考虑使用哈希在合并两个儿子的时候来匹配括号,可以用线段树套上可持久化 \(\tt treap\),时空复杂度都是 \(O(n\log^2 n)\)
发现空间复杂度的瓶颈是要维护出所有前缀后缀的哈希值,考虑套用只递归半边的线段树模型,这样我们就只需要维护抵消过后,前面右括号的整体哈希值,后面左括号的整体哈希值。
考虑怎么快速取出一个前缀或者后缀,下面是取出前缀右括号哈希值的实现方式:
zxy getl(int i,int k)//节点i,需要取出k的长度
{
if(!k) return zxy(0,0);//出口
if(k==vl[i].y) return vl[i];//整个相等,可以直接返回
if(k<=vl[ls].y) return getl(ls,k);//右括号全部来源于左半边,单边递归
return vl[ls]+(getl(rs,k-vl[ls].y+vr[ls].y)-vr[ls]);
//还需要递归右半边,但是这样就需要和左半边的左括号抵消
//所以获取的长度要改变,并且结果需要减去左半边的左括号
}
把哈希函数封装就可以方便的实现,注意右括号的合并顺序应该是从右到左(回文的顺序)
询问的时候,我们先把 \(O(\log n)\) 个区间取出来,然后用栈的方式合并,时间复杂度 \(O(n\log ^2n)\),空间复杂度 \(O(n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 1e9+7;
#define ll long long
#define ls (i<<1)
#define rs (i<<1|1)
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,w[M],iw[M],fl[M<<2];
struct zxy
{
int x,y;
zxy(int X=0,int Y=0) : x(X) , y(Y) {}
friend bool operator == (zxy a,zxy b)
{return a.x==b.x && a.y==b.y;}
friend zxy operator + (zxy a,zxy b)
{return zxy((a.x+(ll)b.x*w[a.y])%MOD,a.y+b.y);}
friend zxy operator - (zxy a,zxy b)
{return zxy((ll)(a.x-b.x+MOD)*iw[b.y]%MOD,a.y-b.y);}
}vl[M<<2],vr[M<<2];
ll qkpow(ll a,ll b)
{
ll r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
zxy getl(int i,int k)
{
if(!k) return zxy(0,0);
if(k==vl[i].y) return vl[i];
if(k<=vl[ls].y) return getl(ls,k);
return vl[ls]+(getl(rs,k-vl[ls].y+vr[ls].y)-vr[ls]);
}
zxy getr(int i,int k)
{
if(!k) return zxy(0,0);
if(k==vr[i].y) return vr[i];
if(k<=vr[rs].y) return getr(rs,k);
return vr[rs]+(getr(ls,k-vr[rs].y+vl[rs].y)-vl[rs]);
}
void up(int i)
{
if(fl[ls] || fl[rs]) {fl[i]=1;return ;}
fl[i]=0;vl[i]=vl[ls];vr[i]=vr[rs];
if(vr[ls].y<=vl[rs].y)
{
if(vr[ls]==getl(rs,vr[ls].y))
vl[i]=vl[i]+(vl[rs]-vr[ls]);
else fl[i]=1;
}
else
{
if(vl[rs]==getr(ls,vl[rs].y))
vr[i]=vr[i]+(vr[ls]-vl[rs]);
else fl[i]=1;
}
}
void ins(int i,int l,int r,int x,int y)
{
if(l==r)
{
if(y>0) vl[i]=zxy(0,0),vr[i]=zxy(y,1);
if(y<0) vl[i]=zxy(-y,1),vr[i]=zxy(0,0);
return ;
}
int mid=(l+r)>>1;
if(mid>=x) ins(ls,l,mid,x,y);
else ins(rs,mid+1,r,x,y);
up(i);
}
int k,s[50];zxy h[50];
void get(int i,int l,int r,int L,int R)
{
if(L>r || l>R) return ;
if(L<=l && r<=R) {s[++k]=i;return ;}
int mid=(l+r)>>1;
get(ls,l,mid,L,R);
get(rs,mid+1,r,L,R);
}
zxy work(int i,int k)
{
if(!k) return zxy(0,0);
if(k==h[i].y) return h[i];
if(k<=vr[s[i]].y) return getr(s[i],k);
return vr[s[i]]+(work(i-1,k-vr[s[i]].y+vl[s[i]].y)-vl[s[i]]);
}
int ask(int l,int r)
{
k=0;get(1,1,n,l,r);
for(int i=1;i<=k;i++)
{
if(fl[s[i]]) return 0;
if(h[i-1].y<vl[s[i]].y) return 0;
if(vl[s[i]]==work(i-1,vl[s[i]].y))
h[i]=vr[s[i]]+(h[i-1]-vl[s[i]]);
else return 0;
}
return h[k].y==0;
}
signed main()
{
n=read();read();
w[0]=iw[0]=1;w[1]=371;iw[1]=qkpow(371,MOD-2);
for(int i=2;i<=n;i++) w[i]=(ll)w[i-1]*w[1]%MOD;
for(int i=2;i<=n;i++) iw[i]=(ll)iw[i-1]*iw[1]%MOD;
for(int i=1;i<=n;i++) ins(1,1,n,i,read());
m=read();
while(m--)
{
int op=read(),x=read(),y=read();
if(op==1) ins(1,1,n,x,y);
else puts(ask(x,y)?"Yes":"No");
}
}
Path
题目描述
解法
设 \(d(x)\) 表示 \(x\) 到根边权的异或和,那么 \(f(x,y)\) 可以表示成 \(d(x)\oplus d(y)\),这种只和两个单点有关的形式很舒服。
使用枚举法转化问题,由于限制是点不交,考虑对于每个点 \(u\) 分别求出,从子树内选出两个点(包含 \(u\))的最大异或和;从子树外选出两个点(不包含 \(u\))的最大异或和;把这两个东西加起来求个最大值就是答案。
第一个问题是比较常规的,考虑树上启发式合并,再拿棵 \(\tt trie\) 树支持最大值查询即可,时间复杂度 \(O(n\log^2 n)\)
第二个问题也很好做,可以考虑求出全局的最优点对 \(d(p)\oplus d(q)\),这样如果一个点不在 \(p,q\) 到根的链上,最大异或和就是 \(d(p)\oplus a(q)\),那么现在我们只需要对于 \(p,q\) 这两条链求一遍。
直接按 \(\tt dfn\) 序扫链,由于只会进入其中一个子树,直接暴力插入其他子树即可,时间复杂度 \(O(n\log n)\)
总时间复杂度 \(O(n\log ^2 n)\)
总结
对于限制较弱的最值问题(比如树上求子树外的最值),可以考虑先求出全局最值点,然后收紧限制,这样可以将要考虑的范围缩小(比如本题最后就只需要计算两条链的情况)
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 30005;
const int N = 128*M;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,tot,f[M],d[M],id[M],fa[M],in[M],out[M];
int cnt,rt[M],ch[N][2],t[N],fl[M],b[M];
struct edge{int v,c,next;}e[M<<1];vector<int> v[M];
void clr() {memset(t,0,sizeof t);}
void ins(int &x,int y)
{
if(!x) x=++cnt;t[x]++;
for(int i=30,p=x;i>=0;i--)
{
int d=y>>i&1;
if(!ch[p][d]) ch[p][d]=++cnt;
t[p=ch[p][d]]++;
}
}
int ask(int x,int y)
{
int r=0;
for(int i=30;i>=0;i--)
{
int d=y>>i&1;
if(t[ch[x][d^1]]) x=ch[x][d^1],r|=(1<<i);
else x=ch[x][d];
}
return r;
}
void mg(int x,int u,int z,int i)
{
if(!x) return ;
if(i==-1)
{
in[u]=max(in[u],ask(rt[u],z));
ins(rt[u],z);return ;
}
mg(ch[x][0],u,z,i-1);
mg(ch[x][1],u,z|(1<<i),i-1);
}
void dfs(int u,int p)
{
id[++m]=u;fa[u]=p;ins(rt[u],d[u]);
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v,c=e[i].c;
if(v==p) continue;
d[v]=d[u]^c;dfs(v,u);
if(t[rt[u]]<t[rt[v]]) swap(rt[u],rt[v]);
mg(rt[v],u,0,30);in[u]=max(in[u],in[v]);
}
}
void work(int u)
{
for(int x=u;x;x=fa[x]) fl[x]=1;
clr();int mx=0;
for(int k=1;k<=m;k++)
{
int i=id[k];v[i].clear();
b[i]=fl[i]?i:b[fa[i]];
v[b[i]].push_back(d[i]);
}
for(int k=1;k<=m;k++)
{
int i=id[k];
if(!fl[i]) continue;
out[i]=max(out[i],mx);
for(int x:v[i])
mx=max(mx,ask(rt[0],x)),ins(rt[0],x);
fl[i]=0;v[i].clear();
}
}
signed main()
{
n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read(),c=read();
e[++tot]=edge{v,c,f[u]},f[u]=tot;
e[++tot]=edge{u,c,f[v]},f[v]=tot;
}
dfs(1,0);clr();
int A=0,B=0,p=0,q=0,ans=0;
for(int i=1;i<=n;i++)
{
int v=ask(rt[0],d[i]);ins(rt[0],d[i]);
if((A^B)<v) A=d[i],B=v^d[i],p=i;
}
clr();memset(out,-1,sizeof out);
for(int i=1;i<=n;i++) if(B==d[i]) q=i;
work(p);work(q);
for(int i=1;i<=n;i++)
if(out[i]==-1) out[i]=A^B;
for(int i=2;i<=n;i++)
ans=max(ans,in[i]+out[i]);
printf("%d\n",ans);
}
Treequery
题目描述
解法
注意题目是求公共部分(我一开始读成了求并),我们按照 \(u\) 与 \([l,r]\) 中点的位置关系来分类讨论:
- 如果 \([l,r]\) 中的点全部处于 \(u\) 的子树内,我们找到 \([l,r]\) 中 \(\tt dfs\) 序最小和最大的点,求出它们的 \(\tt lca\) 记为 \(x\),根据虚树的有关理论,\((u,x)\) 的距离就是答案。
- 如果 \([l,r]\) 中的点分居 \(u\) 的子树内和子树外,答案为 \(0\)
- 如果 \([l,r]\) 中的点都处于 \(u\) 的子树外,那么 \(u\) 到 \([l,r]\) 的虚树的最短距离就是答案。
第三种情况有点难做,我们不妨再对虚树和 \(u\) 的位置关系进行分类讨论:
- 如果 \(u\) 不在虚树根的子树内,那么答案是 \(u\) 和虚树根的距离。
- 否则尝试把 \(u\) 插入虚树,即找到 \(\tt dfs\) 序的前驱后继,和 \(u\) 可以求出两个 \(\tt lca\),记为 \(x_1,x_2\),那么 \(u\) 和 \(x_1,x_2\) 距离的最小值就是答案。
在主席树上二分就可以查询 \(\tt dfs\) 序的前驱后继,时间复杂度 \(O(n\log n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,tot,f[M],fa[M][20],in[M],out[M],dep[M],id[M];
int cnt,dis[M],rt[M],s[20*M],ls[20*M],rs[20*M];
struct edge{int v,c,next;}e[M<<1];
void dfs(int u,int p)
{
fa[u][0]=p;dep[u]=dep[p]+1;
id[in[u]=++cnt]=u;
for(int i=1;i<20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v,c=e[i].c;
if(v==p) continue;
dis[v]=dis[u]+c;
dfs(v,u);
}
out[u]=cnt;
}
int lca(int u,int v)
{
if(dep[u]<=dep[v]) swap(u,v);
for(int i=19;i>=0;i--)
if(dep[fa[u][i]]>=dep[v])
u=fa[u][i];
if(u==v) return u;
for(int i=19;i>=0;i--)
if(fa[u][i]^fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void ins(int &x,int y,int l,int r,int p)
{
x=++cnt;s[x]=s[y]+1;
ls[x]=ls[y];rs[x]=rs[y];
if(l==r) return ;
int mid=(l+r)>>1;
if(mid>=p) ins(ls[x],ls[y],l,mid,p);
else ins(rs[x],rs[y],mid+1,r,p);
}
int ask(int x,int y,int l,int r,int L,int R)
{
if(L>r || l>R) return 0;
if(L<=l && r<=R) return s[x]-s[y]>0;
int mid=(l+r)>>1;
return ask(ls[x],ls[y],l,mid,L,R)|
ask(rs[x],rs[y],mid+1,r,L,R);
}
int getp(int x,int y,int l,int r,int p)
{
if(l>p || s[x]-s[y]==0) return 0;
if(l==r) return id[l];
int mid=(l+r)>>1,t=0;
if(t=getp(rs[x],rs[y],mid+1,r,p)) return t;
return getp(ls[x],ls[y],l,mid,p);
}
int gets(int x,int y,int l,int r,int p)
{
if(r<p || s[x]-s[y]==0) return 0;
if(l==r) return id[l];
int mid=(l+r)>>1,t=0;
if(t=gets(ls[x],ls[y],l,mid,p)) return t;
return gets(rs[x],rs[y],mid+1,r,p);
}
signed main()
{
n=read();m=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read(),c=read();
e[++tot]=edge{v,c,f[u]},f[u]=tot;
e[++tot]=edge{u,c,f[v]},f[v]=tot;
}
dfs(1,0);
for(int i=1;i<=n;i++)
ins(rt[i],rt[i-1],1,n,in[i]);
for(int i=1,ans=0;i<=m;i++)
{
int u=read(),l=read(),r=read();
u^=ans;l^=ans;r^=ans;l--;
int A=ask(rt[r],rt[l],1,n,1,in[u]-1);
int B=ask(rt[r],rt[l],1,n,in[u],out[u]);
int C=ask(rt[r],rt[l],1,n,out[u]+1,n);
if(B&(A|C)) {printf("%d\n",ans=0);continue;}
if(A+C==0)
{
int v1=getp(rt[r],rt[l],1,n,out[u]);
int v2=gets(rt[r],rt[l],1,n,in[u]);
ans=dis[lca(v1,v2)]-dis[u];
printf("%d\n",ans);continue;
}
int v1=gets(rt[r],rt[l],1,n,1);
int v2=getp(rt[r],rt[l],1,n,n);
int x=lca(v1,v2);
if(!(in[x]<=in[u] && in[u]<=out[x]))
{
ans=dis[x]+dis[u]-2*dis[lca(x,u)];
printf("%d\n",ans);continue;
}
ans=0x3f3f3f3f;
if(A)
{
int v=getp(rt[r],rt[l],1,n,in[u]-1);
ans=min(ans,dis[u]-dis[lca(u,v)]);
}
if(C)
{
int v=gets(rt[r],rt[l],1,n,out[u]+1);
ans=min(ans,dis[u]-dis[lca(u,v)]);
}
printf("%d\n",ans);
}
}