1. 程式人生 > >Codeforces-708C題解

Codeforces-708C題解

一、題意

給定一顆樹,對於每一個節點,判斷能否在樹中刪除某一條邊,然後在任意兩個節點之間加一條邊,使這個點成為重心。

注:刪除樹中某一條邊後,標程並不會這麼無聊地把這棵樹變成兩個孤立的連通圖,而是再讓所有節點組合成一棵樹。如果在本來就連通的節點之間再連一條邊,形成一個圖,那必定會造成所有節點之間不能相互連通,那當前列舉的節點肯定不能成為重心。而如果再形成一個圖,當前列舉的節點還有可能成為重心,何樂而不為呢?

二、思路

這題我做了很多遍,換了很多種思路,也看了網上其他人很多題解,仍然沒看懂。後來,看了Codeforces上的官方思路以及和一位朋友討論了一番之後,終於明白這題的解法了。網上有很多方法,但仍然很難看懂,也許是現在閱讀程式碼的能力還不夠吧。所以,我在這裡儘自己最大的表達能力把自己的思路詳細地講解清楚,並附上程式碼。程式碼有些冗長,但是,我自認為,絕對是淺顯易懂的。
首先,以1號節點為樹根,找出給定樹的重心,假設為C;

找到重心C後,把C當作整棵樹的樹根。

然後,由於在第一步找重心的過程中,以每個節點為根的子樹的節點個數已經計算出來了,但是那是在以1為根的情況下計算出來的。而現在真正的樹根是重心C,所以,需要以重心C為樹根再次計算出每個節點的子樹的節點個數。我把他們放在了amts數組裡面。含義為:amounts。

由樹的重心的定義可知,重心C的所有子樹的節點個數都不超過n/2。而重心C的子節點下子、孫、曾孫等節點,以它們為根的子樹的節點個數更小於n/2。所以,當列舉每一個非重心節點v,判斷它能否通過上述操作使v變成重心時,amts[v]肯定是<=n/2的。也就是說,以v為根的子樹的節點個數肯定不超過n/2。所以,我們只需要判斷能否在v節點的上面找到一條邊,幹掉這條邊,再從兩個斷點中找一個點連一條邊到節點v上,使得v是重心。特別要注意這裡:為什麼時是連線到v上,而不是連線到v上面的其他節點,或者v的子或孫或曾孫等節點呢?關於這個問題,我也一直被困擾著。現在終於比較明白了。要注意,我們是列舉,每一節點都有機會被輪到,如果從斷點中連一條邊到別的地方,可能接下來某一次又有可能從斷點處連一條邊到這個節點v,這叫做打亂仗,徹底亂套了。這做題壓根沒法做下去了。再說一遍,我們是列舉,對於當前列舉的節點v,我們就只考慮和當前節點v有關係的操作,而不是列舉我的時候連你,列舉你的時候連他。

然後接下來就是刪邊的問題了。刪哪條邊呢?首先,剔除節點v以下的部分,因為它們加起來的數量也不超過n/2,刪除一條邊,再連一條到v,v子樹以外的樹根本沒動。然後把整棵樹畫標準,重心在最頂上,葉子節點在最下面。從最下層的邊開始找,假設最下層邊的層號為f。刪除f層的一條邊,還不如刪除f-1層的一條邊,因為刪除f-1層的一條邊可以把出v子樹以外的可能超過n/2個節點平分地更加均勻。如下圖所示。


當前列舉的節點是6時,我與其刪掉最下層的e(4, 8)或e(5,11)等,還如果刪掉e(2,4)或e(1,2),因為這樣可以使除節點6、12和13外的所有節點平分地更加均勻,所以,很顯然,刪掉重心C和它的子節點之間的邊是最合適的。因為被獨立出來的這一個聯通塊所包含的節點樹最多,更接近於n/2。

好,現在問題又來了。

1、當我列舉每一個節點的時候,都需要去查詢重心C的子節點中節點數最多的那棵樹,假設為T,這樣時間複雜度是O(n^2),顯然超時。但是,這很容易優化,因為T是不變的,只要找一次就行了。所以,只要記錄好這棵樹的節點編號就行。

2、如果當前列舉的節點在T中,這又怎麼搞呢?如果這樣,那就還需要再記錄重心C的子節點中節點數第二多的一棵樹的根節點編號,假設為T2。如果當前列舉的節點在子樹T中,那麼,就刪掉T2和重心C的連線,然後從子樹T2連一條邊到節點v來,即讓子樹T2成為v的孩子。否則,就刪掉T和重心C的連線,然後從子樹T連一條邊到節點v來,即讓子樹T成為v的孩子。然後再判斷n - 以節點v為根的子樹的節點數 - T子樹或T2子樹的節點數是否小於等於n/2。如果是,說明節點v可以通過上述操作成為重心,否則,不行。

3、如果有多個重心怎麼辦,選哪一個?這很好辦,任意選取一個即可。而選擇哪一個不用我們來做抉擇。

當然,如果只這樣,很快就會被樣例卡掉。比如這麼一棵樹。


顯然,重心是2或3。假設程式幫我們選取的是3。3的最大子樹的編號是2,第二大的是4(4和7是一樣的)。如果使用上面的演算法,當列舉到6的時候,由於6在最大子樹中,所以,就從第二大的子樹4中連一條邊過來,刪除3和4之間的邊。那麼,n - 以節點v為根的子樹的節點數 - T子樹或T2子樹的節點數 = 10 - 2 -  2 = 6 > 10 / 2 = 5,所以,判定6號節點不能通過以上操作成為重心。而實際上,應該把刪除3和2之間的連線,然後把3連到6上來,然後6可以成為重心。

所以,上述演算法還有不夠完善的地方:如果當前列舉的節點v在最大子樹T中,應該嘗試兩種刪邊的方法。①:刪除重心和第二大的子樹T2之間的連線,讓T2成為節點v的孩子;②:刪除重心和最大的子樹T之間的連線,此時,判斷v是否可以成為重心的算式變成:n - T子樹的節點數是否小於等於n/2。也就是說,我們需要嘗試讓重心成為節點v的孩子,看看是否能讓v成為重心。

思路大致如此,如果您不明白或者有疑問,可以聯絡我的QQ郵箱:[email protected]。我們可以一起討論。

三、程式碼

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 400010;
typedef struct {
    int to, next;
} Edge;
Edge tree[MAXN * 2];
int head[MAXN], cnt;
int n;
bool ans[MAXN];
/**重心節點編號*/
int centroid;
/**
    amts[i]:以i為根節點的子樹的節點個數。
    maxSubamt[i]:以i的子節點為根節點的子樹的最大節點個數。
*/
int amts[MAXN], maxSubamt[MAXN];
/**
    重心的兩個子節點,這兩個子節點的amt是最大和次大的。
*/
struct Son {
    set<int> subset;
    int id;
    int amt;
} maxSon[2];

void add(int from, int to) {
    tree[cnt].to = to;
    tree[cnt].next = head[from];
    head[from] = cnt++;
}

void init() {
    memset(head, -1, sizeof(head));
    memset(ans, 0, sizeof(ans));
    cnt = 0;
    centroid = -1;
    maxSon[0].amt = maxSon[1].amt = 0;
    maxSon[0].subset.clear();
    maxSon[1].subset.clear();
}

/**找出重心*/
void find_centroid(int root, int par) {
    amts[root] = 1, maxSubamt[root] = 0;
    for(int i = head[root], to = 0; i != -1; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            find_centroid(to, root);
            amts[root] += amts[to];
            maxSubamt[root] = max(maxSubamt[root], amts[to]);
        }
    }
    if(max(maxSubamt[root], n - amts[root]) <= n / 2)centroid = root;
}

/**重新計算對於每個節點以它為根節點的子樹的節點個數。因為樹根不再是1,而是重心centroid。*/
void recalculate_amts(int root, int par){
    amts[root] = 1;
    for(int i = head[root], to = 0; i != -1; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            recalculate_amts(to, root);
            amts[root] += amts[to];
        }
    }
}

/**找出重心的子節點中,節點數最多和次多的兩個子節點。*/
void find_max2sons(int root, int par) {
    for(int i = head[root], to = 0; i != -1; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            if(maxSon[0].amt < amts[to]) {
                maxSon[1] = maxSon[0];
                maxSon[0].amt = amts[to];
                maxSon[0].id = to;
            } else if(maxSon[1].amt < amts[to]) {
                maxSon[1].amt = amts[to];
                maxSon[1].id = to;
            }
        }
    }
}

/**找到son[0]的所有子節點,包括它自己 。*/
void find_subset(int root, int par) {
    maxSon[0].subset.insert(root);
    for(int i = head[root], to = 0; i != -1; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            find_subset(to, root);
        }
    }
}

/**
    對於非重心的每個節點,都嘗試三種可能的情況。
*/
void dfs_ans(int root, int par) {
    if(root == centroid)ans[root] = true;
    else {
        if(maxSon[0].subset.count(root)) {
            if(n - amts[root] - maxSon[1].amt <= n / 2)ans[root] = true;
            else if(n - maxSon[0].amt <= n / 2)ans[root] = true;
        } else if(n - amts[root] - maxSon[0].amt <= n / 2)ans[root] = true;
    }
    for(int i = head[root], to = 0; i != -1; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            dfs_ans(to, root);
        }
    }
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("Dinput.txt", "r", stdin);
    //freopen("Doutput2.txt", "w", stdout);
#endif // ONLINE_JUDGE
    int a, b;
    while(~scanf("%d", &n)) {
        init();
        for(int i = 1; i < n; ++i) {
            scanf("%d%d", &a, &b);
            add(a, b);
            add(b, a);
        }
        find_centroid(1, -1);
        recalculate_amts(centroid, -1);
        find_max2sons(centroid, -1);
        if(centroid != -1) {
            find_subset(maxSon[0].id, centroid);
            dfs_ans(centroid, -1);
        }
        for(int i = 1; i <= n; ++i)printf("%d%c", ans[i] ? 1 : 0, i < n ? ' ' : '\n');
    }
    return 0;
}

四、總結

1、在樹形DP中,思路不能混亂,一定搞清楚層次關係。對於某個操作,如果它需要對每個節點都操作一次,那麼,在編寫程式碼的時候,就無需考慮該操作和其他節點的關係。否則,邏輯關係、思路將會變得一團糟。以至於讓一個簡單的程式變得非常複雜。

2、由於樹本身的特點,當我們需要選取某個節點v的兩個子節點時(比如選兩個節點數最多的子節點),壓根不用考慮是否會重複的問題。因為程式在一次搜尋中不會遍歷一個節點兩次,所以,不會出現同一個節點被統計兩次的情況。所以,如果是選兩個節點數最多的子節點,最大的那個不斷更新,發現比已記錄的最大的值更大的值時,把原來記錄的給第二大的值。否則,通過max更新第二大的值。

附:樹的測試資料生成器程式碼

#include<bits/stdc++.h>
using namespace std;

int t;
int n, m;
int a, b, c, d;

int random1(int mod) {
    if(mod == 0)return 1;
    return (rand() + 1) % mod + 1;
}

int main() {
    freopen("input.txt", "w", stdout);
    srand(time(NULL));
    t = 10;
    printf("%d\n", t);
    for(int i = 0; i < t; ++i) {
        n = random1(20);
        printf("%d\n", n);
        for(int k = 2; k <= n; ++k)printf("%d %d\n", random1(k - 1), k);
        printf("\n");
    }
}

相關推薦

Codeforces-708C題解

一、題意 給定一顆樹,對於每一個節點,判斷能否在樹中刪除某一條邊,然後在任意兩個節點之間加一條邊,使這個點成為重心。 注:刪除樹中某一條邊後,標程並不會這麼無聊地把這棵樹變成兩個孤立的連通圖,而是再讓所有節點組合成一棵樹。如果在本來就連通的節點之間再連一條邊,形成一個圖,那

codeforces - 723C 題解

div algo bool pre algorithm while clu style 問題 題目大意是:給定一個歌曲演唱的序列,第i個數字代表第i首歌由ai號樂隊演唱;問如何用最少的操作將這些歌改成1-m號樂隊演唱,且每個樂隊演唱歌曲數的最小值最大。 問題解法:首先分析如

codeforces - 448C 題解

space max pri print main oid ios limit ont 題意:給定一個柵欄,每次塗一行或者一列,問最少幾次能夠塗完 題解:分治算法+DP思想,每次的狀態從豎著塗和橫著塗中選擇,同時向更高的部分遞歸計算。 1 #include<iost

codeforces Towers 題解

-m from oss courier cli div new nsf cout Little Vasya has received a young

Codeforces 985A 題解

def pan line ons 直接 優化 += span force 題意 有一個長度為\(n\)(\(n\)為偶數)的棋盤,黑白相間,類似於BWBW...BW.現有\(n/2\)個位置有棋子,每次只能向左或向右移動一個棋子一步,棋子不能重疊或移出棋盤.求最少要用多少次

Codeforces 985B 題解

n-1 lib cst 矩陣 初始 def ID ctype tchar 題意 有$n$個開關和$m$盞燈,給定一個矩陣$a(a_{i,j}\in [0,1])$,若$a_{i,j}=1$則說明開關$i$與第$j$盞燈連接.初始時所有燈都是關閉的.按下某開關後,所有與這個開

Codeforces 1062E 題解

給出一棵有根樹,1為根結點,接下來q次詢問,每次給出一個[l,r]區間,現在允許刪掉[l,r]區間內任何一個點,使得所有點的最近公共祖先的深度儘可能大,問刪掉的點是哪個點,深度最大是多少。 做法: 線段樹維護區間dfs序的最大值,最小值。 首先,區間的LCA等價於區間dfs序的最小值和最大值對應的兩個點

Codeforces 1093G題解(線段樹維護k維空間最大曼哈頓距離)

  題意是,給出n個k維空間下的點,然後q次操作,每次操作要麼修改其中一個點的座標,要麼查詢下標為[l,r]區間中所有點中兩點的最大曼哈頓距離。 思路:參考blog:https://blog.csdn.net/Anxdada/article/details/81980574,裡面講了k維空間中的

codeforces 1093 題解

A 題: 題目保證一定有解,就可以考慮用 $ 2 $ 和 $ 3 $ 來湊出這個數 $ n $ 如果 $ n $ 是偶數,我們用 $ n / 2 $ 個 $ 2 $ 來湊出 $ n $ 即可 如果 $ n $ 是奇數,就用 $ n / 2 - 1 $ 個 $ 2 $ 和 $ 1 $ 個 $ 3 $ 湊出

Codeforces 513F2 題解 (網路流-最大流 二分 BFS)

Scaygerboss 題目描述 在一個有障礙的網格圖中,有male 個男人和female 個女人,還有一個叫BOSS的人妖(既可以當男人又可以當女人)。這些人分佈在地圖上,每一個cell可以同時有多個人。這些人每個人移動各需要ti 的時間,問最小

Codeforces 691E題解 DP+矩陣快速冪

題面 傳送門:http://codeforces.com/problemset/problem/691/E E. Xor-sequences time limit per test3 seconds memory limit per test256 me

codeforces 1096 題解

A: 發現最優的方案一定是選 $ l $ 和 $ 2 * l $,題目保證有解,直接輸出即可 #include <bits/stdc++.h> #define Fast_cin ios::sync_with_stdio(false), cin.tie(); #define rep(i, a,

codeforces #270題解

題目連結:http://codeforces.com/contest/472 A:要求輸入一個n,拆分成兩個素數的和即可 // >File Name: codeforces270A.cpp // > Author: Webwei #include&

Codeforces 785E 題解(樹套樹-樹狀陣列套線段樹)

題目大意: 對於一個長度為n的序列進行k次操作,每次操作都是交換序列中的某兩個數。對於每一個操作,回答當前序列中有多少個逆序對。 題解: 每次更改序列都可以理解為,將答案減去被調換的位置原有數字的對答案的貢獻,然後調換兩數字的位置,然後將答案加上被調換

Codeforces 515C 題解(貪心+數論)(思維題)

題面 Drazil is playing a math game with Varda. Let’s define f(x)f(x)for positive integer x as a product of factorials of its digi

Codeforces 821A Okabe and Future Gadget Laboratory 題解

can printf else std bre 中間 sub 同一行 clu 此文為博主原創題解,轉載時請通知博主,並把原文鏈接放在正文醒目位置。 題目鏈接:http://codeforces.com/problemset/problem/821/A 時間限制:2秒

Educational Codeforces Round 21 A-E題題解

說了 else round int 排序 總量 允許 main ret A題 ............太水就不說了,貼下代碼 #include<string> #include<iostream> #include<cstring&

CodeForces - 592D Super M 題解

fin read size void spa clu family () 思路 題目大意:   一棵樹 n個點 有m個點被標記 求經過所有被標記的點的最短路徑的長度以及起點(如有多條輸出編號最小的起點)。 思路:   1.當且僅當一個點本身或其子樹中有點被標記時該點在最短的

Codeforces 841A 841B題解

c代碼 char ret getchar line 分配 配方 end str 此文為博主原創題解,轉載時請通知博主,並把原文鏈接放在正文醒目位置。 A. Generous Kefa    B. Godsend 兩道水題... A - 題目大意:把n個字母分配給

codeforces】【比賽題解】#849 CF Round #431 (Div.2)

font pen 我們 sca oot can 結束 memset [0 cf的比賽越來越有難度了……至少我做起來是這樣。 先看看題目吧:點我。 這次比賽是北京時間21:35開始的,算是比較良心。 【A】奇數與結束 "奇數從哪裏開始,又在哪裏結束?夢想從何處起航,它們又是否