1. 程式人生 > >hdu-2376 - Average distance(樹形dp )

hdu-2376 - Average distance(樹形dp )

Given a tree, calculate the average distance between two vertices in the tree. For example, the average distance between two vertices in the following tree is (d 01 + d 02 + d 03 + d 04 + d 12 +d 13 +d 14 +d 23 +d 24 +d 34)/10 = (6+3+7+9+9+13+15+10+12+2)/10 = 8.6.

在這裡插入圖片描述

Input
On the first line an integer t (1 <= t <= 100): the number of test cases. Then for each test case:

One line with an integer n (2 <= n <= 10 000): the number of nodes in the tree. The nodes are numbered from 0 to n - 1.

n - 1 lines, each with three integers a (0 <= a < n), b (0 <= b < n) and d (1 <= d <= 1 000). There is an edge between the nodes with numbers a and b of length d. The resulting graph will be a tree.

Output
For each testcase:

One line with the average distance between two vertices. This value should have either an absolute or a relative error of at most 10 -6

Sample Input
1
5
0 1 6
0 2 3
0 3 7
3 4 2
Sample Output
8.6
題目連結
參考題解1
參考題解2

  • 題意:給你一棵樹,將其中所有兩點直接能形成的通路加和,然後除以通路數量求平均值。如圖,可以理解一下。
  • 題目思路:剛開始我根本就不知道樹形dp是個什麼,然後這個題目就想著用dfs遍歷整個圖然後每兩個點求一下加和,這樣暴力來做,可是一想就會TLE。所以就看了一下題解,是用樹形dp,然後看了一下思路,其實和原本的dfs差不多,只不過是在其中做了一些比較巧妙的改動,使得程式比較精巧。

首先,我們要求所有的通路權值加和,暴力肯定是不可以的,那麼就換種思維方式,我們在加和的過程中,其實有很多邊是要重複走多次的,那麼這種邊到底要走多少次呢?從一個端點到另一個端點,到以另一個端點為根的所有子樹的節點,另一側也是一樣的道理,所以兩邊統計樹中的節點個數就可以了。如果一個端點的子樹節點個數為n個,那麼另一個端點的子樹節點個數就是m = (N - n)(N為這棵樹所有節點的個數),因為整棵樹,被這條邊分成了兩部分,節點被分在倆邊,那麼這條路徑要被通過的次數就是n * m次,每一條邊都是這樣算。
那麼這樣可以隨便選取一個點作為根節點,dfs往下面遍歷沒遇到一條邊的時候就找這個邊下面兩點的個數相乘加到父節點上,最後所有權值之和就都集合到了最開始的根節點上。(筆者語言表達能力有限,如果還是不明白,請參考兩個題解)

#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
#define LL long long
const int maxn = 1e4 + 5;
int t, n;
LL sum[maxn], dp[maxn];

struct node
{
    int to, val;
};
vector<node> vec[maxn];

void init()
{
    memset(dp, 0, sizeof(dp));
    for(int i = 0; i < n; i++)
    {
        sum[i] = 1; //預設自己這個節點算作一個,後面每有一個子樹就加一
        vec[i].clear();
    }
}

void tree_dp(int root, int father)  //root記錄當前這個要尋找子樹的節點,即以當前節點作為樹根,father代表這個節點的來源,這個節點的父節點
{
    int len = vec[root].size(); //相連邊的條數
    for(int i = 0; i < len; i++)
    {
        int son = vec[root][i].to;
        if(son == father) //如果是父節點的話就跳過
            continue ;
        tree_dp(son, root); //繼續找子樹的子樹
        int val = vec[root][i].val;
        //將子樹的個數加到當前根節點上,這次呼叫完成之後返回,也會被當做子節點使用。所以要把這個以這個點為根節點的所有子樹個數統計下來
        sum[root] += sum[son];
        //dp記錄當前這條邊以及之後的所有權值之和,最後都加在最開始呼叫時的根節點上,即為最後的總和
        dp[root] = dp[root] + dp[son] + sum[son] * (n - sum[son]) * val;
    }
}

int main()
{
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        init();
        int to, from, val;
        for(int i = 0; i < n - 1; i++)
        {
            scanf("%d%d%d", &from, &to, &val);
            //建立雙向邊
            //node{引數1, 引數2},這樣可以直接傳入,避免程式碼的麻煩
            vec[from].push_back(node{to, val});
            vec[to].push_back(node{from, val});
        }
        //結點從0開始,那麼就預設以0為根節點進行遍歷,根節點沒有父節點,所以設根節點為-1
        tree_dp(0, -1);
        //計算通路的數量,n個節點,連個點就可以構成一條通路,c(n,2)
        int temp = n * (n - 1) / 2;
        printf("%f\n", dp[0] / (temp * 1.0));
    }
    return 0;
}