1. 程式人生 > >[luogu]P3629 [APIO2010]巡邏

[luogu]P3629 [APIO2010]巡邏

原題連結:P3629 [APIO2010]巡邏

題意

給定一棵樹,每次要走過每條邊。

現在要求加$k$條邊,使得走過每條邊後走過的距離最小,求這個最小值。

 

分析

首先不考慮加邊,也就是說$k=0$時,是一棵樹,顯然每條邊要經過兩次。

考慮加一條邊,很容易發現形成的環上的點只需要經過一遍,最終經過的距離就是$((n-1)\times 2-len+2)$,$len$是最大環的長度,明顯是樹的直徑$+1$。

考慮加兩條邊對答案有什麼影響。

分兩類考慮:

1.第一次形成的環和第二次沒重疊:答案繼續減去$len_2+2$.

2.第一次形成的環和第二次有重疊:畫過圖就很容易發現,重疊部分是經過了兩次的,答案就是$((n-1)\times 2-len_1+2_len_2+2+k)$,$k$是這個重疊部分的長度。

那麼我們就考慮怎麼求出這個重疊部分了。

考慮差分。

第一次已經減去了這個長度,第二次減去這個長度的相反數就相當於加回上了這個長度。

那麼我們只需要把直徑上的邊權置為$-1$,再求一次樹的直徑即可。

注意第二次的直徑可以為$0$(自環)。

實現的方式

我們常用dfs寫樹的直徑,但是我們發現這樣子是不能跑負權圖的,因為dfs不能統計負權的情況。

而樹形dp求樹的直徑雖然可以跑負權圖,但是卻很難轉移出其他量。

我們結合這兩種演算法,在第一次時用dfs求出直徑,並儲存出直徑上的邊,第二次再跑一次樹形dp就可以了。

儲存直徑上的邊的時候,我們需要一點小技巧。

運用費用流中求增廣路的方法,我們儲存每個節點的祖先節點(從哪裡轉移過來),並利用成對儲存的方法儲存每條邊和它的反向邊。

具體實現見程式碼。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+1000;
int read(){
    char c;int num,f=1;
    while(c=getchar(),!isdigit(c))if(c=='-')f=-1;num=c-'0';
    while(c=getchar(), isdigit(c))num=num*10+c-'0';
    return f*num;
}
int n,k,ans,pre[N*2],maxn,id,dis[N*2
]; int head[N],nxt[N*2],ver[N*2],edge[N*2],tot=1; void add_edge(int u,int v,int w){ ver[++tot]=v;edge[tot]=w;nxt[tot]=head[u];head[u]=tot; ver[++tot]=u;edge[tot]=w;nxt[tot]=head[v];head[v]=tot; } void dfs(int x,int val,int pr){ if(val>maxn){ maxn=val; id=x; } for(int i=head[x];i;i=nxt[i]){ if(ver[i]==pr)continue; pre[ver[i]]=i; dfs(ver[i],val+edge[i],x); } } int dfs_get_diam(){ dfs(1,0,1); memset(pre,0,sizeof(pre)); maxn=0; dfs(id,0,id); return maxn; } void dp_get_diam(int x,int fa){ for(int i=head[x];i;i=nxt[i]){ if(ver[i]==fa)continue; dp_get_diam(ver[i],x); maxn=max(maxn,dis[x]+dis[ver[i]]+edge[i]); dis[x]=max(dis[x],dis[ver[i]]+edge[i]); } } int main() { n=read();k=read(); for(int i=1;i<n;i++){ int u,v; u=read();v=read(); add_edge(u,v,1); ans+=2; } ans-=dfs_get_diam()-1; if(k==1){ printf("%d\n",ans); return 0; } for(int i=pre[id];i;i=pre[ver[i^1]]){ edge[i]=-1; edge[i^1]=-1; } maxn=0; dp_get_diam(1,1); ans-=maxn-1; printf("%d\n",ans); return 0; }
View Code