洛谷 P3629 【[APIO2010]巡逻】

树的直径(DFS+DP)

首先考虑$k=0$,也就是单只有一棵树的时候,由DFS深度优先遍历可知,搜一遍正好每条边都进出一次,答案是$2(n-1)$

然后考虑$k=1$的情况,也就是出现有且只有一个环,不难想到,环上各边都只需经过一次,因此只需求出树的直径为$L1$,则答案为$2(n-1)-(L1-1)$

最后比较复杂的是$k=2$的情况了。容易发现形成两个环,它们的公共边任然要经过两次,其他环上的边都仅需一次,非环上边还是两次。因此可以在第二次求树的直径之前把第一次树的直径路径上的边权改为$-1$,表示已经是第一个环上的边了。设第二次用DP做出来的树的直径为$L2$,那么第一次答案减掉$(L1-1)$后公共边部分表示经过一次,再减掉$(L2-1)$后,相当于把公共边少走一次的加回来,表示公共边经过两次。

PS:第一次全是正权值的边,树的直径可以用DFS;第二次由负边,只能用DP

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define fr(i,a,b) for(int i=a;i<=b;i++)
#define fo(i,a,b) for(int i=a;i>=b;i--)
#pragma GCC optimize(2)
const int M=100005;
int n,k,Max,pos,ans;
int head[M],Next[2*M],ver[2*M],edge[2*M],tot,v[M],d[M],fa[M];
void add(int x,int y) {
	Next[++tot]=head[x];
	head[x]=tot;
	ver[tot]=y;
	edge[tot]=1;
}
void dfs(int x) {
	v[x]=1;
	if(Max<d[x]) {
		Max=d[x];
		pos=x;
	}
	for(int i=head[x];i;i=Next[i]) {
		int y=ver[i];
		if(v[y]) continue;
		d[y]=d[x]+1;
		fa[y]=i; //记录y点是从那条边过来的
		dfs(y);
	}
}
void dp(int x) {
	v[x]=1;
	for(int i=head[x];i;i=Next[i]) {
		int y=ver[i],z=edge[i];
		if(v[y]) continue;
		dp(y);
		Max=max(Max,d[x]+d[y]+z);
		d[x]=max(d[x],d[y]+z);
	} //d[x]表示以x为根的子树到叶子的最长距离
}
int main(){
	std::ios::sync_with_stdio(false);
	tot=1;
	scanf("%d%d",&n,&k);
	fr(i,1,n-1) {
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	dfs(1);
	int x=pos;
	d[x]=fa[x]=0;
	Max=0;
	memset(v,0,sizeof(v));
	dfs(x);
	int y=pos;
	ans=(n-1)*2-Max+1;
	if(k==2) {
		while(fa[y]) { //上一次树的直径的边 边权取反
			edge[fa[y]]=edge[fa[y]^1]=-1;
			y=ver[fa[y]^1]; //对称边的终点就是下一个顶点
		}
		memset(v,0,sizeof(v));
		memset(d,0,sizeof(d));
		Max=0;
		dp(1);
		ans-=Max-1;
	}
	printf("%d\n",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/shaozhihang/p/15607179.html