📜  树的质心分解

📅  最后修改于: 2021-04-17 12:41:34             🧑  作者: Mango

背景 :
什么是树的质心?
树的质心是一个节点,如果将其从树中删除,则会将其拆分为“林”,这样林中的任何树最多将具有原始树中顶点数的一半。

假设树中有n个节点。节点的“子树大小”是指以该节点为根的树的大小。

Let S(v) be size of subtree rooted at node v

   S(v) = 1 + ? S(u) 

Here u is a child to v (adjacent and at a depth one 
greater than the depth of v).  

Centroid is a node v such that,

maximum(n - S(v), S(u1, S(u2, .. S(um) <= n/2

where ui is i'th child to v.

寻找质心
令T为具有n个节点的无向树。在树中选择任意节点v。如果v满足质心的数学定义,则我们有质心。否则,我们知道我们的数学不等式不成立,并据此得出结论,在v附近存在一些u,使得S(u)> n / 2。我们将u设为新的v并递归。

centroidDecomposition1

我们从不重新访问节点,因为当我们决定从其移到子树大小大于n / 2的节点时,我们有点宣称它现在属于节点小于n / 2的组件,因此我们将永远找不到我们的质心在那里。
无论如何,我们都在朝着质心前进。此外,树中的顶点数量有限。该过程必须停止,并且它将停止在所需的顶点。

算法 :

  1. 选择任意节点v
  2. 从v启动DFS,并设置子树大小
  3. 重新定位到节点v(或从属于树的任意v开始)
  4. 检查质心的数学条件v
    1. 如果条件通过,则将当前节点作为质心返回
    2. 否则,移动到具有“最大”子树大小的相邻节点,然后返回到步骤4

定理:给定一棵具有n个节点的树,质心始终存在。
证明:从解决问题的方法中可以清楚地看出,总是可以使用上述步骤找到质心。

时间复杂度

  1. 选择任意节点v:O(1)
  2. DFS:O(n)
  3. 重新定位到v:O(1)
  4. 查找质心:O(n)

重心分解:

找到树的质心是我们在此要实现的一部分。我们需要考虑如何将树组织成一种结构,以降低回答某些“类型”查询的复杂性。

算法

  1. 将质心作为新树的根(我们将其称为“质心树”)
  2. 递归分解生成的森林中的树木
  3. 将这些树的质心作为最后分裂它们的质心的子代。

质心树的深度为O(lg n),可以在O(n lg n)中构造,因为我们可以在O(n)中找到质心。

说明性的例子
让我们考虑一棵有16个节点的树。该图具有使用节点1的DFS设置的子树大小。

重心

我们从节点1开始,看看质心的条件是否成立。请记住,S(v)是v的子树大小。
光盘3

我们将节点6设为质心的根,然后递归于森林质心的3棵树上,将原始树分成两部分。

注意:在图中,质心生成的子树被与质心颜色相同颜色的虚线包围。

光盘4

我们将随后发现的质心作为最后分裂它们的质心的子代,并获得我们的质心树。

光盘5

注意:仅包含单个元素的树与其质心具有相同的元素。我们尚未对此类树使用颜色区分,而叶节点代表它们。

// C++ program for centroid decomposition of Tree
#include 
using namespace std;
  
#define MAXN 1025
  
vector tree[MAXN];
vector centroidTree[MAXN];
bool centroidMarked[MAXN];
  
/* method to add edge between to nodes of the undirected tree */
void addEdge(int u, int v)
{
    tree[u].push_back(v);
    tree[v].push_back(u);
}
  
/* method to setup subtree sizes and nodes in current tree */
void DFS(int src, bool visited[], int subtree_size[], int* n)
{
    /* mark node visited */
    visited[src] = true;
  
    /* increase count of nodes visited */
    *n += 1;
  
    /* initialize subtree size for current node*/
    subtree_size[src] = 1;
  
    vector::iterator it;
  
    /* recur on non-visited and non-centroid neighbours */
    for (it = tree[src].begin(); it!=tree[src].end(); it++)
        if (!visited[*it] && !centroidMarked[*it])
        {
            DFS(*it, visited, subtree_size, n);
            subtree_size[src]+=subtree_size[*it];
        }
}
  
int getCentroid(int src, bool visited[], int subtree_size[], int n)
{
    /* assume the current node to be centroid */
    bool is_centroid = true;
  
    /* mark it as visited */
    visited[src] = true;
  
    /* track heaviest child of node, to use in case node is 
       not centroid */
    int heaviest_child = 0;
  
    vector::iterator it;
  
    /* iterate over all adjacent nodes which are children 
       (not visited) and not marked as centroid to some 
       subtree */
    for (it = tree[src].begin(); it!=tree[src].end(); it++)
        if (!visited[*it] && !centroidMarked[*it])
        {
            /* If any adjacent node has more than n/2 nodes,
             * current node cannot be centroid */
            if (subtree_size[*it]>n/2)
                is_centroid=false;
  
            /* update heaviest child */
            if (heaviest_child==0 ||
                subtree_size[*it]>subtree_size[heaviest_child])
                heaviest_child = *it;
        }
  
    /* if current node is a centroid */
    if (is_centroid && n-subtree_size[src]<=n/2)
        return src;
  
    /* else recur on heaviest child */
    return getCentroid(heaviest_child, visited, subtree_size, n);
}
  
/* function to get the centroid of tree rooted at src.
 * tree may be the original one or may belong to the forest */
int getCentroid(int src)
{
    bool visited[MAXN];
  
    int subtree_size[MAXN];
  
    /* initialize auxiliary arrays */
    memset(visited, false, sizeof visited);
    memset(subtree_size, 0, sizeof subtree_size);
  
    /* variable to hold number of nodes in the current tree */
    int n = 0;
  
    /* DFS to set up subtree sizes and nodes in current tree */
    DFS(src, visited, subtree_size, &n);
  
    for (int i=1; i::iterator it;
  
    /* for every node adjacent to the found centroid
     * and not already marked as centroid */
    for (it=tree[cend_tree].begin(); it!=tree[cend_tree].end(); it++)
    {
        if (!centroidMarked[*it])
        {
            /* decompose subtree rooted at adjacent node */
            int cend_subtree = decomposeTree(*it);
  
            /* add edge between tree centroid and centroid of subtree */
            centroidTree[cend_tree].push_back(cend_subtree);
            centroidTree[cend_subtree].push_back(cend_tree);
        }
    }
  
    /* return centroid of tree */
    return cend_tree;
}
  
// driver function
int main()
{
    /* number of nodes in the tree */
    int n = 16;
  
    /* arguments in order: node u, node v
     * sequencing starts from 1 */
    addEdge(1, 4);
    addEdge(2, 4);
    addEdge(3, 4);
    addEdge(4, 5);
    addEdge(5, 6);
    addEdge(6, 7);
    addEdge(7, 8);
    addEdge(7, 9);
    addEdge(6, 10);
    addEdge(10, 11);
    addEdge(11, 12);
    addEdge(11, 13);
    addEdge(12, 14);
    addEdge(13, 15);
    addEdge(13, 16);
  
    /* generates centroid tree */
    decomposeTree(1);
  
    return 0;
}

输出 :

6 4 1 2 3 5 7 8 9 11 10 12 14 13 15 16

应用:

考虑下面的示例问题

Given a weighted tree with N nodes,  find the minimum number
of edges in a path of length K, or return -1 if such a path
does not exist.
  1 <= N <= 200000
  1 <= length(i;j) <= 1000000 (integer weights)
  1 <= K <= 1000000 

蛮力解决方案:对于每个节点,执行DFS查找到每个其他节点的距离和边数

时间复杂度:O(N 2 )显然无效,因为N = 200000

我们可以使用质心分解在O(N Log N)时间内解决上述问题。

  1. 执行质心分解以获得“子树树”
  2. 从分解的根部开始,如下解决每个子树的问题
    1. 解决当前子树的每个“子树”的问题。
    2. 从当前子树上的质心执行DFS,以计算包含质心的路径的最小边数
      1. 两种情况:质心在路径的末端或中间

        使用带时间戳的大小为1000000的数组来跟踪距质心的可能距离以及该距离的最小边数

      取上述两个中的最小值

基于质心分解的解决方案的时间复杂度为O(n log n)

参考 :
http://www.ugrad.cs.ubc.ca/~cs490/2014W2/pdf/jason.pdf