树中最小顶点覆盖的数量

时间:2013-08-31 17:55:54

标签: algorithm recursion graph-theory dynamic-programming greedy

[至少]有三种算法在线性(O(n))时间内在树中找到最小顶点覆盖。我感兴趣的是修改了这些算法的所有,这样我也可以得到这些最小顶点覆盖的数量。

例如,对于树P4(具有4个节点的路径),MVC的数量是3,因为我们可以选择节点:1和3,2和4或2和3。

当然,您可以为任何免费算法描述解决方案 - 不是全部3.我只是对所有这些算法感兴趣,但如果您有任何要添加的内容,请不要犹豫。

我将描述我知道的算法,以便您更轻松。

1。贪心算法。

我们可以注意到,对于每个边缘,我们必须包含其中一个节点。哪一个选择?假设我们有一个带有“普通”节点和叶子的边缘。哪个节点更好选择?当然不是叶子,因为另一个节点可能会帮助我们再多一个边缘。算法如下:

  1. 从任何不是叶子的节点开始。
  2. 为每个孩子进行DFS调用,并在返回时检查父项或子项是否被标记为顶点覆盖中的节点。如果不是,你必须选择其中一个,所以选择父母(并标记)。
  3. 叶子什么都不做。
  4. 以下是代码:https://ideone.com/mV4bqg

    #include<stdio.h>
    #include<vector>
    using namespace std;
    
    vector<int> graph[100019];
    int mvc[100019];
    
    int mvc_tree(int v)
    {
        mvc[v] = -1;
        if(graph[v].size() == 1)
            return 0;
        int x = 0;
        for(int i = 0; i < graph[v].size(); ++i)
            if(!mvc[graph[v][i]])
            {
                x += mvc_tree(graph[v][i]);
                if(mvc[v] < 1 && mvc[graph[v][i]] < 1)
                    ++x,
                    mvc[v] = 1;
            }
        return x;
    }
    
    int main()
    {
        int t, n, a, b, i;
    
        scanf("%d", &t);
        while(t--)
        {
            scanf("%d", &n);
            for(i = 1; i <= n; ++i)
                graph[i].clear();
            for(i = 1; i < n; ++i)
            {
                scanf("%d%d", &a, &b);
                graph[a].push_back(b);
                graph[b].push_back(a);
                mvc[i] = 0;
            }
            mvc[n] = 0;
            if(n < 3)
            {
                puts("1");
                continue;
            }
            for(i = 1; i <= n; ++i)
                if(graph[i].size() > 1)
                    break;
            printf("%d\n", mvc_tree(i));
        }
        return 0;
    }
    

    2。动态编程算法。

    我们也可以使用递归来解决任务。

    MVC(v) = min(
                  1 + sum(MVC(child) for child in v.children),
                  v.children.size + sum(MVC(grandchild) for grandchild in v.grandchildren)
                )
    

    当我们在节点v时,它可以是MVC或不是。如果是,我们将它添加到我们的结果1(因为我们包括v)和所有v的子项的子树的子结果。另一方面,如果它不在MVC中,那么他的所有孩子都必须在MVC中,所以我们添加了孩子的结果数量,并且为每个孩子添加他们孩子的子结果(所以v的孙子)。 该算法是线性的,因为我们检查每个节点2次 - 为他们的父母和祖父母。

    3。动态编程没有2。

    而不是节点v的2个状态(1 - 在MVC中,2 - 不在MVC中)我们可以使3添加“可能在MVC中”。这有什么用?首先,我们称MVC(v =随机节点,“可能”),因为我们不知道v是否应该在MVC中。 “可能”的结果是“是”和“否”的结果最小。 “是”的结果是1 +总和(v.children中的孩子的MVC(孩子,“可能”))。并且“否”的结果是(v.children)中的孩子的总和(MVC(孩子,“是”))。我觉得很明显为什么。如果没有,请在评论中提问。 因此,公式是:

    MVC(v, "maybe") = min(MVC(v, "yes"), MVC(v, "no"))
    MVC(v, "yes") = 1 + sum(MVC(child, "maybe") for child in v.children)
    MVC(v, "no") = sum(MVC(child, "yes") for child in v.children)
    

    复杂性也是O(n),因为每个节点都被检查两次 - “是”和“否”。

1 个答案:

答案 0 :(得分:4)

动态编程解决方案

此解决方案扩展了您的第三个算法“动态编程no 2”:我们递归地定义了六个函数

cover_maybe(v) := min(cover_no(v), cover_yes(v))
cover_no   (v) := sum(cover_yes  (child) for child in v.children)
cover_yes  (v) := sum(cover_maybe(child) for child in v.children) + 1

count_maybe(v) :=
  count_no (v)                  if cover_no(v) < cover_yes(v) 
  count_yes(v)                  if cover_no(v) > cover_yes(v) 
  count_no (v) + count_yes(v)   if cover_no(v) == cover(yes)

count_no   (v) := product(count_yes  (child) for child in v.children)
count_yes  (v) := product(count_maybe(child) for child in v.children)

前三个函数cover_maybe,cover_no和cover_yes与状态“可能”,“否”和“是”的函数MVC精确对应。它们计算需要包含在v下的子树的顶点覆盖中的最小顶点数:

  • cover_maybe(v)确定v。
  • 下面的子树的最小顶点覆盖
  • cover_no(v):v下面的子树的MVC,条件是 包含在此封面中。
  • cover_yes(v):v下面的子树的MVC,条件是v包含在本封面中。

说明:

  • cover_maybe(v):在任何顶点封面中,v是否包含在封面中。 MVC选择包含最少数量顶点的解决方案:cover_no(v)和cover_yes(v)的最小值。
  • cover_no(v):如果封面中未包含v,则所有儿童必须包含在封面内(以覆盖v与儿童的边缘)。因此,我们需要为v。
  • 的所有子项添加cover_yes(child)中包含的顶点
  • cover_yes(v):因为v包含在封面中,它已经覆盖了从v到孩子们的边缘---我们不限制是否将孩子包括在封面中,因此添加了cover_maybe(child)for v。
  • 的所有孩子

接下来的三个函数计算了这些MVC问题的解决方案数量:

  • count_maybe(v)计算v下的子树的MVC解决方案的数量。
  • count_no(v)计算MVC解决方案的数量,条件是 包含在封面中。
  • count_yes(v)计算MVC解决方案的数量,条件是封面中包含v。

说明:

  • count_maybe(v):我们需要考虑三种不同的情况:如果cover_no(v)小于cover_yes(v),那么最好总是从封面中排除v:count_maybe(v)= count_no(v) 。类似地,如果cover_yes(v)小于cover_no(v),我们总是在封面中包含v并设置count_maybe(v)= count_yes(v)。但是如果count_no(v)等于count_yes(v),那么我们可以从封面中包含或排除v。可能性的数量是总和:count_maybe(v)= count_no(v)+ count_yes(v)。
  • count_no(v)和count_yes(v):因为我们已经知道是否将节点v包含或排除到封面中,所以我们为子节点留下了独立的子树。可能的解决方案的数量是每个子树的解决方案计数的乘积。选择正确的子问题(count_yes或count_maybe)如上所述(对于cover_no(v)和cover_yes(v))。

有关实施的两点说明:

  • 与动态编程一样,您必须缓存每个函数的结果:第一次计算结果并将其存储在缓存中。当再次询问相同的查询时,结果将从缓存中读出,而不是再次计算。通过这种缓存,该算法的运行时间为O(n),因为每个节点最多可以计算一次六个函数中的每一个。
  • 您必须使用树的根开始计算(不是您在问题中建议的随机节点):即使问题是通过无向定义的 - 我们的“分而治之”算法选择一个根节点并根据与这个根的距离来安排节点的子节点。