Luogu P3629 [APIO2010]巡逻

毒瘤(树形?)dp,调了本蒟蒻一上午+几乎一个下午

题目链接

(详细解法可自行查看题解)

求树的直径(一棵树内最长的一条路径——两遍dfs或两遍bfs。先随便找一个点s,求出离它最远的点t,再从那个最远的点t求出离t最远的点d,t到d即为树的直径。

我们先来看加边是怎样影响路径长度的:
在这里插入图片描述
考虑从一号点走到四号点的路径,从一号点走到四号点需经过每条边两次,距离即2*3=6。
在这里插入图片描述
在一和四之间修一条路后,不用像刚才那样原路返回,原来的路只需经过一次,距离即为3,再加上需经过新加的边,则为3+1=4

由于只可能修1或2条道路,可分开考虑。
修一条路时,考虑到在越长的路径两端修路可缩短的路程越多,故找树的直径即可。答案为(图中节点数-1)*2-(直径长度-1)。
修两条路时,再次考虑树的直径。但这两条路可能会有重边,即
在这里插入图片描述
在原有的树上新建1到4,5到6两条边,二者同时经过了2到3这条边。而实际答案中2到3这条边便会经过两次,这时我们就将第一次求树的直径路径上的边权都由1改为-1,这样若第二次求直径时若有与第一次求直径时重复的边,在求答案的算式中该边就会被正确统计,即被经过两次。
答案=(图中节点数-1)*2-(第一次直径长度-1)-(第二次直径长度-1)。在直径中该重复边权值为-1,与括号前面的减号抵消,相对于还是加了两次。

但是,bfs,dfs不能处理负权边(坑死我了)。需要运用树形dp的思想,由底部向根部维护出每个节点的子树中,端点为该点的第一长与第二长边,二者相加更新答案。
还有,在修改边权为-1时,可记录每个点的父亲节点以保存路径。重点是,由于边为双向,所以i号边及i^1号边都要修改,而我建边时序号是从1开始的,双向边的一组序号为1与2,3与4等等,但i,i ^1这样的一组边实际为0与1,2与3等等。所以……
蒟蒻被虐出翔了

CODE:

#include<bits/stdc++.h>
using namespace std;

const int maxn=1000005;
int n,m,cnt,la,lb,md,t,k;
int head[maxn],d[maxn],f[maxn],s[maxn],l[maxn];
bool vis[maxn];

int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') f=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') {x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

struct edge
{
	int v,nxt,w;
}e[maxn];

void add(int u,int v,int w)
{
	e[++cnt].nxt=head[u];
	e[cnt].w=w;
	e[cnt].v=v;
	head[u]=cnt;
}

void dfs(int u,int fa)
{
	if(d[u]>d[t]) t=u;
	vis[u]=1;f[u]=fa;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].v,w=e[i].w;
		if(vis[v]) continue;
		d[v]=d[u]+w;
		dfs(v,u);
	}
	vis[u]=0;
}

void dp(int u)
{
	vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].v,w=e[i].w;
		if(vis[v]) continue;
		dp(v);
		if(d[u]+d[v]+w>md) md=d[u]+d[v]+w;
		d[u]=max(d[u],d[v]+w);
	}
}

int main()
{
	//freopen("input.txt","r",stdin);
	n=read(),m=read();
	int x,y;
	for(int i=1;i<n;i++)
	{
		x=read(),y=read();
		add(x,y,1),add(y,x,1);
	}
	dfs(1,0);
	d[t]=f[t]=md=0;
	int tt=t;t=0;
	dfs(tt,0);
	la=d[t];
	if(m==1) printf("%d",2*(n-1)-la+1);
	else
	{
		while(f[t]) 
		{
			for(int i=head[t];i;i=e[i].nxt)
			{
				int v=e[i].v;
				if(v!=f[t]) continue;
				e[i].w=e[(i%2==1 ? i+1:i-1)].w=-1;
				break;
			}
			t=f[t];
		}
		memset(d,0,sizeof(d));
		t=0;dp(1);lb=md;
		printf("%d",2*n-la-lb);
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值