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

[洛谷P3629] [APIO2010]巡邏

並且 相交 problem 數據 exit 輸入格式 安全 dfs apio2010

洛谷題目鏈接:[APIO2010]巡邏

題目描述

在一個地區中有 n 個村莊,編號為 1, 2, ..., n。有 n – 1 條道路連接著這些村 莊,每條道路剛好連接兩個村莊,從任何一個村莊,都可以通過這些道路到達其 他任一個村莊。每條道路的長度均為 1 個單位。 為保證該地區的安全,巡警車每天要到所有的道路上巡邏。警察局設在編號 為 1 的村莊裏,每天巡警車總是從警察局出發,最終又回到警察局。 下圖表示一個有 8 個村莊的地區,其中村莊用圓表示(其中村莊 1 用黑色的 圓表示),道路是連接這些圓的線段。為了遍歷所有的道路,巡警車需要走的距 離為 14 個單位,每條道路都需要經過兩次。

技術分享圖片

為了減少總的巡邏距離,該地區準備在這些村莊之間建立 K 條新的道路, 每條新道路可以連接任意兩個村莊。兩條新道路可以在同一個村莊會合或結束 (見下面的圖例(c))。 一條新道路甚至可以是一個環,即,其兩端連接到同一 個村莊。 由於資金有限,K 只能是 1 或 2。同時,為了不浪費資金,每天巡警車必須 經過新建的道路正好一次。 下圖給出了一些建立新道路的例子:

技術分享圖片

在(a)中,新建了一條道路,總的距離是 11。在(b)中,新建了兩條道路,總 的巡邏距離是 10。在(c)中,新建了兩條道路,但由於巡警車要經過每條新道路 正好一次,總的距離變為了 15。 試編寫一個程序,讀取村莊間道路的信息和需要新建的道路數,計算出最佳 的新建道路的方案使得總的巡邏距離最小,並輸出這個最小的巡邏距離。

輸入輸出格式

輸入格式:

第一行包含兩個整數 n, K(1 ≤ K ≤ 2)。接下來 n – 1 行,每行兩個整數 a, b, 表示村莊 a 與 b 之間有一條道路(1 ≤ a, b ≤ n)。

輸出格式:

輸出一個整數,表示新建了 K 條道路後能達到的最小巡邏距離。

輸入輸出樣例

輸入樣例#1:

8 1
1 2
3 1
3 4
5 3
7 5
8 5
5 6

輸出樣例#1:
11

輸入樣例#2:

8 2
1 2
3 1
3 4
5 3
7 5
8 5
5 6

輸出樣例#2:

10

輸入樣例#3:

5 2
1 2
2 3
3 4
4 5

輸出樣例#3:

6

說明

10%的數據中,n ≤ 1000, K = 1;

30%的數據中,K = 1;

80%的數據中,每個村莊相鄰的村莊數不超過 25;

90%的數據中,每個村莊相鄰的村莊數不超過 150; 100%的數據中,3 ≤ n ≤ 100,000, 1 ≤ K ≤ 2。

一句話題意: 給出一顆樹,你可以在樹中加入\(k\)條邊,且加入的邊只能經過一次,問從\(1\)

節點出發,到達每個點\(1\)次,至少需要走多遠的距離.

題解: 首先發現\(k\)的範圍非常小,可以考慮從這個入手,我們采用分類討論的方式來分析一下.

首先考慮如果\(k=0\),因為樹上兩個點只有一條路徑,所以如果要到每一個點並回來的話,需要的步數就是\((n-1)*2\)步.

也可以從歐拉回路的角度來理解一下上面講的東西,每個點的度數都是\(2\)的倍數,說明一定是可以從一個點出發到所有點並回來的,而因為樹上沒有環,走到哪裏就必須從哪裏回來,所以要到每一個點就要將每條邊遍歷\(2\)遍.

然後我們在來想\(k=1\)的情況:現在新加入了一條邊,那麽顯然此時就不是一顆樹了,現在樹上必定有一個環.而因為有了這個環,那麽這個環上的邊都是只需要遍歷一遍的.所以根據貪心的思想,我們肯定是要這個環上的邊盡量多,也就是連之前連的那條鏈盡量長,也就是樹的直徑.減去這個環上少走的邊數再加上我們重新連的邊長度,此時答案為\((n-1)*2-len1+1\),其中\(len1\)為這次求得的直徑長度.

接下來我們需要求解\(k=2\)的情況,我們可以繼續用之前分析\(k=1\)的情況的時候用的方法來處理\(k=2\)的情況.但是如果直接重新求一遍直徑肯定是不行的,因為這一次選擇連一條邊之後有可能形成的環和之前形成的環上有重復的邊,而這個長度是不能重復算入總答案中的.

又因為題目中說到新加入的邊是只能走一次並且必須走一次的,所以其實在分析過\(k=1\)的情況後就不需要再知道之前連邊的信息了,因為重新連一條邊與之前的連邊位置是沒有關系的,最多只會影響兩次在樹上邊的經過次數.

那麽我們來分析一下如果第二次加邊後的答案應該如何計算,顯然為了答案最大,第二次加邊後的答案是\((n-1)*2-len1+1-len2+1-len3\).其中\(len2\)為這次求得的直徑大小,\(len3\)為這次直徑與上次求的直徑的重疊的部分.

然而我們在求直徑的時候並不能求出這條直徑與另一條直徑相交的長度,所以我們可以在上次求出直徑後直接將直徑上經過的所有邊的權值改為\(-1\),這樣在下次求直徑的時候就避免了求重復長度的問題了.

然後因為有負權邊,所以在求解樹的直徑的時候要用\(DP\)的方式來求.

#include<bits/stdc++.h>
using namespace std;
const int N = 100000+5;

int n, k, ecnt = 1, last[N], ans, len0 = 0, len1 = 0, dep[N], f[N], pre[N], fa[N], L, R;

struct edge{
    int to, w, nex;
}e[N*2];

void add(int x, int y, int z){
    e[++ecnt].to = y, e[ecnt].w = z, e[ecnt].nex = last[x], last[x] = ecnt;
}

void dfs(int x, int las, int deep){
    dep[x] = deep, fa[x] = las;
    for(int to, i=last[x];i;i=e[i].nex){
        to = e[i].to; if(to == las) continue;
        dfs(to, x, deep+1);
        if(len0 < f[x]+f[to]+e[i].w) len0 = f[x]+f[to]+e[i].w, L = pre[x], R = pre[to];
        if(f[x] < f[to]+e[i].w) f[x] = f[to]+e[i].w, pre[x] = pre[to];
    }
}

void tag(int x, int y){
    if(dep[x] < dep[y]) swap(x, y);
    while(dep[x] > dep[y]){
        for(int i=last[x];i;i=e[i].nex)
            if(e[i].to == fa[x]) e[i].w = e[i^1].w = -1;
        x = fa[x];
    }
    if(x == y) return;
    while(x != y){
        for(int i=last[x];i;i=e[i].nex)
            if(e[i].to == fa[x]) e[i].w = e[i^1].w = -1;
        for(int i=last[y];i;i=e[i].nex)
            if(e[i].to == fa[y]) e[i].w = e[i^1].w = -1;
        x = fa[x], y = fa[y];
    }
}

void dfs2(int x){
    for(int to, i=last[x];i;i=e[i].nex){
        to = e[i].to; if(to == fa[x]) continue;
        dfs2(to);
        if(len1 < f[x]+f[to]+e[i].w) len1 = f[x]+f[to]+e[i].w;
        if(f[x] < f[to]+e[i].w) f[x] = f[to]+e[i].w;
    }
}

int main(){
    ios::sync_with_stdio(false);
    int x, y; cin >> n >> k, ans = (n-1)*2;
    for(int i=1;i<=n;i++) pre[i] = i;
    for(int i=1;i<n;i++) cin >> x >> y, add(x, y, 1), add(y, x, 1);
    dfs(1, -1, 1);
    if(k == 1) cout << ans-len0+1 << endl, exit(0);
    memset(f, 0, sizeof(f)), tag(L, R), dfs2(1);
    cout << ans-len0+1-len1+1 << endl;
    return 0;
}

[洛谷P3629] [APIO2010]巡邏