題目鏈接:P3629 [APIO2010]巡邏
首先看題,從1號結點開始,全部遍歷並回到1號結點會恰好經過所有的邊兩次,這樣總長度爲 。
那麼如果建立一條路以後,由題意可知新道路必須經過恰好一次,也只能經過一次,所以新路從x到y形成一個環,這樣就可以用新路的權值1來代替老路徑中從x到y的路徑1次,也就是說從x到y之間的路徑在建立了新路以後只需要再經過一次就行了。所以我們要求最短,肯定要把整棵樹上的最長鏈也就是樹的直徑省掉答案最優。所以當k = 1 時,我們找到最長鏈,在這兩個端點之間加一條新路,設直徑爲L,則答案(+1是因爲要走一次新加的新路)。
當k = 2時,我們按照剛剛的理論,再求一次最長鏈。這樣形成兩個環,第一條肯定不能跟第二條一樣,所以我們求完第一條以後要把第一條最長鏈給刪去。這裏找到第一次的最長鏈,遍歷一遍,將值從1變成-1。
如果第二條與第一條鏈沒有重疊,那麼就跟第一條鏈一樣,直接減去即可。
若第二條連與第一條鏈有重疊,那麼由於本應只走一次的路徑會走兩次,多減去的值需要加回來,但是由於第一條鏈上的所有邊已經取反爲-1,這樣減去負數又相當於加上這個數,所以兩種答案是一樣的。
最終的答案爲
總時間複雜度爲
這裏將第一條鏈的值全部賦值爲-1的時候用到了一個位運算的小技巧 —— 成對變換。
要注意成對變換的時候要初始化tot = 1
當n爲偶數的時候,n XOR 1 = n + 1 當n爲奇數的時候,n XOR 1 = n - 1
利用這個小技巧,我們在使用鏈式前向星的時候初始化tot等於1,這樣每天無向邊看成的兩條有向邊會成對存儲在ver和edge數組的下標爲2和3,4和5…的位置上,通過對下標進行XOR1運算,就可以直接定位到當前邊的反向的邊。換句話說,如果ver[i]是第i條邊的終點,那麼ver[i xor1]就是第i條邊的起點。
——《算法競賽進階指南》
放到代碼裏就是這樣:
while(fa[q]){//成對變換
edge[fa[q]] = edge[fa[q]^1] = -1;
q = ver[fa[q]^1];
}
具體的看下面的代碼就好
//樹上對點差分 + 線段樹動態開點 + 線段樹合併
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;
const int M = 2007;
int n,k;
int ver[N << 1],edge[N << 1],nex[N << 1],head[N],tot = 1;
int fa[N];
bool vis[N];
int dis[N];
void add(int u,int v,int val){
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
void dfs(int x,int &t){//不能處理負值
vis[x] = 1;
for(int i = head[x];i;i = nex[i]){
int y = ver[i],z = edge[i];
if(vis[y])continue;
dis[y] = dis[x] + z;
if(dis[y] >= dis[t])t = y;
fa[y] = i;//存路徑,用於成對變換,存的是前向星的指針
dfs(y,t);
}
vis[x] = 0;//回溯
}
void DP(int x,int &t){//可以處理負值
vis[x] = 1;
for(int i= head[x];i;i = nex[i]){
int y = ver[i],z = edge[i];
if(vis[y])continue;
DP(y,t);
t = max(t,dis[x] + dis[y] + z);
dis[x] = max(dis[x],dis[y] + z);
}
}
int main()
{
scanf("%d%d",&n,&k);
over(i,1,n-1){
int x,y;
scanf("%d%d",&x,&y);
add(x,y,1);
add(y,x,1);
}
int p = 1;//兩次dfs找最遠的p和q
dfs(1,p);
dis[p] = fa[p] = 0;//找到之後把它當成根節點
int q = p;
dfs(p,q);
int ans = ((n - 1) * 2) - dis[q] + 1;// + 1是因爲新增了一條必須走一次的邊
if(k == 2){
while(fa[q]){//成對變換
edge[fa[q]] = edge[fa[q]^1] = -1;
q = ver[fa[q]^1];
}
q = 0;
memset(dis,0,sizeof dis);
DP(p,q);
ans -= q - 1;
}
printf("%d\n",ans);
return 0;
}