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

2015 Multi-University Training Contest 1

题意:

给出一个树(满足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序的应用总结:点击打开链接

时间: 2024-08-04 20:23:53

HDU 5296 Annoying problem(LCA模板+树的dfs序心得)的相关文章

HDU 5296 Annoying problem LCA+树状数组

题解链接 Annoying problem Time Limit: 16000/8000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Total Submission(s): 480    Accepted Submission(s): 146 Problem Description Coco has a tree, whose nodes are conveniently labeled by 1,2,-,n, w

HDU 5296 Annoying Problem 树链剖分 LCA 倍增法

HDU 5296 Annoying Problem 题目链接:hdu 5296 题意:在一棵给定的具有边权的树,一个节点的集合S(初始为空),给定Q个操作,每个操作增加或删除S中的一个点,每个操作之后输出使集合S中所有点联通的最小子树的边权和. 思路:最小子树上的节点的充要条件: 节点为(S集合中所有点的LCA)的子节点: 节点有一个子孙为S集合中的点. 那么我们给每个节点都开一个标记数组,初始为零,每加入一个节点,就把从这个节点到根节点路径上的点的值都+1,反之-1,这样通过对每个单节点值的查

[HDU 5296] Annoying problem (DFS序性质+LCA)

HDU - 5296 一棵树上有若干个点,每条边有一个边权 给一个初始为空的集合,每次向集合内添加一个点或者删除一个点 问每次操作结束后,将集合内所有点连起来的边权和为多少 假设集合内已经有一些点,那么再加一个点所增加的边权 将会是这个点到某一条链的距离 但是这条链不能随便挑选,否则可能会经过已经选择的边 挑选策略就是,找到集合内dfs序比当前点大和小的两个点组成的链 换句话来说,就是集合内dfs序最靠近当前点的两个点 设当前点 x,这样选链,选出的链是 (u,v),并且设dfsn[u]<dfs

HDOJ 5296 Annoying problem LCA+数据结构

dfs一遍得到每一个节点的dfs序,对于要插入的节点x分两种情况考虑: 1,假设x能够在集合中的某些点之间,找到左边和右边距离x近期的两个点,即DFS序小于x的DFS序最大点,和大于x的DFS序最小的点...... 2.假设x在集合中的点某一側,则找距离x的dfs序最小和最大的点 将x插入这个集合最少要走的距离为 dist[x]-dist[LCA(left,x)]-dist[LCA(right,x)]+dist[LCA(left,right)] 删除同理 Annoying problem Tim

【LCA】HDU 5296 Annoying problem

通道:http://acm.hdu.edu.cn/showproblem.php?pid=5296 题意:给一棵n个节点的树,再给q个操作,初始集合S为空,每个操作要在一个集合S中删除或增加某些点,输出每次操作后:要使得集合中任意两点互可达所耗最小需要多少权值.(记住只能利用原来给的树边.给的树边已经有向.10万个点,10万个操作) 思路: 代码: 1 #pragma comment(linker, "/STACK:102400000,102400000") 2 #include &l

HDU 5296 Annoying problem

http://acm.hdu.edu.cn/showproblem.php?pid=5296 实际上问题可以转化为点到一个连通子树的最短距离.. //Hello. I'm Peter. #pragma comment(linker, "/STACK:102400000,102400000") #include<cstdio> #include<iostream> #include<sstream> #include<cstring> #i

BZOJ 3779 重组病毒 LCT+线段树维护DFS序

题目大意:给定一棵树,初始每个点都有一个颜色,支持三种操作: 1.将某个点到根的路径上所有点染上一种新的颜色 2.将某个点到根的路径上所有点染上一种新的颜色,然后把根设为这个点 3.定义一个点的代价为这个点到根路径上颜色的种类数,求某个点子树中所有点代价的平均值 我真是炖了狗了-- 容易发现这玩应就是个LCT,操作1就是Access,操作2就是Move_To_Root,代价就是一个点到根路径上的虚边数量+1 我们用LCT模拟上述操作,用线段树维护DFS序维护信息,一旦LCT中出现了虚实边的切换,

CF877E Danil and a Part-time Job 线段树维护dfs序

\(\color{#0066ff}{题目描述}\) 有一棵 n 个点的树,根结点为 1 号点,每个点的权值都是 1 或 0 共有 m 次操作,操作分为两种 get 询问一个点 x 的子树里有多少个 1 pow 将一个点 x 的子树中所有节点取反 对于每个 get 给出答案 \(\color{#0066ff}{输入格式}\) 第一行一个整数 n 第二行共 n?1 个整数,第 i 个数 \(x_i\) 表示 \(x_i\) 是 i+1 的父亲, 第三行给出每个点的初始权值 第四行一个整数 m 接下来

树的dfs序,p1539,其他经典问题,2018/11/08模拟赛T3

树的dfs序指从根节点进行dfs(先序遍历),每次到达某个点的时间和离开这个点的时间.它可以将树上的问题转换成序列问题进行处理. 比如对于p1539的样例可以这样解释. 每个点的左边数字表示进入该点的"时间",右边的数字表示离开该点的"时间".对dfs序的介绍就到这里. 然后来看一个例题: 先读入边,跑一遍dfs确定dfs序. 对于操作1,把点x的进入的"时间"+=a,把x出去的"时间"-=a 这样操作3询问根节点到y的路径点