[NOIP2024] 树上查询 题解

[NOIP2024] 树上查询 题解


知识点

LCA,偏序,扫描线,线段树。

分析

法 1

考虑不断从树底往上合并,那么我们就可以得到一堆连续段,在每个节点 \(u\) 处记加入 \(u\) 后哪个连续段连了起来,记为 \([l_u,r_u]\),然后就可以做扫描线。

第一步 DSU 求出即可,后面可以用三维偏序卡或者降到二维偏序。

法 2

首先有:

\[\mathrm{LCA}^*(l,r) = \min_{i=l}^{r-1} \mathrm{dep}_{\mathrm{lca(i,i+1)}} \\ \]

转化可得:

\[\max_{i=l}^{r-k+1} \min_{j=i}^{i+k-1} \mathrm{dep}_j \\ \]

那么其实就是把 \(\mathrm{dep}\) 连着做 \(k\)\(\mathrm{dep}_i \gets \min(\mathrm{dep}_i,\mathrm{dep}_{i+1})\)​,然后求区间最小值。

那么考虑将询问按 \(k\) 排序,然后维护连续段合并操作即可。注意 \(k=1\) 需要特判。

具体的,发现一个连续段的长度变化与其值和左右两边连续段大小比较有关,每次只会是 \(-1/0/1\),那么我们维护这个东西即可。

代码

只有法 1。

constexpr int N(5e5+10);

bool vis[N],con[N];
int n,Q,idx,tot;
int dl[N],dr[N],fa[N],ans[N],dep[N],dfn[N],siz[N],son[N];
vector<int> g[N];
struct node {
	int l,r,d;
} a[N<<1];
struct query {
	int l,r,k,idx;
} q[N];
struct DSU {
	int fa[N],siz[N],l[N],r[N];
	
	void Init() {
		FOR(i,1,n)siz[fa[i]=l[i]=r[i]=i]=1;
	}
	
	int Get(int x) {
		return fa[x]^x?fa[x]=Get(fa[x]):x;
	}
	
	bool Merge(int u,int v) {
		if((u=Get(u))==(v=Get(v)))return 0;
		if(siz[u]<siz[v])swap(u,v);
		return siz[fa[v]=u]+=siz[v],tomin(l[u],l[v]),tomax(r[u],r[v]),1;
	}
	
} dsu;
struct SEG {
#define ls (p<<1)
#define rs (p<<1|1)
#define mid ((l+r)>>1)
	struct node {
		int mx,tag;

		void down(int _tag) {
			tomax(mx,_tag),tomax(tag,_tag);
		}

	} tr[N<<2];

	void Up(int p) {
		tr[p].mx=max(tr[ls].mx,tr[rs].mx);
	}

	void Down(int p) {
		if(tr[p].tag)tr[ls].down(tr[p].tag),tr[rs].down(tr[p].tag),tr[p].tag=0;
	}

	void Build(int p=1,int l=1,int r=n) {
		tr[p]= {0,0};
		if(l==r)return;
		Build(ls,l,mid),Build(rs,mid+1,r);
	}

	void toMax(int L,int R,int d,int p=1,int l=1,int r=n) {
		if(L<=l&&r<=R)return tr[p].down(d);
		Down(p);
		if(L<=mid)toMax(L,R,d,ls,l,mid);
		if(mid<R)toMax(L,R,d,rs,mid+1,r);
		Up(p);
	}

	int Max(int L,int R,int p=1,int l=1,int r=n) {
		if(L<=l&&r<=R)return tr[p].mx;
		Down(p);
		if(R<=mid)return Max(L,R,ls,l,mid);
		if(mid<L)return Max(L,R,rs,mid+1,r);
		return max(Max(L,R,ls,l,mid),Max(L,R,rs,mid+1,r));
	}

#undef ls
#undef rs
#undef mid
} seg;

bool rela(int u,int pa) {
	return dl[pa]<=dl[u]&&dr[u]<=dr[pa];
}

void solve(int u,int son,int fa) {
	if(u>1&&!con[u-1]&&!rela(u-1,son)&&rela(u-1,fa))
		dsu.Merge(u-1,u),a[++tot]={dsu.l[dsu.Get(u)],dsu.r[dsu.Get(u)],dep[fa]},con[u-1]=1;
	if(u<n&&!con[u]&&!rela(u+1,son)&&rela(u+1,fa))
		dsu.Merge(u,u+1),a[++tot]={dsu.l[dsu.Get(u)],dsu.r[dsu.Get(u)],dep[fa]},con[u]=1;
}

void dfs(int u) {
	dep[dfn[dl[u]=++idx]=u]=dep[fa[u]]+1,siz[u]=1,son[u]=0,a[++tot]={u,u,dep[u]};
	for(int v:g[u])if(v^fa[u])fa[v]=u,dfs(v),siz[u]+=siz[v],son[u]=(siz[son[u]]>siz[v]?son[u]:v);
	dr[u]=idx,solve(u,0,u);
	for(int v:g[u])if(v!=fa[u]&&v!=son[u])FOR(i,dl[v],dr[v])solve(dfn[i],v,u);
}

int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	/*Input*/
	I(n);
	FOR(i,2,n) {
		int u,v;
		I(u,v),g[u].push_back(v),g[v].push_back(u);
	}
	/*Query*/
	I(Q);
	FOR(i,1,Q)I(q[i].l,q[i].r,q[i].k),q[i].idx=i;
	/*Build*/
	dsu.Init(),dfs(1);
	/*Left*/
	seg.Build();
	sort(a+1,a+tot+1,[&](node a,node b) { return a.l<b.l; });
	sort(q+1,q+Q+1,[&](query a,query b) { return a.l<b.l; });
	int it(1);
	FOR(i,1,Q) {
		while(it<=tot&&a[it].l<=q[i].l)seg.toMax(a[it].l,a[it].r,a[it].d),++it;
		tomax(ans[q[i].idx],seg.Max(min(q[i].l+q[i].k-1,n),n));
	}
	/*Middle*/
	seg.Build();
	sort(a+1,a+tot+1,[&](node a,node b) { return a.r-a.l+1>b.r-b.l+1; });
	sort(q+1,q+Q+1,[&](query a,query b) { return a.k>b.k; });
	it=1;
	FOR(i,1,Q) {
		while(it<=tot&&a[it].r-a[it].l+1>=q[i].k)seg.toMax(a[it].l,a[it].l,a[it].d),++it;
		tomax(ans[q[i].idx],seg.Max(q[i].l,q[i].r-q[i].k+1));
	}
	/*Output*/
	FOR(i,1,Q)O(ans[i],'\n');
	return 0;
}

反思

  1. 尝试换用多种视角来分析问题,比如从 \([l,r]\) 整个区间直接查找最小值,还是从最小值来找到符合条件的区间。
  2. 偏序问题可以尝试用一些方法把不必要的维数降掉。
posted @ 2025-11-17 20:44  Add_Catalyst  阅读(12)  评论(0)    收藏  举报