【问题标题】:Number of minimum vertex covers in a tree树中的最小顶点覆盖数
【发布时间】:2013-09-04 06:16:25
【问题描述】:

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

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

当然,您可以描述任何免费算法的解决方案 - 不是全部 3 个。我只是对所有这些算法感兴趣,但如果您有什么要补充的,请不要犹豫。

我将描述我所知道的算法以使您更容易。

1。贪心算法。

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

  1. 从任何不是叶子的节点开始。
  2. 对每个子节点进行 DFS 调用,并在它返回时检查父节点或子节点是否被标记为顶点覆盖中的节点。如果不是,您必须选择其中之一,因此请选择父级(并标记它)。
  3. 对于一片叶子什么都不做。

这是代码: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 = random node, "maybe") 因为我们不知道 v 是否应该在 MVC 中。 “也许”的结果是“是”和“否”的最小结果。 “是”的结果是 1+sum(MVC(child, "maybe") for child in v.children)。 “no”的结果是 sum(MVC(child, "yes") for child in v.children)。我认为原因很清楚。如果没有,请在 cmets 中询问。 因此公式为:

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),因为每个节点都要检查两次——“是”和“否”。

【问题讨论】:

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


    【解决方案1】:

    动态编程解决方案

    此解决方案扩展了您的第三个算法“动态规划 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 精确对应,用于状态“maybe”、“no”和“yes”。他们计算需要包含在 v 下面的子树的顶点覆盖中的最小顶点数:

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

    解释:

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

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

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

    解释:

    • count_maybe(v):我们需要考虑三种不同的情况:如果cover_no(v) 小于cover_yes(v),那么最好总是从cover 中排除v:count_maybe(v)=count_no(v) .类似地,如果cover_yes(v) 小于cover_no(v),我们总是将v 包含在cover 中并设置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),因为六个函数中的每一个都可以对每个节点最多计算一次。
    • 您必须从树的根开始计算(而不是像您在问题中建议的那样使用随机节点):即使问题是用无向定义的 --- 我们的“分而治之”算法会选择一个根节点并根据与该根节点的距离排列节点的子节点。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-02-15
      • 1970-01-01
      • 1970-01-01
      • 2010-10-29
      • 1970-01-01
      • 1970-01-01
      • 2016-02-25
      • 2021-10-26
      相关资源
      最近更新 更多