[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;
}
反思
- 尝试换用多种视角来分析问题,比如从 \([l,r]\) 整个区间直接查找最小值,还是从最小值来找到符合条件的区间。
- 偏序问题可以尝试用一些方法把不必要的维数降掉。

浙公网安备 33010602011771号