[做题笔记] 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);
	}
}
posted @ 2022-06-27 17:37  C202044zxy  阅读(409)  评论(0编辑  收藏  举报