Эффективный алгоритм для определения того, содержит ли предполагаемое двоичное дерево цикл?

Один из моих любимых вопросов на собеседовании -

За время O (n) и пространство O (1) определите, содержит ли связанный список цикл.

Это можно сделать с помощью алгоритма поиска цикла Флойда.

Мой вопрос в том, можно ли получить такие хорошие временные и пространственные гарантии при попытке определить, содержит ли двоичное дерево цикл. То есть, если кто-то дает вам struct определение в духе

struct node {
    node* left;
    node* right;
};

Насколько эффективно вы можете проверить, что данная структура действительно является двоичным деревом, а не, скажем, DAG или графом, содержащим цикл?

Есть ли алгоритм, который, учитывая корень двоичного дерева, может определить, содержит ли это дерево цикл за время O (n) и лучше, чем за O (n) пространство? Ясно, что это можно сделать с помощью стандартной DFS или BFS, но для этого требуется O (n) пространства. Можно ли это сделать в O (n) пространстве? O (log n) пробел? Или (Святой Грааль) всего за O (1) пространство? Мне любопытно, потому что в случае связанного списка это можно сделать в пространстве O (1), но я никогда не видел соответствующего эффективного алгоритма для этого случая.


person templatetypedef    schedule 21.08.2011    source источник
comment
Да: bool containsCycle(Tree t) { return false; }. Деревья не могут содержать циклов по определению. Вы хотели спросить, есть ли в каком-нибудь общем графике цикл?   -  person Peter Alexander    schedule 21.08.2011
comment
Дерево не может содержать цикл по определению. Я думаю, вы имеете в виду какой-то график.   -  person Karl Knechtel    schedule 21.08.2011
comment
@Karl - если это не генеалогическое древо: stackoverflow.com/ questions / 6163683 /   -  person Flexo    schedule 21.08.2011
comment
(Также, если это был вопрос на собеседовании, как предполагает тег - беги!)   -  person Flexo    schedule 21.08.2011
comment
Вы имеете в виду направленные или ненаправленные циклы?   -  person svick    schedule 21.08.2011
comment
@awoodland: Забавно, но все еще не имеет циклов :-) Семейные деревья - это ориентированные ациклические графы. Наличие цикла в генеалогическом древе означало бы, что чья-то дочь также была их матерью (или что-то в этом роде).   -  person Peter Alexander    schedule 21.08.2011
comment
@ svick- Извините, я должен был быть более точным в вопросе. Ищу неориентированные циклы.   -  person templatetypedef    schedule 21.08.2011
comment
@ Питер Александр: Да, извините, я знал это. Мой вопрос больше похож на то, если у вас есть структура данных, которая может кодировать двоичное дерево (каждый узел имеет в нем два указателя), действительно ли она описывает двоичное дерево? Или он что-то кодирует с циклом? Спасибо что подметил это!   -  person templatetypedef    schedule 21.08.2011
comment
В теме, я считаю, что лучше писать бинарные деревья, чем бинарные деревья поиска, даже если это не деревья.   -  person sdcvvc    schedule 21.08.2011
comment
@ sdcvvc- Упс! Да, это определенно была ошибка. Чувак, я здесь в восторге от этого вопроса ...   -  person templatetypedef    schedule 21.08.2011
comment
если круга нет, но есть 2 маршрута к одной и той же вершине [т.е. edge to a sibling], что должен дать алгоритм?   -  person amit    schedule 21.08.2011
comment
@ amit- Поскольку это неориентированный цикл, он должен (надеюсь) сообщить о цикле. Однако, если вы можете заставить алгоритм работать исключительно для случая направленного цикла, это все равно будет довольно круто.   -  person templatetypedef    schedule 21.08.2011
comment
Просто подумайте: если вы также сохраняете то, что больше поддерево (левое или правое) в каждом узле, я думаю, вы можете использовать DFS с O(log n) пространством, аналогично тому, как вы получили бы быструю сортировку для использования O(log n) пространства: рекурсивно только на меньшем поддерево и повторение / использование хвостовой рекурсии на большем дереве.   -  person IVlad    schedule 22.08.2011
comment
Траверс дерева сверху вниз. Отметьте каждый посещенный узел. Если вы встретите отмеченный узел - у вас цикл. Пробел: O (n) / Время: O (nlog (n))   -  person dfens    schedule 23.08.2011
comment
Downvoter - Вы можете объяснить, что не так в этом вопросе?   -  person templatetypedef    schedule 23.08.2011
comment
@ dfens - Мой вопрос в основном о том, как сделать это меньше, чем за O (n) пространство. Но спасибо за мысль!   -  person templatetypedef    schedule 23.08.2011
comment
извините: время O (n), а не O (nlog (n)), конечно   -  person dfens    schedule 23.08.2011
comment
Проблема здесь в том, что как только вы обмениваете пространство, время увеличивается и наоборот. Я не уверен, что это вполне возможно - хотя вы могли бы написать алгоритм, который экономил бы пространство, если график недействителен (см. мой ответ).   -  person Jonathan Dickinson    schedule 23.08.2011
comment
O (1) для времени будет хранить логическое значение, указывающее, является ли узел родительским, и предотвращать вставку, если это так.   -  person Jonathan Dickinson    schedule 23.08.2011
comment
Вы хотите обнаруживать только циклы (дающие бесконечный нисходящий путь) или также два способа добраться до вершины? Например, x = новый узел (null, null); y = новый узел (x, x); не имеет бесконечного нисходящего пути, но есть два способа добраться от y до x.   -  person sdcvvc    schedule 26.08.2011
comment
+1 Если не думать о правильно описанной постановке задачи, это действительно хороший вопрос.   -  person Saeed Amiri    schedule 12.09.2011


Ответы (10)


Вы даже не можете посетить каждый узел настоящего честного дерева без циклов в пространстве O (1), поэтому то, о чем вы просите, явно невозможно. (Уловки с изменением дерева по пути не занимают O (1) пробела).

Если вы хотите рассмотреть алгоритмы на основе стека, то обычный обход дерева можно легко изменить по аналогии с алгоритмом Флойда.

person n. 1.8e9-where's-my-share m.    schedule 22.08.2011
comment
Это хороший момент, что вы не можете использовать пространство O (1), но могли бы вы сделать это (скажем) в пространстве O (log n)? Я думаю, вам просто нужно достаточно места для хранения решений, которые вы приняли в прошлом, чтобы вы могли избежать ненужного повторения одного и того же пути много раз. - person templatetypedef; 22.08.2011
comment
@templatetypedef: вы можете сделать это за O (h), где h - высота дерева. Для сбалансированных деревьев это O (log N), для произвольных деревьев O (N). Вы сохраняете свои решения в списке и запускаете указатель с половинной скоростью вниз по этому списку, как в алгоритме Флойда. Для каждых двух шагов вниз по дереву дополнительный указатель перемещается на один шаг вниз по пути принятия решения. Единственное отличие от исходного алгоритма Флойда состоит в том, что вы иногда возвращаетесь назад, как и дополнительный указатель. - person n. 1.8e9-where's-my-share m.; 23.08.2011
comment
Пространство будет зависеть от глубины дерева, поэтому, если окажется, что у него есть цикл, пространство, вероятно, будет больше похоже на O (n). - person phkahler; 23.08.2011
comment
Было бы легко сделать это в пространстве O (lg n), если бы мы могли сказать, что ни один узел не имеет более одного родителя. В противном случае нам определенно понадобится O (n) пространство. - person user606723; 24.08.2011
comment
Мне очень жаль, но обход Морриса действительно работает в O (1) (дополнительном) пространстве для изменяемых, допустимых бинарных деревьев. Если в спецификации задачи нет ограничения на неизменяемость двоичного дерева, вы не должны отклонять любое решение, основанное на нем. - person Frigo; 25.08.2011
comment
@Frigo: обход Морриса использует дочерние указатели NULL, которые существуют только в том случае, если узлы имеют фиксированный размер и действительно тратят пространство на указатели NULL. Одного бита достаточно, чтобы указать, есть ли у узла дочерний элемент. Обход Морриса с такой реализацией работать не будет. - person n. 1.8e9-where's-my-share m.; 25.08.2011
comment
Я не понимаю, как у вас может быть реализация двоичного дерева без явного хранения его дочерних элементов, только с битом. Конечно, хранение потомков в списке или на карте заставляет Морриса обходить алгоритм пространства O (n), но я сомневаюсь, что многие реализации хранят деревья таким образом. - person Frigo; 25.08.2011
comment
@Frigo: вам нужно сохранить ребенка, если есть есть ребенок. У половины узлов нет потомков. - person n. 1.8e9-where's-my-share m.; 25.08.2011

можно проверить в логарифмическом пространстве, принадлежат ли две вершины графа одному компоненту связности (Рейнгольд, Омер (2008), «Ненаправленная связность в лог-пространстве», Журнал ACM 55 (4): Статья 17, 24). страницы, DOI: 10.1145 / 1391289.1391291). Компонент связности циклический; следовательно, если вы можете найти две вершины в графе, которые принадлежат одной и той же компоненте связности, значит, в графе есть цикл. Рейнгольд опубликовал алгоритм через 26 лет после того, как впервые был поставлен вопрос о его существовании (см. http://en.wikipedia.org/wiki/Connected_component_%28graph_theory%29). Наличие алгоритма пространства O (1) звучит маловероятно, учитывая, что на поиск решения в пространстве журнала потребовалось 25 лет. Обратите внимание, что выбор двух вершин из графа и вопрос о том, принадлежат ли они циклу, эквивалентен вопросу о том, принадлежат ли они компоненту связности.

Этот алгоритм может быть расширен до решения в лог-пространстве для графов с исходящей степенью 2 (OP: «деревья»), поскольку достаточно проверить для каждой пары узла и одного из его ближайших братьев и сестер, принадлежат ли они к одному и тому же connect, и эти пары можно перечислить в пространстве O (log n), используя стандартный рекурсивный спуск по дереву.

person Antti Huima    schedule 22.08.2011
comment
Но докажет ли этот алгоритм, что граф - это дерево? В O (N) время? - person Hot Licks; 22.08.2011
comment
Не знаю, может быть, граф нециклический, но не дерево. Но это был не исходный вопрос. - person Antti Huima; 22.08.2011
comment
Я хочу сказать, что доказательство существования (или отсутствия) цикла между двумя точками на графике далеко от того, что необходимо для доказательства того, что граф является деревом (или кустом, или лесом, или чем-то еще). Некоторое количество усилий и памяти требуется для навигации по предполагаемому дереву и проверки всех ветвей. - person Hot Licks; 22.08.2011
comment
Верно, но у вас может быть специальный класс графов, который представляет собой двоичные деревья с обратными указателями, то есть классы, которые можно разложить на (1) двоичное дерево и (2) дуги, указывающие от узлов к их предкам в дереве. … и именно это, как я думал, имел в виду ОП. Это набор графов, отличный от общих ориентированных графов. Как только вы предположите этот набор графиков, логарифмического пространства будет достаточно для просмотра предполагаемого дерева. - person Antti Huima; 25.08.2011
comment
Алгоритм Рейнгольда - O (log n) для машин Тьюринга (O (1) явно невозможно, поскольку постоянное пространство = обычные языки), использует пространство O (1) в модели машины RAM. Требуется, чтобы для любой вершины вы могли перечислить всех соседей. В этом вопросе для любой вершины вам известны адреса только левой и правой соседей. Так что я не знаю, чем это может помочь. - person sdcvvc; 26.08.2011
comment
@sdcvvc Если это двоичное дерево с обратными указателями (эта структура данных имеет также другие имена, также известные как «дерево поиска DFS», и фактически используется в некоторых алгоритмах планаризации графа), то единственным отсутствующим соседом является родительский элемент узел; но вы можете сохранить указатель на него, когда спуститесь к самому узлу, поскольку на него нет другого указателя входа. В любом случае, я полагаю, что на этом этапе обсуждение потребует более глубокого исследования алгоритма Рейнгольда. - person Antti Huima; 13.09.2011

Если вы предполагаете, что цикл указывает на узел на той же или меньшей глубине в «дереве», тогда вы можете сделать BFS (итеративная версия) с двумя стеками, один для черепахи (x1) и один для зайца ( x2 скорости). В какой-то момент стек Зайца будет либо пустым (без цикла), либо будет подмножеством стека черепахи (цикл найден). Требуемое время - O (n k), а пространство - O (lg n), где n - количество используемых узлов, а k - время, необходимое для проверки условия подмножества, которое может быть ограничено сверху lg (n). Обратите внимание, что исходное предположение о цикле не ограничивает исходную задачу, поскольку предполагается, что это дерево, но для конечного числа дуг, которые образуют цикл с предыдущими узлами; ссылка на более глубокий узел в дереве не будет формировать цикл, а разрушит древовидную структуру.

Если дополнительно можно предположить, что цикл указывает на предка, тогда условие подмножества можно изменить, проверив, что оба стека равны, что происходит быстрее.

person HeXuX    schedule 22.08.2011
comment
пространство O (n). для полного двоичного дерева в стеке n / 2 узлов при посещении первого листа [полное двоичное дерево имеет n / 2 листьев]. - person amit; 25.08.2011
comment
Это не сработает для дерева, в котором узел более низкого уровня указывает на более высокий уровень. Более высокий узел будет в наборах черепаха и заяц, но цикла нет. i.imgur.com/EYVUk1M.png - person cdbitesky; 10.06.2013

Узнали о посещении

Вам нужно будет переопределить структуру как таковую (я не буду указывать здесь указатели):

class node {
    node left;
    node right;
    bool visited = false;
};

И используйте следующий рекурсивный алгоритм (очевидно, переделав его, чтобы использовать собственный стек, если ваше дерево может вырасти достаточно большим):

bool validate(node value)
{
   if (value.visited)
      return (value.visited = false);
   value.visited = true;
   if (value.left != null && !validate(value.left))
      return (value.visited = false);
   if (value.right != null && !validate(value.right))
      return (value.visited = false);
   value.visited = false;
   return true;
}

Комментарии: Технически он имеет O (n) пробел; из-за дополнительного поля в структуре. В худшем случае это также будет O (n + 1), если все значения находятся на одной стороне дерева и каждое значение находится в цикле.

Учитывать глубину

При вставке в дерево можно было отслеживать максимальную глубину:

struct node {
  node left;
  node right;
};
global int maximumDepth = 0;

void insert(node item) { insert(root, item, 1); }
void insert(node parent, node item, int depth)
{
    if (depth > maximumDepth)
        maximumDepth = depth;
    // Do your insertion magic, ensuring to pass in depth + 1 to any other insert() calls.
}

bool validate(node value, int depth)
{
    if (depth > maximumDepth)
        return false;
    if (value.left != null && !validate(value.left, depth + 1))
        return false;
    if (value.right != null && !validate(value.right, depth + 1))
        return false;
    return true;
}

Комментарии: объем памяти равен O (n + 1), потому что мы храним глубину в стеке (а также максимальную глубину); время по-прежнему O (n + 1). Это было бы лучше для недопустимых деревьев.

person Jonathan Dickinson    schedule 23.08.2011
comment
рекурсивный вызов также требует пространства O (n) [в стеке вызовов], поэтому это не только поле "посещено" - person amit; 25.08.2011

Удалось понять это правильно!

  • Время выполнения: O (n). Я подозреваю, что он проходит через край самое большее постоянное количество раз. Никаких формальных доказательств.
  • Пробел: O (1). Хранит только несколько узлов. Не создает новых узлов или ребер, а только меняет их порядок.
  • Разрушительный: Да. Он сглаживает дерево, каждый узел имеет преемника в порядке его правого дочернего элемента, а ноль - в качестве левого.

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

Есть три случая в зависимости от локальной конфигурации узлов вокруг текущего: левый дочерний элемент такой же, как и предшественник, левый дочерний элемент отличается от предшественника или нет левого поддерева. Первый случай тривиален. Во втором случае требуется найти предшественника, в третьем случае нужно найти узел справа с левым поддеревом. Графическое представление помогает понять их.

В последних двух случаях мы можем зацикливаться. Поскольку мы просматриваем только список правильных дочерних элементов, мы можем использовать алгоритм обнаружения циклов Флойда, чтобы находить и сообщать о циклах. Рано или поздно каждый цикл превратится в такую ​​форму.

#include <cstdio>
#include <iostream>
#include <queue>

#define null NULL
#define int32 int

using namespace std;

/**
*   Binary tree node class
**/

template <class T>
class Node
{

    public:

    /*  Public Attributes   */

        Node* left;
        Node* right;
        T value;

};

/**
*   This exception is thrown when the flattener & cycle detector algorithm encounters a cycle
**/

class CycleException
{

    public:

    /*  Public Constructors */

        CycleException () {}
        virtual ~CycleException () {}

};

/**
*   Biny tree flattener and cycle detector class.
**/

template <class T>
class Flattener
{

    public:

    /*  Public Constructors */

        Flattener () :
            root (null),
            parent (null),
            current (null),
            top (null),
            bottom (null),
            turtle (null),
        {}

        virtual ~Flattener () {}

    /*  Public Methods  */

        /**
        *   This function flattens an alleged binary tree, throwing a new CycleException when encountering a cycle. Returns the root of the flattened tree.
        **/

        Node<T>* flatten (Node<T>* pRoot)
        {
            init(pRoot);
            //  Loop while there are left subtrees to process
            while( findNodeWithLeftSubtree() ){
                //  We need to find the topmost and rightmost node of the subtree
                findSubtree();
                //  Move the entire subtree above the current node
                moveSubtree();
            }
            //  There are no more left subtrees to process, we are finished, the tree does not contain cycles
            return root;
        }

    protected:

    /*  Protected Methods   */

        void init (Node<T>* pRoot)
        {
            //  Keep track of the root node so the tree is not lost
            root = pRoot;
            //  Keep track of the parent of the current node since it is needed for insertions
            parent = null;
            //  Keep track of the current node, obviously it is needed
            current = root;
        }

        bool findNodeWithLeftSubtree ()
        {
            //  Find a node with a left subtree using Floyd's cycle detection algorithm
            turtle = parent;
            while( current->left == null and current->right != null ){
                if( current == turtle ){
                    throw new CycleException();
                }
                parent = current;
                current = current->right;
                if( current->right != null ){
                    parent = current;
                    current = current->right;
                }
                if( turtle != null ){
                    turtle = turtle->right;
                }else{
                    turtle = root;
                }
            }
            return current->left != null;
        }

        void findSubtree ()
        {
            //  Find the topmost node
            top = current->left;
            //  The topmost and rightmost nodes are the same
            if( top->right == null ){
                bottom = top;
                return;
            }
            //  The rightmost node is buried in the right subtree of topmost node. Find it using Floyd's cycle detection algorithm applied to right childs.
            bottom = top->right;
            turtle = top;
            while( bottom->right != null ){
                if( bottom == turtle ){
                    throw new CycleException();
                }
                bottom = bottom->right;
                if( bottom->right != null ){
                    bottom = bottom->right;
                }
                turtle = turtle->right;
            }
        }

        void moveSubtree ()
        {
            //  Update root; if the current node is the root then the top is the new root
            if( root == current ){
                root = top;
            }
            //  Add subtree below parent
            if( parent != null ){
                parent->right = top;
            }
            //  Add current below subtree
            bottom->right = current;
            //  Remove subtree from current
            current->left = null;
            //  Update current; step up to process the top
            current = top;
        }

        Node<T>* root;
        Node<T>* parent;
        Node<T>* current;
        Node<T>* top;
        Node<T>* bottom;
        Node<T>* turtle;

    private:

        Flattener (Flattener&);
        Flattener& operator = (Flattener&);

};

template <class T>
void traverseFlat (Node<T>* current)
{
    while( current != null ){
        cout << dec << current->value << " @ 0x" << hex << reinterpret_cast<int32>(current) << endl;
        current = current->right;
    }
}

template <class T>
Node<T>* makeCompleteBinaryTree (int32 maxNodes)
{
    Node<T>* root = new Node<T>();
    queue<Node<T>*> q;
    q.push(root);
    int32 nodes = 1;
    while( nodes < maxNodes ){
        Node<T>* node = q.front();
        q.pop();
        node->left = new Node<T>();
        q.push(node->left);
        nodes++;
        if( nodes < maxNodes ){
            node->right = new Node<T>();
            q.push(node->right);
            nodes++;
        }
    }
    return root;
}

template <class T>
void inorderLabel (Node<T>* root)
{
    int32 label = 0;
    inorderLabel(root, label);
}

template <class T>
void inorderLabel (Node<T>* root, int32& label)
{
    if( root == null ){
        return;
    }
    inorderLabel(root->left, label);
    root->value = label++;
    inorderLabel(root->right, label);
}


int32 main (int32 argc, char* argv[])
{
    if(argc||argv){}

    typedef Node<int32> Node;

    //  Make binary tree and label it in-order
    Node* root = makeCompleteBinaryTree<int32>(1 << 24);
    inorderLabel(root);

    //  Try to flatten it
    try{
        Flattener<int32> F;
        root = F.flatten(root);
    }catch(CycleException*){
        cout << "Oh noes, cycle detected!" << endl;
        return 0;
    }

    //  Traverse its flattened form
//  traverseFlat(root);

}
person Frigo    schedule 12.09.2011
comment
Есть ли преимущество у алгоритма Флойда перед алгоритмом Брента? Последнее не потребовало бы перемещения двух указателей по дереву. Кроме того, я думаю, что это должно быть осуществимо и не слишком сложно (особенно при использовании алгоритма Брента вместо алгоритма Флойда), чтобы алгоритм оставил допустимое дерево в том же состоянии, в каком оно было до его запуска. Не уверен в алгоритме отката изменений в дереве с помощью циклов. - person supercat; 06.12.2011
comment
Так как я проверяю только списки в этом алгоритме, да, Брент тоже хорош. У меня также были некоторые мысли о том, чтобы оставить подсказки в преобразованном дереве для его восстановления (сохранение информации путем помещения поддерева в левый или правый дочерний элемент; или использование петель), но я не стал исследовать дальше. Если у вас есть успех, дайте мне знать;) - person Frigo; 07.12.2011

Как сказал Карл, по определению «Дерево» не имеет циклов. Но все же я улавливаю тот настрой, в котором задается вопрос. Зачем нужны навороченные алгоритмы определения цикла в любом графе. Вы можете просто запустить BFS или DFS, и если вы посещаете узел, который уже посещен, это подразумевает цикл. Это будет работать за O (n) время, но сложность пространства также O (n), не знаю, можно ли это уменьшить.

person FUD    schedule 21.08.2011
comment
Я думаю, вы упускаете суть вопроса. Цель не в том, чтобы обнаружить циклы в произвольном графе; только те, у которых каждый узел имеет валентность не более двух. Кроме того, меня особенно интересуют алгоритмы, которые не используют пространство O (n), хотя я знаю, что они существуют. - person templatetypedef; 21.08.2011
comment
Как упомянул автор, этот вопрос аналогичен циклу поиска в связанном списке, и хотя считается, что у узла есть левый и правый дочерние элементы, каждый из которых указывает на узел, который не может быть посещен каким-либо другим путем, нарушение означает структуру, подобную генеалогическому древу. . - person Shamim Hafiz; 21.08.2011
comment
Посмотрите, является ли дерево бинарным, оно не очень особенное по сравнению с, скажем, одномерным деревом (то есть списком), трехкомпонентным деревом и т.д. для двоичного дерева. Поскольку это обобщенная полиномиальная проблема, а не проблема np, где вы можете ожидать эффективных алгоритмов для небольших случаев, таких как (установить задачу покрытия для размера набора два). В любом случае будем искать дельных ответчиков. - person FUD; 21.08.2011
comment
если дерево сбалансировано, оно будет O (logn) [с DFS]. BFS не справится с этой задачей, вы, если есть преимущество перед братом (без круга), алгоритм даст неправильный ответ. - person amit; 21.08.2011
comment
@amit: Нет, это O (n). Вы должны проверить все дерево. - person hammar; 21.08.2011
comment
@hammar: O (logn) пространство, а не время. вы сохраняете только маршрут к этой вершине, размер которой равен O (logn) в сбалансированных деревьях. - person amit; 21.08.2011
comment
@hammar, читая комментарий OP относительно неориентированных циклов [и не направленных], я возвращаю свой комментарий, вам нужно будет сохранить каждую вершину. - person amit; 21.08.2011
comment
@amit: А, понятно. Что ж, это сработает для поиска циклов, но не обнаружит, является ли график DAG. - person hammar; 21.08.2011
comment
@amit, пожалуйста, обратитесь сюда en.wikipedia.org/wiki/Depth-first_search, чтобы узнать время выполнения. DFS, а BFS - это не что иное, как симуляция DFS без рекурсии, которая тоже будет работать нормально. - person FUD; 21.08.2011
comment
@ChingPing: BFS НЕ является имитацией DFS без рекурсии, он (BFS) использует очередь, в то время как DFS использует стек [стек вызовов]. если OP хочет направленный цикл, BFS может вернуть неправильный ответ, когда есть граница между двумя соседними вершинами. - person amit; 21.08.2011

Как упоминал ChingPing, простая DFS должна помочь. Вам нужно будет пометить каждый узел как посещенный (необходимо выполнить некоторое сопоставление из ссылки узла на целое число), и если повторный вход предпринимается на уже посещенном узле, это означает, что существует цикл.

Однако это O (n) в памяти.

person Shamim Hafiz    schedule 21.08.2011
comment
Да, я в курсе, но я специально ищу алгоритмы сублинейного пространства, если это возможно. - person templatetypedef; 21.08.2011
comment
видя сложный характер проблемы, Вы можете опубликовать ее на cstheory.stackexchange.com. - person Shamim Hafiz; 21.08.2011

С первого взгляда видно, что эта проблема может быть решена недетерминированным применением алгоритма Флойда. Итак, что произойдет, если мы применим метод Флойда по принципу разделения и ветвления?

Что ж, мы можем использовать Floyd's из базового узла, а затем добавить дополнительный Floyd в каждую ветвь. Итак, для каждого терминального пути у нас есть экземпляр алгоритма Флойда, который на этом заканчивается. И если цикл когда-либо возникает, есть черепаха, у которой ДОЛЖЕН быть соответствующий заяц, преследующий ее. Итак, алгоритм завершен. (и как побочный эффект, каждый конечный узел достигается только одной парой заяц / черепаха, поэтому посещений O (n) и, следовательно, времени O (n). (сохраните узлы, от которых произошли ответвления, это не увеличивается) порядка величины памяти и предотвращает выбросы памяти в случае циклов) .Кроме того, это гарантирует, что объем памяти такой же, как и количество конечных узлов.Количество конечных узлов равно O (log n), но O (n) в худшем случае.

TL; DR: применять метод Флойда и ветвь каждый раз, когда у вас есть выбор:
время: O (n)
пробел: O (log n)

person Jean-Bernard Pellerin    schedule 21.08.2011
comment
Это классная идея, но я не уверен, что она выполняется за O (n) времени или использует пространство O (lg n). Если вам нужно разветвлять алгоритм каждый раз, когда вы сталкиваетесь с выбором, чтобы каждый независимый алгоритм Флойда работал правильно, разве вам не нужно хранить, какие выборы вы сделали на каждой ветке, чтобы черепаха и заяц шли по одному пути? через дерево? Точно так же, если вы можете в конечном итоге запустить алгоритм параллельно для каждой точки выбора, как вы можете гарантировать, что он будет работать за время O (n), когда для каждого из (возможно) O (n) путей длины O (lg n ) вам нужно O (lg n) раз каждый? - person templatetypedef; 22.08.2011
comment
количество конечных узлов для идеального двоичного дерева равно n/2. кроме того, если вы «разделите» каждый внутренний узел, у вас будет n / 2 из них, что приведет к пространству O (n) [по крайней мере, O (n) указателей], если я правильно понял предложенный алгоритм. - person amit; 22.08.2011
comment
Вы правы, я пошел слишком быстро и предположил log n. Этот метод не работает. - person Jean-Bernard Pellerin; 22.08.2011
comment
Это не сработает для дерева, в котором узел более низкого уровня указывает на более высокий уровень. Более высокий узел будет в наборах черепаха и заяц, но цикла нет. i.imgur.com/EYVUk1M.png - person cdbitesky; 10.06.2013

Я не верю, что существует алгоритм обхода дерева с размером меньше O (N). И для (предполагаемого) двоичного дерева для обнаружения циклов требуется не больше пространства / времени (в терминах «порядка»), чем для обхода дерева. Я считаю, что DFS будет обходить дерево за время O (N), поэтому, вероятно, O (N) является пределом в обоих измерениях.

person Hot Licks    schedule 21.08.2011
comment
Вы говорите, что я не верю, что существует алгоритм обхода дерева с меньшим, чем O (N) пространством. Я тоже в это не верю ... но мне бы хотелось увидеть доказательство. Прежде чем увидеть алгоритм Флойда, я мог бы также сказать, что не верю, что существует алгоритм для просмотра связанного списка с меньшим, чем O (N) пространством. И все же вот оно. - person Don Hatch; 05.02.2019
comment
@DonHatch - для обнаружения цикла вам нужен хотя бы один бит на узел, чтобы сохранить ранее посещенный статус. - person Hot Licks; 27.02.2019
comment
вы говорите: «Чтобы обнаружить цикл, вам нужен хотя бы один бит на узел, чтобы сохранить ранее посещенный статус». Да, это кажется очевидным, но, как я сказал выше, это также кажется очевидным для связанного списка, и все же это неверно для связанного списка: см. Алгоритм Флойда. Если вы все равно считаете, что это верно для деревьев, объясните, пожалуйста, принципиальное различие между связанным списком и деревом, которое делает хранение одного бита на узел необходимым для дерева, но не для связанного списка. - person Don Hatch; 27.02.2019

Хорошо, после дальнейших размышлений, я считаю, что нашел способ, при условии, что вы

  • знать количество узлов заранее
  • может вносить изменения в двоичное дерево

Основная идея состоит в том, чтобы пройти по дереву с помощью обхода дерева порядка Морриса и подсчитать количество посещенных узлов, как на этапе посещения, так и на этапе поиска индивидуальных предшественников. Если какой-либо из них превышает количество узлов, у вас определенно есть цикл. Если у вас нет цикла, то он эквивалентен простому обходу Морриса, и ваше двоичное дерево будет восстановлено.

Я не уверен, возможно ли это, не зная заранее количество узлов. Будем думать об этом больше.

person Frigo    schedule 22.08.2011