树的子树

4
我有一棵包含n个节点的树。任务是找到给定树中出边指向其补集的子树数量,该数量小于或等于给定数字K。
例如:如果n=3且k=1,给定树为1---2---3,则总有效子树数为6。
{}, {1}, {3}, {1,2}, {2,3}, {1,2,3}

我知道我可以枚举所有 2^n 棵树并检查它们是否有效,但是有没有更快的方法?我能在 n 的多项式时间内完成吗?类似于 O(n^3) 或者甚至 O(n^4) 的复杂度会很好。

编辑:对于 k=1,这个值实际上等于 2*n


“1--2--3” 对我来说看起来不像一棵树? - Bergi
2
你错了,这个问题请在http://math.stackexchange.com/上提问。 - Maurice
1
@Bergi 1---2---3 是一棵树,朋友。它有3个节点和2条边。还可以有更复杂的树,我选了这个作为简单的例子。 - Salena
为什么不考虑{2}?补语也必须是一棵树吗? - ypercubeᵀᴹ
3
@MauriceA. 我并不完全不同意,但是math.stackexchange.com是否涉及计算复杂性?在我看来,它似乎也非常适合于SO。 - Wayne Uroda
显示剩余12条评论
3个回答

3
这是DP-on-a-tree范例的典型情况。我们稍微推广一下问题,允许指定根节点v,并按两种方式分层计数小边界树的数量:是否包括v,以及包含多少条边。
基本情况很简单。没有边缘,因此有两个子树:一个包括v,另一个不包括v,两个都没有边缘。否则,让e = {v,w}成为与v相邻的边。实例看起来像这样。
|\         /|
| \   e   / |
|L v-----w R|
| /       \ |
|/         \|

递归计算L以v为根和R以w为根的分层计数。

包括v的子树由包括v的L中的子树、可选的e和包括w的R中的子树组成。不包括v的子树由不包括v的L中的子树或R中的子树(重复计数空树)组成。这意味着我们可以通过将L的分层计数与R的分层计数进行卷积来获得分层计数。

以下是在您的示例上如何工作的方法。让我们选择根1。

  e
1---2---3

我们选择如图所示的e并进行递归。
1

includes-1 的向量为 [1],因为一个子树是 {1},没有边界。excludes-1 的向量也是 [1],因为一个子树是 {},同样没有边界。
2---3

我们计算2和3的方式与1相同。includes-2 的向量为 [1, 1],因为 {2, 3} 没有边界边,{2} 有一条。我们通过将 includes-2 向量为 2 并加上新边界边导致的移位 1,变成 [0, 1],再与 includes-3 向量为 3 的 includes-2 向量卷积得到,它是 [1, 0]。excludes-2 向量为 [1] + [1, 1] - [1] = [1, 1],其中 [1, 1] 是移位了的 includes-3 向量和 excludes-3 向量之和,减法是为了抵消 {} 的重复计数。
现在,对于原始调用,要获得 includes-1 向量,我们将 [0, 1],即 1 的 includes-1 向量移位后加上 [1] 和 [1, 1] 卷积的结果 [1, 2]。检查一下:{1, 2, 3} 没有边界,{1} 和 {1, 2} 有一条边界边。excludes-1 向量为 [1] + [1, 2, 1] - [1] = [1, 2, 1]。检查一下:{} 没有边界,{2, 3} 和 {3} 有一条边界,{2} 有两条边界。

2
@Abhishek 不好意思,我不能这样做。请花费超过7分钟的时间仔细思考一下。 - David Eisenstat
有没有什么特定的资源可以帮助我理解包含/排除向量? - Salena
@DavidEisenstat 我查阅了有关算法入门课程以及两个高级算法的主修课程材料,均没有动态规划的章节,当然涉及到了Dijkstra算法和Bellman-Ford算法。 - G. Bach
1
@G.Bach,这里有一个不错的SO答案:https://dev59.com/sG025IYBdhLWcg3wKCb-。优秀的[算法设计手册](http://www.amazon.com/Algorithm-Design-Manual-Steven-Skiena/dp/1848000693)详细介绍了它。引用一下书中的话:“DP通过存储部分结果有效地实现递归算法。关键是看原始算法是否反复计算相同的子问题。如果是,将每个子问题的答案存储在表中以便查找而不是重新计算可以导致高效的算法。” - TooTone
请问您在向量中存储了什么内容,以及如何计算边界边的数量? - kushpf
显示剩余6条评论

2

这是我对David Eisenstat方案的Python实现:

from sys import stdin
from numpy import *
from scipy import *

def roundup_pow2(x):
  """
  Round up to power of 2 (obfuscated and unintentionally faster :).
  """
  while x&(x-1):
    x = (x|(x>>1))+1
  return max(x,1)

def to_long(x):
    return long(rint(x))

def poly_mul(a,b):
  n = len(a) + len(b) - 1
  nr = roundup_pow2(n)
  a += [0L]*(nr-len(a))
  b += [0L]*(nr-len(b)) # pad with zeros to length n
  u = fft(a)
  v = fft(b)
  w = ifft(u*v)[:n].real             # ifft == inverse fft
  return map(to_long,w)

def pad(l,s) :
    return l+[0L]*(s-len(l))

def make_tree(l,x,y):
    l[x][y]=y
    l[x].pop(y)
    for child in l[x]:
        make_tree(l,child,x)

def cut_tree(l,x) :
    if len(l[x])==0:
        return [1L],[1L]
    y,_ = l[x].popitem()
    ai,ax=cut_tree(l,x)
    bi,bx=cut_tree(l,y)

    ci=[0L]+ai
    tmp=poly_mul(ai,bi)
    padlen=max(len(ci),len(tmp))
    ci=pad(ci,padlen)
    tmp=pad(tmp,padlen)
    ci=map(add,ci,tmp)

    cx=[0L]+bi
    padlen=max(len(cx),len(bx),len(ax))
    cx=pad(cx,padlen)
    bx=pad(bx,padlen)
    ax=pad(ax,padlen)
    tmp=pad([-1],padlen)
    cx=map(add,cx,bx)
    cx=map(add,cx,ax)
    cx=map(add,cx,tmp)

    return ci,cx



n,k = map(int,raw_input().split())
l=[{}]
for i in range(1,n+1):
    d={}
    l.append(d)
for i in range(1,n):
    x,y = map(int,raw_input().split())
    l[x][y]=y
    l[y][x]=x
make_tree(l,1,0)

i,x = cut_tree(l,1)
padlen=max(len(i),len(x))
i=pad(i,padlen)
x=pad(x,padlen)
combined=map(add,i,x)
sum=0L
for i in range(0,k+1) :
    sum+=combined[i]

print sum

0

让我们创建一个稍微大一点的树,就像下面这样。

        1
      / | \
    2   3  \
       /    4 
      7    / \
          5   6

让我们为每个节点'a'定义一个函数F(a, k),其中从节点'a'及以下移除'k'条边。 例如,如果从节点'a'移除'k'条边,则我们创建F(a, k)数量的子树。 (如果'a'不是根,则假定它连接到其父级)。
例如,在上面的树中(F(4, 1) = 2),因为我们通过在'4'下方删除2条边来创建2棵树 (我们假设4与父级相连,并且子树(5)和(6)不计入F(4,1))
我们首先遍历并计算每个子节点的'F'。然后使用子节点的F计算父节点的F。
叶节点的F(a, k)为所有k都为0。
对于非叶节点。
F(a, k) = SUM(F(child, k)) + Z
而F(child, k)可以通过递归计算。
另一方面,Z是通过找到所有组合来计算的,其中一些子节点取出ri条边,使得SUM(ri) = k。

在编程中,可以通过固定给定子节点的“j”边,然后计算将“k-j”边分配给其他子节点所创建的树的数量来实现。

例如,在上述树中:

F(1, 3) = F(2, 3) + F(3, 3) + F(4, 3) +  // we pass k as-is to child  
      F(2,1)*F(3,1)*F(4,1) + F(2,1)*F(3,2) + F(2,1)*F(4,2) + //consume 1 edge by 2 and distribute 2 to other children
      F(2, 2)*F(3,1) + F(2,2)*F(4,1) + // consume 2 edges from node '2' and 1 for other children
      F(3,1)*F(4,2) 

如上所述,我们将节点2的'r'边固定,然后将'3-r'边分配给其他子节点。 我们对'1'的所有子节点都这样做。

此外,当我们从父节点中分离一个节点时,我们会创建子树。 例如,在上面的情况下,当我们计算F(1, 3)时,我们创建以下 分离的树。 detached_tree += F(2, 2) + F(3, 2) + F(4, 2)
在这里,我们假设一个边被用于从父节点中分离子节点, 并且在子节点中,如果我们使用'k-1'条边,我们将创建F(child, k-1)个子树。 这些树被计数并单独存储在detached_trees中。

一旦我们计算出了所有节点的F(a,k)。

所有子树为'SUM(F(root, k)) for all k' + 'total nodes - 1' + detached_trees。

我们将'total nodes - 1'添加到总数中。这是因为当一个节点(除了根节点)被分离 从树中,它会创建两个缺少1条边的树。虽然其中一个树被计算 在F(parent, 1)中,另一个树没有被计算在任何地方,因此需要在总数中计算。

以下是上述算法的C代码。递归可以进一步优化。

#define MAX 51
/* We use the last entry of alist to store number of children of a given node */
#define NUM_CHILD(alist, node) (alist[node][MAX])
int alist[MAX][MAX+1] = {0};
long F[MAX][MAX]={0};
long detached_subtrees = 0;

/*
 *  We fix one of the child node for 'i' edges out of 'n', then we traverse
 *  over  the rest of the children to get 'n-i' edges, we do so recursivly.
 *  Note that if 'n' is 1, we can always build a subtree by detaching.
 */
long REST_OF_NODES_SUM(int node, int q, int n)
{
    long sum = 0, i, node2, ret = 0, nd;

    /* fix node2 and calcualte the subtree for rest of the children */
    for(nd = q; nd < NUM_CHILD(alist, node); nd++) {
            node2 = alist[node][nd];
            /* Consume 'i' edges and send 'n-i' for other children of node */
            for (i = 1; i < n ; i++) {
                    sum = REST_OF_NODES_SUM(node, nd + 1, n - i);
                    ret += (F[node2][i] * sum);
                    /* Add one for 'node2' getting detached from tree */
                    if (i == 1) { ret += sum; }
            }
            ret += F[node2][n];
            /* If only one edge is to be consumed, we detach 'node2' from the tree */
            if (n == 1) { ret++; }
    }

    return ret;
}

void get_counts(int N, int K, int node, int root)
{
    int child_node;
    int i, j, p, k;

    if (NUM_CHILD(alist, node) == 0) { return; }

    for(i = 0 ; i < NUM_CHILD(alist, node); i++) {
            child_node = alist[node][i];
            /* Do a recursive traversal of all children */
            get_counts(N, K, child_node, node);
            F[node][1] += (F[child_node][1]);
    }

    F[node][1] += NUM_CHILD(alist, node);

    for (k = 2; k <= K; k++) {
            for(p = 0; p < NUM_CHILD(alist, node); p++) {
                    child_node = alist[node][p];
                    F[node][k] += F[child_node][k];
                    /* If we remove this child, then we create subtrees in the child */
                    detached_subtrees += F[child_node][k-1];

                    /* Assume that 'child_node' is detached, find tree created by rest
                     * of children for 'k-j' edges */
                    F[node][k] += REST_OF_NODES_SUM(node, p + 1, k - 1);

                    /* Fix one child node for 'j' edges out of 'k' and traverse over the rest of
                     * children for 'k - j' edges */
                    for (j = 1; j < k ; j++) {
                            if (F[child_node][j]) F[node][k] += (F[child_node][j] * REST_OF_NODES_SUM(node, p + 1, k - j));
                    }
            }
    }
}


void remove_back_ref(int parent, int node)
{
    int c;
    for (c = 0; c < NUM_CHILD(alist, node); c++) {
            if (alist[node][c] == parent) {
                    if ((c + 1) == NUM_CHILD(alist, node)) {
                            NUM_CHILD(alist, node)--;
                            alist[node][c] = 0;
                    } else {
                            /* move last entry here */
                            alist[node][c] = alist[node][NUM_CHILD(alist, node)-1];
                            alist[node][NUM_CHILD(alist, node)-1] = 0;
                            NUM_CHILD(alist, node)--;
                    }
            }
    }
}

/* go to each child and remove back links */
void normalize(int node)
{
    int j, child;

    for (j = 0; j < NUM_CHILD(alist, node); j++) {
            child = alist[node][j];
            remove_back_ref(node, child);
            normalize(child);
    }
}

long cutTree(int N, int K, int edges_rows, int edges_columns, int** edges)
{
    int i, j;
    int node, index;
    long ret = 0;

    /* build an adjacency list from the above edges */
    for (i = 0; i < edges_rows; i++) {
            alist[edges[i][0]][NUM_CHILD(alist, edges[i][0])] = edges[i][1];
            alist[edges[i][1]][NUM_CHILD(alist, edges[i][1])] = edges[i][0];
            NUM_CHILD(alist, edges[i][0])++;
            NUM_CHILD(alist, edges[i][1])++;
    }

    /* get rid of the back links in children */
    normalize(1);
    get_counts(N, K, 1, 1);
    for (i = 1; i <= K; i++) { ret += F[1][i]; }
    /* Every node (except root) when detached from tree, will create one extra subtree. */
    ret += (N - 1);
    /* The subtrees created by detaching from parent */
    ret += detached_subtrees;
    /* Add two for empty and full tree */
    ret += 2;

    return ret;
}

main(int argc, char *argv[])
{
    int **arr;
    int ret, i, N, K, x, y;

    scanf("%d%d", &N, &K);
    arr = malloc((N - 1) * sizeof(int*));
    for (i = 0; i < (N - 1); i++) { arr[i] = malloc(2*sizeof(int)); }

    for (i = 0; i < N-1; i++) { scanf("%d%d", &x, &y); arr[i][0] = x; arr[i][1] = y; }

    printf("MAX %d ret %ld\n", MAX, cutTree(N, K, N-1, 2, arr));
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接