P3629 [APIO2010] 巡逻

P3629 [APIO2010] 巡逻

看题解大佬们都用数学推理求两遍直径的方法,然而萌新并不会这么高级的方式,只能大力分讨了。

思路

对于 \(k=1\) 的情况,加上一条边时,树上出现了一条环且长为 \(S\),环上的原路径都可以少走一遍,再算上新路径要走一遍。此时答案为 \(2 \times (n-1) - (S-1) + 1\)
我们需要令 \(S-1\) 尽可能大,即原路径的那条链尽可能长,那么应该取树的直径。

对于 \(k=2\) 的情况,加上两条边会出现两条环。由于出现的每条环长度比所选链长多一,所以只考虑树上所选链的情况即可。
若想使总步数尽可能小,那么选出的两条链要尽可能多的覆盖树上的路径。且在每条环上,我们都会把环上所有边都经过一遍再走出这条环,两条链重合的部分会重复走,所以两条链的重合部分要尽可能少
我们巡逻的策略是一但遇到不在环上的点,就优先去走它,然后再想办法走到环上。所以实际答案为 \(2 \times (n-1) - S_{并}+S_{交}+2\)

贪心地取,我们取到的其中一条链一定是树的直径。
粗略证明:
对于我们选的两条链,它们分别越长一定不劣。如果两条链没有重合部分,那么其中一条上半部分选中两条链中间的部分,接到另一条的下半部分,答案不劣。由于两条链都要尽量长,那么第一条链如此操作一定能够取到直径。

第二条链在取的时候我们分类讨论。
设第一条链选出的点共有 \(m\) 个,点集为 \(T\),按照深度顺序排列。

如果第二条链与第一条链没有重叠部分,那么我们对每个 \(T_i\) 求其子树直径即可。
如果第二条链与第一条链有重叠部分,我们找到重合部分的两个端点 \(T_i\)\(T_j\)。那么我们将这条链分为一段重合部分、和两段非重合部分。可以钦定 \(i < j\),那么 \(S_{重} = j-i\)

我们需要使 \(S_{并}-S_{交}\) 最大,那么分别取到 \(T_i\)\(T_j\) 的子树内距其最深的深度 \(maxdep{T_i}\)\(maxdep{T_j}\) 一定更优。
此时答案为 \(2 \times (n-1) -(m-1)-[ maxdep{T_1}+maxdep{T_2} - (j-i)] +2\),找到\(\max\{maxdep{T_1}+maxdep{T_2} - (j-i)\}\) 即可。

\(g_i = maxdep{T_i}+i\)\(f_j = maxdep{T_j}-j\),此时转化为求 \(\max \{ g_i+f_j \}\)\(i<j\))。
只需要 \(O(n)\) 扫一遍 \(f_j\),对于每个 \(f_j\) 维护出最大的 \(g_i\)(在求第二条链的时候顺便维护即可)。

代码

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
template <typename T>
inline void read(T&x){ //快读
	int w = 0;x = 0;
	char ch = getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') w = 1;
		ch = getchar();
	}
	while(ch>='0' && ch<='9'){
		x = (x<<1)+(x<<3)+(ch^48);
		ch = getchar();
	}
	if(w) x = ~x+1;
}
template <typename T,typename...Args>
inline void read(T&t,Args&...args){
	read(t);read(args...);
}
template <typename T>
inline T Max(T x,T y){ return (x > y ? x : y); }
const int N = 1e5+10;
int n,m,cnt;
struct{
	int to,nex;
}edge[N<<1];
int head[N],edge_num;
inline void add(int x,int y){// 存边
	edge[++edge_num].to = y;
	edge[edge_num].nex = head[x];
	head[x] = edge_num;
}
int dis[N],fa[N],vis[N];
inline int bfs(int s,int flag){// 用来找以 s 为根最深的深度对应的点
	memset(dis,0,sizeof(dis));
	queue <int> q;
	q.push(s);
	int idx = 0;
	if(flag) fa[s] = 0; //只有选直径的时候才标记父亲
	while(!q.empty()){
		int now = q.front();
		q.pop();
		for(int i=head[now];i;i=edge[i].nex){
			int tto = edge[i].to;
			if(dis[tto] || tto==s || vis[tto]) continue;
			if(flag) fa[tto] = now;
			dis[tto] = dis[now]+1;
			if(dis[tto]>dis[idx]) idx = tto;
			q.push(tto);
		}
	}
	return idx;
}
int main(){
	// freopen("data.out","r",stdin);
	// freopen("put.out","w",stdout);

	read(n,m);
	for(int i=1,u,v;i<n;++i){
		read(u,v);
		add(u,v); add(v,u);
	}
	int t1 = bfs(1,1),t2 = bfs(t1,1); //求直径
	if(m==1){// k==1
		printf("%d",2*(n-1)-dis[t2]+1);
		return 0;
	}
    // 把直径上的点标记,强制第二次找最深的深度时,只在直径上的点的子树内
	for(int i=t2;i;i=fa[i]) vis[i] = 1;
	int maxdis = dis[t2],maxn = 0;
	for(int i=t2,f,g,maxg=0;i;i=fa[i]){
		vis[i] = 0;// 防止第一个儿子可能统计不到
		t1 = bfs(i,0);// 最大深度

		++cnt;
        // 求 g 和 f
		g = dis[t1]+cnt;
		f = dis[t1]-cnt;
		// 求 max{g + f}
		maxn = Max(maxn,f+maxg);
		maxg = Max(maxg,g);

		t2 = bfs(t1,0);// 求其子树直径
		vis[i] = 1;// 记得加回来
		maxn = Max(maxn,dis[t2]);// 没有重合部分的情况
	}
	printf("%d",2*(n-1)-maxdis-maxn+2);// 答案

	fclose(stdin);
	fclose(stdout);
	return 0;
}
posted @ 2025-03-09 12:04  Tmbcan  阅读(36)  评论(0)    收藏  举报