D50 树的直径 两次DFS+树形DP P3629 [APIO2010] 巡逻

D50 树的直径 P3629 [APIO2010] 巡逻_哔哩哔哩_bilibili

 

P3629 [APIO2010] 巡逻 - 洛谷

给定一颗有根树,任意添加 K 条边(K=1 或 2),求从根出发最后回到根的最小路程。要求添加的边只走一次。

image

在 (a) 中,新建了一条道路,总的距离是 11。在 (b) 中,新建了两条道路,总的巡逻距离是 10。在 (c) 中,新建了两条道路,但由于巡警车要经过每条新道路正好一次,总的距离变为了 15。

思路

k=1 的情况
没有环时,每条树边走两边,所以 $2(n+1)$。修了一条道路后,这棵树中就出现了一个环,我们要将这个环只走一遍。假设这个环中除去新加的边长度为 $d1$,$d1$ 就是树的直径,现在直径的边只走一遍。所以答案为 $2(n−1)−d1+1$。

k=2 的情况

image

我们从红色点开始走,黄色边为新修建的边,红色边为重叠边,易知红边要走两次。 所以计算答案的时候需要减去两环中的黑边之和。
设重叠边的长度为 $x$,则右环中的黑边之和 $a=d1-x$。
把直径边的边权均取反(即 $-1$)后,树形 DP 求一遍树的直径 $d2$,此时 $d2$ 就是左环除去黄边的长度,由于红边为 $-x$,所以左环中的黑边之和 $b=d2+x$。
所以答案为 $2(n−1)−(a+b)+2=2(n−1)−(d1+d2)+2$。

代码实现
两次DFS求直径 $d1$,记录直径路径 $pre$
直径染色
树形DP求直径 $d2$

// 两次DFS+树形DP O(n)
#include<bits/stdc++.h>
using namespace std;

const int N=100005;
int h[N],to[N<<1],ne[N<<1],w[N<<1],idx;
void add(int u,int v){
  to[++idx]=v,w[idx]=1,ne[idx]=h[u],h[u]=idx;
}
int d[N],pre[N],col[N];
int n,k,p,d1,d2;

void dfs(int u,int fa){
  if(d[u]>d[p]) p=u; //记录直径端点
  pre[u]=fa;         //记录直径路径
  for(int i=h[u];i;i=ne[i]){
    int v=to[i];
    if(v!=fa){
      d[v]=d[u]+w[i]; //记录v到根的距离
      dfs(v,u);
    }
  }
}
void dfs2(int u,int fa){
  for(int i=h[u];i;i=ne[i]){
    int v=to[i];
    if(v!=fa){
      if(col[u] && col[v]) w[i]=-1; //直径边取反
      dfs2(v,u);
      d2=max(d2,d[u]+d[v]+w[i]); //记录经过u的直径
      d[u]=max(d[u],d[v]+w[i]);  //记录u的最长链长度
    }
  }
}
int main(){
  scanf("%d%d",&n,&k);
  for(int i=1,u,v;i<n;++i){
    scanf("%d%d",&u,&v);
    add(u,v),add(v,u);
  }
  dfs(1,0); d[p]=0;
  dfs(p,0); d1=d[p]; //两次DFS求直径d1,记录直径路径pre
  
  if(k==1){
    cout<<2*(n-1)-d1+1;
    return 0;
  }
  for(int i=p;i;i=pre[i])col[i]=1; //直径染色
  memset(d,0,sizeof d);
  dfs2(1,0); //树形DP求直径d2
  cout<<2*(n-1)-(d1+d2)+2;
}

 

posted @ 2024-09-17 17:55  董晓  阅读(281)  评论(0)    收藏  举报