1. 程式人生 > >HDU 5296 Annoying problem(LCA模板+樹的dfs序心得)

HDU 5296 Annoying problem(LCA模板+樹的dfs序心得)

Problem Description Coco has a tree, whose nodes are conveniently labeled by 1,2,…,n, which has n-1 edge,each edge has a weight. An existing set S is initially empty.
Now there are two kinds of operation:

1 x: If the node x is not in the set S, add node x to the set S
2 x: If the node x is in the set S,delete node x from the set S

Now there is a annoying problem: In order to select a set of edges from tree after each operation which makes any two nodes in set S connected. What is the minimum of the sum of the selected edges’ weight ?


Input one integer number T is described in the first line represents the group number of testcases.( T<=10 ) 
For each test:
The first line has 2 integer number n,q(0<n,q<=100000) describe the number of nodes and the number of operations.
The following n-1 lines each line has 3 integer number u,v,w describe that between node u and node v has an edge weight w.(1<=u,v<=n,1<=w<=100)
The following q lines each line has 2 integer number x,y describe one operation.(x=1 or 2,1<=y<=n)



Output Each testcase outputs a line of "Case #x:" , x starts from 1.
The next q line represents the answer to each operation.


Sample Input 1 6 5 1 2 2 1 5 2 5 6 2 2 4 2 2 3 2 1 5 1 3 1 4 1 2 2 5
Sample Output Case #1: 0 6 8 8 4
Author FZUACM
Source

題意:

給出一個樹(滿足n個點,n-1條邊),還有一個原本為空集的點集S,有兩種操作:

操作1:向集合S中加入點x(如果x不在集合裡面)

操作2:從集合S中刪去點x(如果x在集合裡面)

對於每次操作,從樹上找一個邊集,滿足S中任意兩點連通,求最小的邊集(邊權之和最小)

分析:

還記得LCA的入門題(HDU2586)求的是樹上任意兩點間的最短距離,當時有公式

d(a,b)min = dist[a] + dist[b] - 2*dist[lca(a,b)],其中dist是節點到根節點的距離

這題顯然也是LCA的題,但是公式不是很好找,題解是這樣說的:

先預處理下dfs序
對於新增點u操作:
每次查詢集合中的點與新增點的dfs序比他小的最大的點和比他大的最小點,

假設這兩個點為x,y(找不到的話就找字典序最大和最小的兩個點,理由下面給出)
每次增加的花費為dis[u] - dis[lca(x,u)] - dis[lca(x,y)];其中dis記得是點到根節點的距離
對於刪除點u操作:
每次先把點從集合刪除,然後再計算減少花費,計算公式和增加的計算方法一樣
根據dfs序選擇兩個點的理由:
如果集合中可以找到dfs序比操作點大和比操作點小的點,那麼變化的費用就相當於
操作點到以這兩個點為端點的鏈的距離,可以用上述公式計算

如果集合中dfs序都比操作點小或者都比操作點大,

那麼變化的費用是操作點到以字典序最大和字典序最小的點為端點的鏈的距離,也可以用上面的公式計算

注意新增點的時候是先計算再新增,刪除點是先刪除再計算

這裡的u說的是dfs序,x,y也是dfs序,其實dfs序只是表示u,x,y之間的關係,真正計算時還是要還原到原樹上的節點來計算

所以相比於一般的LCA模板,還需要再開一個數組儲存dfs序對應的節點

關於dfs序,之前一直感覺模模糊糊不清不楚的,這次借這題好好理解了一下

先上鶸的LCA模板吧:

struct Node
{
    int u, v, w, next;
};
struct LCA
{
    int dp[2 * N][M];
    bool vis[N];
    int tot, head[N];
    Node e[2*N];
    void AddEdge (int u, int v, int w, int &k)
    {
        e[k].u = u, e[k].v = v, e[k].w = w;
        e[k].next = head[u];
        head[u] = k++;
    }
    int ver[2 * N], d[2 * N], first[N], dis[N];//dis[i]表示節點i距離根節點的距離
    //first[i]表示節點i的dfs序
    //節點編號   深度  點編號位置  距離
    void init()
    {
        mem(head,-1);
        mem(vis,0);
        tot = 0;dis[1] = 0;
    }
    void dfs (int u, int dep)
    {
        vis[u] = 1;
        ver[++tot] = u;
        first[u] = tot;
        d[tot] = dep;
        for (int k = head[u]; k != -1; k = e[k].next)
        {
            if (!vis[e[k].v])
            {
                int v = e[k].v, w = e[k].w;
                dis[v] = dis[u] + w;
                dfs (v, dep + 1);
                ver[++tot] = u;
                d[tot] = dep;
            }
        }
    }
    void ST (int n)
    {
        for (int i = 1; i <= n; ++i) dp[i][0] = i;
        for (int j = 1; (1 << j) <= n; ++j)
        {
            for (int i = 1; i + (1 << j) - 1 <= n; ++i)
            {
                int a = dp[i][j - 1], b = dp[i + (1 << (j - 1) )][j - 1];
                dp[i][j] = d[a] < d[b] ? a : b;
            }
        }
    }
    int RMQ (int l, int r)
    {
        int k = 0;
        while ( (1 << (k + 1) ) <= r - l + 1) k++;
        int a = dp[l][k], b = dp[r - (1 << k) + 1][k];
        return d[a] < d[b] ? a : b;
    }
    int Lca(int u, int v)
    {
        int x = first[u], y = first[v];
        if (x > y) swap (x, y);
        return ver[RMQ (x, y)];
    }
}ans;

畫個圖幫助理解dfs序:


第一個表,i表示按先序遍歷順序對樹的遍歷順序,

ver表示遍歷過程中第i次訪問的點的編號,d表示遍歷過程中第i次訪問的點的深度

關鍵是第二個表,其中first就是鶸模板裡面的dfs序,first是怎麼來的呢?

其實就是第一個表中每個點第一次被訪問對應的i的值,圖中紅筆標記的就是弱的DFS序了

網上看到有些人的dfs序是從1,2,3,4......這樣連續的自然數,不過沒關係,我的dfs序離散化之後其實是一樣的

dfs序能把非線性的樹結構用線性結構儲存下來,另外,dfs序一個很重要的性質是:

一顆子樹的所有節點在dfs序中是連續的一段

dfs序在樹狀結構中有很多用法,LCA只是其中一種(文尾貼了dfs序應用的相關資料)

除了dfs序,還有什麼bfs序,樹剖,LCT什麼的(鶸表示暫時還不會。。。)

回到正題。。。。這題。。。。

LCA模板沒什麼好說的,計算的部分程式碼中說的很清楚了,上程式碼:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
const int N = 400010;
const int M = 45;
struct Node
{
    int u, v, w, next;
};
struct LCA
{
    int dp[2 * N][M];
    bool vis[N];
    int tot, head[N];
    Node e[2*N];
    void AddEdge (int u, int v, int w, int &k)
    {
        e[k].u = u, e[k].v = v, e[k].w = w;
        e[k].next = head[u];
        head[u] = k++;
    }
    //節點編號   深度  點編號位置  距離
    int ver[2 * N], d[2 * N], first[N], dis[N];//dis[i]表示節點i距離根節點的距離
    //first[i]表示節點i的dfs序
    int p[2*N];//p[i]表示dfs序為i的節點
    //即 : 若 first[i] = j,則 p[j] = i
    void init()
    {
        mem(head,-1);
        mem(vis,0);
        tot = 0;dis[1] = 0;
    }
    void dfs (int u, int dep)
    {
        vis[u] = 1;
        ver[++tot] = u;
        first[u] = tot;
        p[tot] = u;
        d[tot] = dep;
        for (int k = head[u]; k != -1; k = e[k].next)
        {
            if (!vis[e[k].v])
            {
                int v = e[k].v, w = e[k].w;
                dis[v] = dis[u] + w;
                dfs (v, dep + 1);
                ver[++tot] = u;
                d[tot] = dep;
            }
        }
    }
    void ST (int n)
    {
        for (int i = 1; i <= n; ++i) dp[i][0] = i;
        for (int j = 1; (1 << j) <= n; ++j)
        {
            for (int i = 1; i + (1 << j) - 1 <= n; ++i)
            {
                int a = dp[i][j - 1], b = dp[i + (1 << (j - 1) )][j - 1];
                dp[i][j] = d[a] < d[b] ? a : b;
            }
        }
    }
    int RMQ (int l, int r)
    {
        int k = 0;
        while ( (1 << (k + 1) ) <= r - l + 1) k++;
        int a = dp[l][k], b = dp[r - (1 << k) + 1][k];
        return d[a] < d[b] ? a : b;
    }
    int Lca(int u, int v)
    {
        int x = first[u], y = first[v];
        if (x > y) swap (x, y);
        return ver[RMQ (x, y)];
    }
}ans;
set<int>s;
bool vis[N+5];//判斷點是否在集合中
int Cal(int u)//計算dfs序為u的點的加入集合增加的花費
{
    if (s.empty()) return 0;
    int x,y;//x,y分別是比dfs序為u的點大的最小點和比它小的最大點
    set<int>::iterator it = s.upper_bound(u);//返回集合中第一個鍵值大於u的元素迭代器位置
    if (it == s.end()||it == s.begin())//沒有比他大的或者都比他大
    {
        x = ans.p[*s.rbegin()];//*s.rbegin()是dfs序,在這裡轉換成了原樹上的節點保存於x
        y = ans.p[*s.begin()];
    }
    else
    {
        x = ans.p[*it];
        it--;
        y = ans.p[*it];
    }
    u = ans.p[u];//u也換回成原樹上的節點來計算
    return ans.dis[u] - ans.dis[ans.Lca(x,u)] - ans.dis[ans.Lca(y,u)] + ans.dis[ans.Lca(x,y)];
}
int main()
{
    int T;scanf("%d",&T);int kas = 0;
    while (T--)
    {
        int n,m;scanf("%d %d",&n,&m);
        s.clear();
        ans.init();int k = 0;
        for (int i = 1,u,v,w;i < n;++i)
        {
            scanf("%d %d %d",&u,&v,&w);
            ans.AddEdge(u,v,w,k);
            swap(u,v);
            ans.AddEdge(u,v,w,k);
        }
        ans.dfs(1,1);
        ans.ST(2*n-1);int op,u;
        printf("Case #%d:\n",++kas);
        int sun = 0;mem(vis,0);
        for (int i = 0;i < m;++i)
        {
            scanf("%d %d",&op,&u);
            if (op == 1)
            {
                if (!vis[u])
                {
                    vis[u] = 1;
                    sun += Cal(ans.first[u]);//用u的dfs序去計算
                    s.insert(ans.first[u]);
                }
            }
            else
            {
                if (vis[u])
                {
                    vis[u] = 0;
                    s.erase(ans.first[u]);
                    sun -= Cal(ans.first[u]);
                }
            }
            printf("%d\n",sun);
        }
    }
    return 0;
}

參考的別人的題解,但是別人的LCA模板和我的不一樣,別人的dfs序求出來是從1開始的連續自然數:點選開啟連結

看了看關於DFS序的應用總結:點選開啟連結