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

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

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

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

Мой вопрос заключается в том, можно ли получить такие хорошие временные и пространственные гарантии при попытке определить, содержит ли двоичное дерево цикл. То есть, если кто-то дает вам определение 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), но я никогда не видел соответственно эффективного алгоритма для этого случая.

Ответы [ 10 ]

7 голосов
/ 22 августа 2011

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

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

4 голосов
/ 22 августа 2011

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

2 голосов
/ 22 августа 2011

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

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

1 голос
/ 23 августа 2011

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

Информация о глубине

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

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

0 голосов
/ 12 сентября 2011

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

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

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

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

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

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

}
0 голосов
/ 22 августа 2011

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

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

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

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

0 голосов
/ 22 августа 2011

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

0 голосов
/ 22 августа 2011

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

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

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

0 голосов
/ 21 августа 2011

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

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

0 голосов
/ 21 августа 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...