解題報告: luogu P3629 [APIO2010]巡邏(樹的直徑,位運算成對變換,思維)

題目鏈接:P3629 [APIO2010]巡邏
在這裏插入圖片描述
在這裏插入圖片描述
首先看題,從1號結點開始,全部遍歷並回到1號結點會恰好經過所有的邊兩次,這樣總長度爲 2(n1)2*(n-1)
那麼如果建立一條路以後,由題意可知新道路必須經過恰好一次,也只能經過一次,所以新路從x到y形成一個環,這樣就可以用新路的權值1來代替老路徑中從x到y的路徑1次,也就是說從x到y之間的路徑在建立了新路以後只需要再經過一次就行了。所以我們要求最短,肯定要把整棵樹上的最長鏈也就是樹的直徑省掉答案最優。所以當k = 1 時,我們找到最長鏈,在這兩個端點之間加一條新路,設直徑爲L,則答案ans=2(n1)L+1ans = 2*(n-1) - L +1(+1是因爲要走一次新加的新路)。

當k = 2時,我們按照剛剛的理論,再求一次最長鏈。這樣形成兩個環,第一條肯定不能跟第二條一樣,所以我們求完第一條以後要把第一條最長鏈給刪去。這裏找到第一次的最長鏈,遍歷一遍,將值從1變成-1。
如果第二條與第一條鏈沒有重疊,那麼就跟第一條鏈一樣,直接減去即可。
若第二條連與第一條鏈有重疊,那麼由於本應只走一次的路徑會走兩次,多減去的值需要加回來,但是由於第一條鏈上的所有邊已經取反爲-1,這樣減去負數又相當於加上這個數,所以兩種答案是一樣的。
最終的答案爲ans=2(n1)(L11)(L21)=2nL1L2ans = 2*(n-1) - (L_1-1) - (L_2-1) = 2*n - L_1 - L_2
總時間複雜度爲O(n)O(n)

這裏將第一條鏈的值全部賦值爲-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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章