毒瘤(树形?)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;
}