Подтвердить что ты не робот

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

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

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

Это можно сделать, используя алгоритм поиска циклов Floyd.

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

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

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

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

4b9b3361

Ответ 1

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

Если вы готовы рассмотреть алгоритмы на основе стека, то регулярное древовидное движение может быть легко модифицировано по линиям алгоритма Флойда.

Ответ 2

можно проверить в логарифмическом пространстве, если две вершины графа принадлежат одной и той же компоненте связности (Рейнгольд, Омер (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: "деревья" ), так как достаточно проверить каждую пару a node и одного из ее непосредственных братьев и сестер, если они принадлежат одному компоненту соединения, и эти пары могут быть перечислены в пространстве O (log n) с использованием стандартного рекурсивного спуска дерева.

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

Посетил Aware

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

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), если все значения находятся на одной стороне дерева, и каждое значение находится в цикле.

Глубина Aware

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

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). Это сделало бы лучше на недействительных деревьях.

Ответ 6

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

Ответ 7

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

Это O (n) в памяти, хотя.

Ответ 8

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

Ответ 9

Хорошо после того, как я подумал, я считаю, что нашел способ, если вы

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

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

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

Ответ 10

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

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

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

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

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

#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 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 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);

}