树的直径,树的重心以及树的分治

树的直径,树的重心以及树的分治

树的直径:

树的直径是指树的最长简单路,即树的最长简单路。

现有结论,从任意一点u出发搜到的最远的点一定是s、t中的一点,然后在从这个最远点开始搜,就可以搜到另一个最长路的端点,即用两遍广搜就可以找出树的最长路。

step1:以树中任意一个结点为源点,进行一次广度优先遍历,找出离源点距离最远的点d

step2:以d为源点,进行一次广度优先遍历,找出离d最远的点,并记录其长度



证明:(转自:http://blog.csdn.net/pi9nc/article/details/12394117)

1    设u为s-t路径上的一点,结论显然成立,否则设搜到的最远点为T则

dis(u,T) >dis(u,s)     且  dis(u,T)>dis(u,t)   则最长路不是s-t了,与假设矛盾

2   设u不为s-t路径上的点

    首先明确,假如u走到了s-t路径上的一点,那么接下来的路径肯定都在s-t上了,而且终点为s或t,在1中已经证明过了

    所以现在又有两种情况了:

    1:u走到了s-t路径上的某点,假设为X,最后肯定走到某个端点,假设是t ,则路径总长度为dis(u,X)+dis(X,t)

    2:u走到最远点的路径u-T与s-t无交点,则dis(u-T) >dis(u,X)+dis(X,t);显然,如果这个式子成立,

    则dis(u,T)+dis(s,X)+dis(u,X)>dis(s,X)+dis(X,t)=dis(s,t)最长路不是s-t矛盾。


 附上一张第二种情况的图

     

S-----------c-----------T

                 |

                w------u


例题1:(http://lx.lanqiao.cn/problem.page?gpid=T32)


历届试题 大臣的旅费  
时间限制:1.0s   内存限制:256.0MB
       
问题描述

很久以前,T王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。

为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。

J是T国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了J最常做的事情。他有一个钱袋,用于存放往来城市间的路费。

聪明的J发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第x千米到第x+1千米这一千米中(x是整数),他花费的路费是x+10这么多。也就是说走1千米花费11,走2千米要花费23。

J大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?

输入格式

输入的第一行包含一个整数n,表示包括首都在内的T王国的城市数

城市从1开始依次编号,1号城市为首都。

接下来n-1行,描述T国的高速路(T国的高速路一定是n-1条)

每行三个整数Pi, Qi, Di,表示城市Pi和城市Qi之间有一条高速路,长度为Di千米。

输出格式

输出一个整数,表示大臣J最多花费的路费是多少。

样例输入1
5
1 2 2
1 3 1
2 4 5
2 5 4
样例输出1
135
输出格式

大臣J从城市4到城市5要花费135的路费。

//AC:

#include  
#include  
#include  
#include  
#include  
using namespace std;  
#define MAXN 10100  
  
struct Edge{  
    int v,w;  
    Edge(int vv,int ww):v(vv),w(ww){}  
};  
  
int n;  
int dist[MAXN],max_len,End;  
vector >G;  
  
void dfs(int u,int father,int len)  
{  
    if(len>max_len)max_len=len,End=u;  
    for(int i=0;i



树的重心:


的重心也叫的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点树最小。换句话说,删除这个点后最大连通块(一定是树)的结点数最小。


和树的最大独立问题类似,先任选一个结点作为根节点,把无根树变成有根数,然后设d(i)表示以i为根的子树的结点的个数。不难发现d(i)=∑d(j)+1,j∈s(i)。s(i)为i结点的所有儿子结点的编号的集合。程序也十分简单:只需要DFS一次,在无根树有根数的同事计算即可,连记忆化都不需要——因为本来就没有重复计算。
那么,删除结点i后,最大的连通块有多少个呢?结点i的子树中最大有max{d(j)}个结点,i的“上方子树”中有n-d(i)个结点,如图9-13。这样,在 动态规划的过程中就可以孙便找出树的重心了。

树的直径,树的重心以及树的分治_第1张图片



树的重心的一个的性质:
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
这也是“道路修建”带来的启发。(证明:调整法)

树的重心的另一个性质:
把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
这个让“重心”名副其实了。。。(证明:。。。自己好好思考一下吧。。。)

还有一个性质:
把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
(证明:提示:张放想都没想说,要不然那两个不等式就矛盾了)

思路:
<1>利用
<2>开两个数组,一个用来存放它的所有的子节点数目,一个记录它的分支中最大的子节点数目。

#define _CRT_SECURE_NO_WARNINGS
 
#include 
#include 
#include 
#include 
#include 
#include 
 
using namespace std;
 
int N; // 1<= N <= 20000
const int maxn = 20000;
vector tree[maxn + 5]; // tree[i]表示节点i的相邻节点
int d[maxn + 5]; // d[i]表示以i为根的子树的节点个数
 
#define INF 10000000
 
int minNode;
int minBalance;
 
void dfs(int node, int parent) // node and its parent
{
    d[node] = 1; // d数组记录的是它所有的子节点数目
    int maxSubTree = 0; // subtree that has the most number of nodes
    for (int i = 0; i < tree[node].size(); i++) {
        int son = tree[node][i];
        if (son != parent) {
            dfs(son, node);
            d[node] += d[son];
            maxSubTree = max(maxSubTree, d[son]);//这里就比较神奇了,这一步和循环外的处理可以直接把记录分支中最大的子节点数的数组省略了
        }
    }
    maxSubTree = max(maxSubTree, N - d[node]); // "upside substree with (N - d[node]) nodes"
 
    if (maxSubTree < minBalance){
        minBalance = maxSubTree;
        minNode = node;
    }
}
 
int main()
{
    int t;
    scanf("%d", &t);
    while (t--){
        scanf("%d", &N);
 
        for (int i = 1; i <= N - 1; i++){
            tree[i].clear();
        }
 
        for (int i = 1; i <= N-1; i++){
            int u, v;
            scanf("%d%d", &u, &v);
            tree[u].push_back(v);
            tree[v].push_back(u);
        }
 
        minNode = 0;
        minBalance = INF;
 
        dfs(1, 0); // fist node as root
 
        printf("%d %d\n", minNode, minBalance);
    }
 
    return 0;
}


例题2:(https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1737)

1737 配对
基准时间限制:1 秒 空间限制:131072 KB 分值: 40  难度:4级算法题
 收藏
 关注
给出一棵n个点的树,将这n个点两两配对,求所有可行的方案中配对两点间的距离的总和最大为多少。
Input
一个数n(1<=n<=100,000,n保证为偶数)
接下来n-1行每行三个数x,y,z表示有一条长度为z的边连接x和y(0<=z<=1,000,000,000)
Output
一个数表示答案
Input示例
6
1 2 1
1 3 1
1 4 1
3 5 1
4 6 1
Output示例
7
//配对方案为(1,2)(3,4)(5,6)


//AC:
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define cl(a,b) memset(a,b,sizeof(a))
#define pb push_back
#define mp make_pair
#define lowbit(x) (x)&(-x)
typedef long long LL;
typedef pair PII;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
const int maxn = 100000 + 10;
int son[maxn],vis[maxn];
LL dd[maxn];
int zx,Size;
vectoredge[maxn];
int n;
void init()
{
    for( int i = 1; i <= n; i++ )edge[i].clear();
    cl(vis,0);
    cl(dd,0);
    Size = INF;
    zx = -1;
}
//求树的重心模板
void dfs(int r)
{
    vis[r] = 1;
    son[r] = 0;
    int tmp = 0;
    for( int i = 0; i < edge[r].size(); i++ ){
        int v = edge[r][i].second;
        if(!vis[v]){
            dfs(v);
            son[r] +=  son[v] + 1;
            tmp = max(tmp,son[v] + 1);
        }
    }
    tmp = max(tmp,n - son[r] - 1);
    if(tmp < Size){
        zx = r;
        Size = tmp;
    }
}
void dfs1(int x)
{
    vis[x] = 1;
    for( int i = 0; i < edge[x].size(); i++ ){
        int v = edge[x][i].second;
        if(!vis[v]){
            dd[v] = dd[x] + edge[x][i].first;
            dfs1(v);
        }
    }
}
int main()
{
    while(~scanf("%d",&n)){
        init();
        for( int i = 0; i < n - 1; i++ ){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].pb(mp(w,v));
            edge[v].pb(mp(w,u));
        }
        dfs(1);
        cl(vis,0);
        dfs1(zx);
        LL ans = 0;
        for( int i = 1; i <= n; i++ ){
            ans += dd[i];
        }
        printf("%lld\n",ans);
    }
    return 0;
}


树的分治:

分治,指的是分而治之,即将一个问题分割成一些规模较小的相互独立的子问题,以便各个击破。

我们常见的是在一个线性结构上进行分治,而分治算法在树结构上的运用,称之为树的分治算法。

分治往往与高效联系在一起,而树的分治正是一种用来解决树的路径问题的高效算法。

树的点的分治:首先选取一个点将无根树转为有根树,再递归处理每一颗以根结点的儿子为根的子树。

首先我们考虑如何选取点。对于基于点的分治,我们选取一个点,要求将其删去后,结点最多的树的结点个数最小,这个点就是树的重心。

在基于点的分治中每次我们都会将树的结点个数减少一半,因此递归深度最坏是 O(NlogN) 的,在树是一条链的时候达到上界。

 

对于一棵有根树, 树中满足要求的一个数对所对应的一条路径,必然是以下两种情况之一:
1、经过根节点
2、不经过根节点,也就是说在根节点的一棵子树中
对于情况2,可以递归求解,下面主要来考虑情况1。

设点i的深度为Depth[i],父亲为Parent[i]。
若i为根,则Belong[i]=-1,若Parent[i]为根,则Belong[i]=i,否则Belong[i]=Belong[Parent[i]]。
这三个量都可以通过一次BFS求得。
我们的目标是要统计:有多少对(i,j)满足iBelong[j]

如果这样考虑问题会变得比较麻烦,我们可以考虑换一种角度:
设X为满足i 设Y为满足i 那么我们要统计的量便等于X-Y

求X、Y的过程均可以转化为以下问题:
已知A[1],A[2],...A[m],求满足i
对于这个问题,我们先将A从小到大排序。
设B[i]表示满足A[i]+A[p]<=K的最大的p(若不存在则为0)。我们的任务便转化为求出A所对应的B数组。那么,若B[i]>i,那么i对答案的贡献为B[i]-i。
显然,随着i的增大,B[i]的值是不会增大的。利用这个性质,我们可以在线性的时间内求出B数组,从而得到答案。

综上,设递归最大层数为L,因为每一层的时间复杂度均为“瓶颈”——排序的时间复杂度O(NlogN),所以总的时间复杂度为O(L*NlogN)

然而,如果遇到极端情况——这棵树是一根链,那么随意分割势必会导致层数达到O(N)级别,对于N=10000的数据是无法承受的。因此,我们在每一棵子树中选择“最优”的点分割。所谓“最优”,是指删除这个点后最大的子树尽量小。这个点可以通过树形DP在O(N)时间内求出,不会增加时间复杂度。这样一来,即使是遇到一根链的情况时,L的值也仅仅是O(logN)的。

简单来说:点分治就是每次找到重心,然后把重心去掉,对分成的每两棵树之间分别统计路径信息(以重心的每个相邻点为根,遍历整棵子树即可得到这个根到每个结点的统计信息),就可以知道包含这个重心的所有路径的信息,然后对于剩下的路径就是在子树里面进行同样的操作了,直到只剩一个点为止(注意这一个点所构成的路径有时也要处理一下)。边分治就是每次找到一条边,使得删掉这条边后分成的两棵子树大小尽可能平均,然后以删掉的边的两端点为根,分别统计根到两棵树中的每个结点的路径信息,最后合并算路径,即可得到包含这条边的所有路径的信息,剩下的路径在两棵树中递归处理。


对于树分治可以参考qzc大神的论文(https://wenku.baidu.com/view/8861df38376baf1ffc4fada8.html?re=view)


例题3:(http://poj.org/problem?id=1741)


Tree
Time Limit: 1000MS   Memory Limit: 30000K
Total Submissions: 21999   Accepted: 7228

Description

Give a tree with n vertices,each edge has a length(positive integer less than 1001). 
Define dist(u,v)=The min distance between node u and v. 
Give an integer k,for every pair (u,v) of vertices is called valid if and only if dist(u,v) not exceed k. 
Write a program that will count how many pairs which are valid for a given tree. 

Input

The input contains several test cases. The first line of each test case contains two integers n, k. (n<=10000) The following n-1 lines each contains three integers u,v,l, which means there is an edge between node u and v of length l. 
The last test case is followed by two zeros. 

Output

For each test case output the answer on a single line.

Sample Input

5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0

Sample Output

8

Source

LouTiancheng@POJ

解题思路:

给你一棵TREE,以及这棵树上边的距离。问有多少对点它们两者间的距离小于等于K。

我们知道一条路径要么过根结点,要么在一棵子树中,这启发了我们可以使用分治算法。

bubuko.com,布布扣

只要先求出经过根结点的路径数,再递归的求经过所有子结点的路径数即可。

下面来分析如何处理路径过根结点的情况。 

我们先用一次搜索求出根的所有子结点到根的距离并将其放入一个数组中,复杂度O(n)。

将这个距离数组排序,复杂度O(nlogn)。

这样就将问题转化为了,求一个数组A中,和小于等于K的元素对个数有多少。

由于数组有序,对于区间[L,R],易知若A[L]+A[R]>K,那么区间内没有满足条件的元素对。若A[L]+A[R]<=K,则以L为左端点的点对数有R-L个。

我们从1开始枚举L,当前R不满足条件,就令R-1,否则统计以L为左端点的点对数,令L-1。

用一个线性扫描的扫描可以解决,复杂度O(n)。

最终我们得到了所有子结点到根的距离和小于等于K的点对数。

然而这个并不是最终解,因为我们要求的是经过根的路径,而从一个子树到达根结点又回到同一个子树的路径是不能被计入统计的,所以我们要把多余的点对从结果中减去。

我们只要对每一个子树,求出同一个子树中的结点到根结点又回到子树的路径和小于等于K的点对数,然后从答案中减去即可。 即对于在同一个子树中的节点,在搜大树的时候先不做任何处理。在搜子树的时候减去即可。



//AC

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn=10010;
int N,K;
int ans,root,Max;
struct node
{
    int v,next,w;
}edge[maxn*2];
int head[maxn],tot;
int size[maxn];//树的大小
int maxv[maxn];//最大孩子节点的size
int vis[maxn];
int dis[maxn];
int num;
void init()
{
    tot=0;
    ans=0;
    memset(head,-1,sizeof(head));
    memset(vis,0,sizeof(vis));
}
void add_edge(int u,int v,int w)
{
    edge[tot].v=v;
    edge[tot].w=w;
    edge[tot].next=head[u];
    head[u]=tot++;
}
//处理子树的大小
void dfssize(int u,int f)
{
    size[u]=1;
    maxv[u]=0;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==f||vis[v])continue;
        dfssize(v,u);
        size[u]+=size[v];
        if(size[v]>maxv[u])maxv[u]=size[v];
    }
}
//找重心
void dfsroot(int r,int u,int f)
{
    if(size[r]-size[u]>maxv[u])//size[r]-size[u]是u上面部分的树的尺寸,跟u的最大孩子比,找到最大孩子的最小差值节点
        maxv[u]=size[r]-size[u];
    if(maxv[u]K&&i


你可能感兴趣的:(树的直径,树的重心以及树的分治)