【问题标题】:Why DFS isn't fast enough in checking if a graph is a tree为什么 DFS 在检查图是否为树时不够快
【发布时间】:2012-10-26 13:21:52
【问题描述】:

我试图解决这个问题 Problem Description 似乎正确的想法是检查给定的图是否有循环(是否是树)。但是,我的代码无法通过测试 7,(总是超过时间限制),知道如何让它更快吗?我使用了 DFS。非常感谢 是的,终于被录取了。问题是每个顶点上的dfs,这是不必要的。 dfs函数应该是这样的。

function dfs(idx: integer; id: integer): boolean;
begin
  if (visited[idx] = id) then
  begin
    Result := false;
    Exit;
  end;
  if (tree[idx] <> 0) then
  begin
    visited[idx] := id;
    Result := dfs(tree[idx], id);
    Exit;
  end;
  Result := true;
end;



program Project2;

{$APPTYPE CONSOLE}

var
  i, m, j, n, k: integer;
  tree: array [1 .. 25001] of integer;
  visited: array [1 .. 25001] of boolean;

function dfs(idx: integer): boolean;
label
  fin;
var
  buf: array[1 .. 25001] of integer;
  i, cnt: integer;
begin
  cnt := 1;
  while (true) do
  begin
    if (visited[idx]) then
    begin
      Result := false;
      goto fin;
    end;
    if (tree[idx] <> 0) then
    begin
      visited[idx] := true;
      buf[cnt] := idx;
      Inc(cnt);
      idx := tree[idx];
    end
    else
    begin
      break;
    end;
  end;
  Result := true;
fin:
  for i := 1 to cnt - 1 do
  begin
    visited[buf[i]] := false;
  end;
end;

function chk(n: integer): boolean;
var
  i: integer;
begin
  for i := 1 to n do
  begin
    if (tree[i] = 0) then continue;
    if (visited[i]) then continue;
    if (dfs(i) = false) then
    begin
      Result := false;
      Exit;
    end;
  end;
  Result := true;
end;

begin
  Readln(m);
  for i := 1 to m do
  begin
    Readln(n);
    k := 0;
    for j := 1 to n do
    begin
      Read(tree[j]);
      if (tree[j] = 0) then
      begin
        Inc(k);
      end;
    end;
    if (k <> 1) then
    begin
      Writeln('NO');
    end
    else
    if (chk(n)) then
    begin
      Writeln('YES');
    end
    else
    begin
      Writeln('NO');
    end;
    Readln;
  end;
  //Readln;
end.

【问题讨论】:

  • 一般的经验法则是递归比迭代慢。如果可能,尝试使用 bfs?
  • 好吧,我同意。但我已经通过使用缓冲区数组取消标记访问数组来删除“递归”。此外,我认为 DFS 发现周期更快....
  • 您当然可以使用树的已知属性,比如边数与顶点数的关系,以避免大部分工作?您需要为每个连接的组件执行此操作。
  • @FUD:声明是错误的。我对它进行了基准测试(并在统计上证明)in this thread 用于迭代/递归快速排序。您需要对堆栈进行非常优化的实现(针对特定需求)才能使该声明成立。
  • 一个图是一棵树当且仅当它 (1) 是连通的并且 (2) 没有环。这些 (1) 和 (2) 等价于 @amit 给出的 (1) 和 (2)。

标签: algorithm graph tree pascal


【解决方案1】:

我对 Pascal 几乎一无所知,所以我可能会误解你在做什么,但我认为主要的罪魁祸首是在 fin 你取消标记访问的顶点。这迫使您从每个顶点执行 DFS,而您只需要对每个组件执行一次。

如果有多个连接的组件,则运动将停止

  • 因为一个顶点指向一个已经标记的顶点,在这种情况下,我们会因为找到一个循环而停止
  • 因为顶点不指向任何人(但它自己),在这种情况下,我们需要找到下一个未标记的顶点并从那里再次启动另一个 DFS

您不必担心回溯的簿记,因为在此问题中每个顶点最多指向另一个顶点。也无需担心哪个 DFS 做了哪个标记,因为每个都只能在其连接的组件内工作。

如果首先遇到指向自身的顶点,则不应该标记它,而是跳过。

使用集合并集和顶点/边数的替代解决方案

由于树具有边数比顶点数少 1 的属性,因此还有另一种思考问题的方法——确定 (1) 连通分量和 (2) 比较边和顶点计算每个组件。

在许多语言中,您都有一个 Set 数据结构,其中包含随时可用的近乎恒定时间的 Union/Find 方法。在这种情况下,解决方案既简单又快速 - 边数接近线性。

为代表其连接组件的每个顶点创建一个集合。然后处理您的边缘列表。对于每条边,将两个顶点表示的集合合并。当你走的时候,跟踪每个集合中的顶点数和边数。同样的例子:

初始设置

Vertex         1  2  3  4  5
Belongs to     S1 S2 S3 S4 S5

Set            S1 S2 S3 S4 S5
Has # vertices 1  1  1  1  1
And # edges    0  0  0  0  0

从 1 到 2 处理边

Vertex         1  2  3  4  5
Belongs to     S1 S1 S3 S4 S5

Set            S1 S3 S4 S5
Has # vertices 2  1  1  1
And # edges    1  0  0  0

处理边缘从 2 到 3

Vertex         1  2  3  4  5
Belongs to     S1 S1 S1 S4 S5


Set            S1 S4 S5
Has # vertices 3  1  1
And # edges    2  0  0

处理边缘从 3 到 4

Vertex         1  2  3  4  5
Belongs to     S1 S1 S1 S1 S5

Set            S1 S5
Has # vertices 4  1
And # edges    3  0

处理边从 4 到 1

Vertex         1  2  3  4  5
Belongs to     S1 S1 S1 S1 S5

Set            S1 S5
Has # vertices 4  1
And # edges    4  0

我们可以在这里停下来,因为此时S1 违反了树的顶点与边数。 S1 中有一个循环。顶点 5 是指向自己还是指向其他人都没有关系。

为了后代,这是 中的一个实现。已经有一段时间了,所以请原谅马虎。它不是最快的,但它确实在时限内通过了所有测试。不相交集编码直接来自Wikipedia's pseudocode

#include <stdio.h>

struct ds_node
{
    struct ds_node *parent;
    int rank;
};

struct ds_node v[25001];

void ds_makeSet(struct ds_node *x)
{
    x->parent = x;
    x->rank = 0;
}

struct ds_node* ds_find(struct ds_node *x)
{
    if (x->parent != x) x->parent = ds_find(x->parent);
    return x->parent;
}

int ds_union(struct ds_node *x, struct ds_node *y)
{
    struct ds_node * xRoot;
    struct ds_node * yRoot;

    xRoot = ds_find(x);
    yRoot = ds_find(y);

    if (xRoot == yRoot) return 0;

    if (xRoot->rank < yRoot->rank) 
    {
        xRoot->parent = yRoot;
    }
    else if (xRoot->rank > yRoot->rank) 
    {
        yRoot->parent = xRoot;
    }
    else 
    {
        yRoot->parent = xRoot;
        xRoot->rank++;
    }
    return 1;
}

int test(int n)
{
    int i, e, z = 0;

    for(i=1;i<=n;i++)
    {
        ds_makeSet(&v[i]);
    }
    for(i=1;i<=n;i++)
    {
        scanf("%d",&e);
        if (e)
        {
            if ( !ds_union(&v[i],&v[e]) ) 
            {
                for(i++;i<=n;i++) scanf("%d",&e);
                return 0;
            }
        }
        else
        {
            z++;
        }
    }
    return (z == 1);
}
int main()
{
    int runs; int n;

    scanf("%d", &runs);
    while(runs--)
    {
        scanf("%d", &n); 
        getc(stdin);

        test(n) ? puts("YES") : puts("NO");
    }
}

【讨论】:

  • 有趣的解决方案,但你能解释一下这比 dfs 更快吗?在我看来,您的解决方案相当于提前退出的 dfs
  • @GreyGeek 是的,我相信你是正确的。我想我会编辑我的答案以将其描述为思考问题的另一种方式。如果我们知道图是连通的,那么比较顶点和边数肯定会比通过 DFS 进行循环检测更快。但是,事实上,我们必须确定连接性,而 DFS 会同时进行。
  • @DoctorLai 是的,因此 DFS 应该同样快,确实更快,因为开销更低。请参阅我的答案开头,以尝试诊断可能会降低 DFS 代码速度的原因。
  • 谢谢.. 是的,你是对的.. 没有必要对每个顶点进行 dfs。
猜你喜欢
  • 2021-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-07
  • 2016-03-29
相关资源
最近更新 更多