[JOI2019 Final] 独特的城市 题解

好题当赏!


容易证明:一个点的独特城市一定在从这个点出发的最长链上,而树上距离点 \(i\) 最远的点,一定是树的直径的两个端点之一。

于是,我们找出一条树的直径,并从它的两个端点各做一次 \(dfs\),企图找到最优解。在 \(dfs\) 时,我们默认此时作为根节点的 \(rt\) 距离所有点最远。显然,两次 \(dfs\) 中,每个点的答案都至少有一个 \(0\),所以答案即为两次计算结果中非 \(0\) 的那个。

我们尝试构造一种通过维护单调栈求解的方案,如下(下设每个点子树内部最长链长度为 \(mx_i\),次长链长度为 \(nx_i\),长儿子为 \(sn_i\),每个点深度为 \(d_i\)):

  1. 有父亲的话插入父亲。

  2. 将单调栈中与当前点 \(u\) 距离 \(\le nx_u\) 的点弹出,同时修改答案。
    此时,我们构建出的单调栈是对于长儿子的,所以选择次长链(所以该问题中,次长链不经过长儿子)。

  3. 遍历长儿子。

  4. 将单调栈中与当前点 \(u\) 距离 \(\le mx_u\) 的点弹出,同时修改答案。
    此时就是对于 \(u\) 和短儿子的状态了。

  5. 此时的答案就是该次 \(dfs\)\(u\) 的答案,记录。

  6. 遍历其他短儿子。

  7. 若父亲还活着,弹出父亲。

时间复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,tp,st[N],tc[N],ans[N];
int tl,tr,c[N],sn[N],d[N],mx[N];
vector<int>g[N];int nx[N],nw;
int dfs(int x,int fa){
	d[x]=d[fa]+1;int re=x;
	for(auto y:g[x]) if(y!=fa){
		int as=dfs(y,x);
		if(d[as]>d[re]) re=as;
	}return re;
}void dfs2(int x,int fa){
	mx[x]=d[x]=d[fa]+1,nx[x]=sn[x]=0;
	for(auto y:g[x]) if(y!=fa){
		dfs2(y,x);if(mx[x]<mx[y])
			nx[x]=mx[x],sn[x]=y,mx[x]=mx[y];
		else nx[x]=max(nx[x],mx[y]);
	}
}void dfs3(int x,int fa){
	if(fa) nw+=(tc[c[st[++tp]=fa]]++==0);
	while(tp&&d[x]*2<=nx[x]+d[st[tp]])
		nw-=(--tc[c[st[tp--]]]==0);
	if(sn[x]) dfs3(sn[x],x);
	while(tp&&d[x]*2<=mx[x]+d[st[tp]])
		nw-=(--tc[c[st[tp--]]]==0);
	ans[x]=max(ans[x],nw);
	for(auto y:g[x]) if(y!=fa&&y!=sn[x]) dfs3(y,x);
	if(fa&&st[tp]==fa) nw-=(--tc[c[st[tp--]]]==0);
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0),cin>>n>>m;
	for(int i=1,u,v;i<n;i++)
		cin>>u>>v,g[u].push_back(v),g[v].push_back(u);
	for(int i=1;i<=n;i++) cin>>c[i];
	tl=dfs(1,0),dfs2(tr=dfs(tl,0),0);
	dfs3(tr,0),dfs2(tl,0),dfs3(tl,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<"\n";
	return 0;
}
posted @ 2025-07-02 18:00  长安一片月_22  阅读(7)  评论(0)    收藏  举报