Почему DFS недостаточно быстро проверяет, является ли граф деревом

Я пытался решить эту проблему Описание проблемы Кажется, правильная идея - проверить если данные графы имеют циклы (является ли деревом). Однако мой код не смог пройти тест 7 (всегда превышен лимит времени), есть идеи, как сделать это быстрее? Я использовал ДФС. Большое спасибо Да, наконец-то приняли. Проблема в 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.

person doctorlai    schedule 06.11.2012    source источник
comment
общее эмпирическое правило заключается в том, что рекурсия медленнее, чем итерационные аналоги. Попробуйте использовать bfs, если это возможно?   -  person FUD    schedule 06.11.2012
comment
хорошо, я согласен. но я удалил «рекурсию», используя буферный массив, чтобы снять отметку с посещенного массива. Кроме того, я думаю, что DFS быстрее находит цикл....   -  person doctorlai    schedule 06.11.2012
comment
Конечно, вы могли бы использовать известное свойство деревьев, сказать что-нибудь о количестве ребер и количестве вершин, чтобы избежать большей части этой работы? Вам нужно будет сделать это для каждого подключенного компонента.   -  person A. Webb    schedule 06.11.2012
comment
@FUD: утверждение неверно. Я проверил (и статистически доказал) это в этом потоке для итеративной/рекурсивной быстрой сортировки. Вам нужно иметь очень оптимизированную реализацию стека (для конкретной потребности), чтобы это утверждение было верным.   -  person amit    schedule 06.11.2012
comment
есть идеи, как сделать алгоритм быстрее, чтобы пройти все тесты?   -  person doctorlai    schedule 06.11.2012
comment
@DoctorLai: мне трудно следовать коду (на самом деле я не программист на паскале, извините) - но граф является деревом тогда и только тогда, когда он (1) связан. (2) |В| = |Е| + 1. Я не уверен, что вы делаете, но я бы использовал DFS или BFS только для проверки связности графа, а затем проверил количество вершин и ребер в графе - я бы попытался минимизировать итерацию работать как можно больше.   -  person amit    schedule 06.11.2012
comment
Благодарю. собственно в этой конкретной проблеме. правильная идея состоит в том, чтобы проверить, есть ли в данном графе циклы.   -  person doctorlai    schedule 06.11.2012
comment
Граф является деревом тогда и только тогда, когда он (1) связен и (2) не имеет циклов. Эти (1) и (2) эквивалентны (1) и (2), данным @amit.   -  person A. Webb    schedule 06.11.2012
comment
Я думаю, что наконец-то смогу понять, что вы здесь сделали (я не говорю на Паскале), и я думаю, что основная проблема заключается в снятии пометок, сделанном в fin:. Это заставляет вас делать DFS из каждой вершины, что здесь не нужно.   -  person A. Webb    schedule 06.11.2012
comment
да, наконец-то приняли!   -  person doctorlai    schedule 07.11.2012


Ответы (1)


Я почти ничего не знаю о Паскале, поэтому я могу неправильно истолковать то, что вы делаете, но я думаю, что главный виновник находится в fin, где вы снимаете отметки с посещенных вершин. Это заставляет вас выполнять DFS для каждой вершины, в то время как вам нужно сделать только одну для каждого компонента.

В случае, когда имеется более одного подключенного компонента, движение либо остановится, либо

  • потому что вершина указывает на уже отмеченную вершину, и в этом случае мы просто останавливаемся из-за того, что цикл был найден
  • потому что вершина не указывает ни на кого (кроме самой себя), и в этом случае нам нужно найти следующую непомеченную вершину и снова запустить другую DFS оттуда

Вам не нужно беспокоиться об учете возвратов, поскольку каждая вершина не более чем указывает на одну другую вершину в этой задаче. Также не нужно беспокоиться о том, какая DFS сделала какую маркировку, так как каждая из них будет работать только в пределах своего подключенного компонента.

В случае, когда вершина, указывающая на себя, встречается первой, ее следует еще не помечать, а пропустить.

Альтернативное решение с использованием набора объединений и количества вершин/ребер

Поскольку дерево обладает свойством, согласно которому количество ребер на единицу меньше количества вершин, есть другой способ решить проблему — определить (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 на себя или на кого-то другого.

Для потомков вот реализация в c. Давно это было, так что простите за небрежность. Он не самый быстрый, но проходит все тесты за отведенное время. Кодирование непересекающихся множеств прямо взято из псевдокода Википедии.

#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");
    }
}
person A. Webb    schedule 06.11.2012
comment
Интересное решение, но не могли бы вы объяснить, как это будет быстрее, чем dfs? Насколько я понимаю, ваше решение эквивалентно dfs с ранним выходом - person GreyGeek; 06.11.2012
comment
@GreyGeek Да, я верю, что ты прав. Я думаю, что отредактирую свой ответ, чтобы описать это как еще один способ подумать о проблеме. Если бы мы знали, что граф связан, сравнение количества вершин и ребер, безусловно, могло бы быть быстрее, чем обнаружение циклов с помощью DFS. Но, как бы то ни было, мы должны определить связность, что при этом делает DFS. - person A. Webb; 06.11.2012
comment
@DoctorLai Да, поэтому DFS должна быть такой же быстрой, даже быстрее из-за меньших накладных расходов. Смотрите начало моего ответа, чтобы узнать, что может замедлять работу вашего кода DFS. - person A. Webb; 07.11.2012
comment
спасибо .. да, вы правы .. нет необходимости выполнять поиск в каждой вершине. - person doctorlai; 07.11.2012