Как случайным образом сгенерировать двоичное дерево по номеру узла? - PullRequest
1 голос
/ 03 июля 2019
//Definition for a binary tree node.

public class TreeNode {
    int key;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { key = x; }
} 

Учитывая общее количество TreeNode int n, как генерировать случайно распределенное двоичное дерево (я имею в виду случайную форму двоичного дерева, не случайный ключ значение. Вы можете установить все ключевые значения TreeNodes равными 1) и вернуть TreeNode root.

Вот как реализовать следующий API:

public class RandomBinaryTree{
    public TreeNode binaryTreeGenerator(int n){

    }
}

PS: Например, n = 3, я хочу, чтобы алгоритм мог случайным образом генерировать одно из следующих 5 двоичных деревьев каждый раз :

     1      1        1           1          1
    /      /        / \           \          \
   1      1        1   1           1          1
  /        \                      /            \
 1          1                    1              1

Существует ли алгоритм генерации двоичных деревьев равновероятно с фиксированным числом узлов n?

1 Ответ

5 голосов
/ 03 июля 2019

Смещенное распределение, простой алгоритм

Начиная с корня, случайным образом выберите количество узлов в каждом поддереве, а затем повторите:

public class RandomBinaryTree {
    private Random random = new Random();

    public TreeNode binaryTreeGenerator(int n, int key){
        if (n == 0)
            return null;

        TreeNode root = new TreeNode(key);

        // Number of nodes in the left subtree (in [0, n-1])
        int leftN = random.nextInt(n);

        // Recursively build each subtree
        root.setLeft(binaryTreeGenerator(leftN, key));
        root.setRight(binaryTreeGenerator(n - leftN - 1, key));

        return root;
    }
}

Этот алгоритм не обеспечивает равномерное распределение результатови сильно поддержит сбалансированные деревья.Для простого доказательства рассмотрим случай n = 3 и вычислим вероятность появления для каждого из 5 возможных бинарных деревьев (см. каталонские числа для связанной комбинаторики).

Равномерное распределение, большевызов

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

Шаг 1: Создать случайное слово сбалансированное слово :

private static Random random = new Random();

// true means '(', false means ')'
private static boolean[] buildRandomBalancedWord(int n) {
    boolean[] word = new boolean[n * 2];
    List<Integer> positions = IntStream.range(0, 2 * n).boxed()
        .collect(Collectors.toList());
    for (int i = n; i > 0; i--) {
        int index = random.nextInt(n + i);
        word[positions.remove(index)] = true;
    }
    return word;
}

Шаг 2: Сгенерированное слово может иметь k «дефектов», которые в основном не соответствуют закрывающим скобкам.В статье показано, что существует способ переставить сгенерированное слово таким образом, чтобы полученное отображение представляло собой биекцию из набора слов с k дефектами в набор слов с 0 дефектами ( правильно сформированными словами ).Вот процедура:

private static void rearrange(boolean[] word, int start, int end) {
    int sum = 0;
    int defectIndex = -1;
    for (int i = start; i < end; i++) {
        sum = sum + (word[i] ? 1 : -1);
        if (defectIndex < 0 && sum < 0) {
            defectIndex = i;
        } else if (defectIndex >= 0 && sum == 0) {
            // We now have irreducible u = rtl spanning [defectIndex, i]
            int uLength = i - defectIndex + 1;
            boolean[] flipped = new boolean[uLength - 2];
            for (int j = 0; j < flipped.length; j++)
                flipped[j] = !word[defectIndex + j + 1];

            // Shift the remaining word
            if (i + 1 < end)
                System.arraycopy(word, i + 1, word, defectIndex + 1, end - (i + 1));

            // Rewrite uw as lwrt*, t* being the flipped array
            word[defectIndex] = true;
            System.arraycopy(flipped, 0, word, end - flipped.length, flipped.length);
            word[end - uLength + 1] = false;

            // Now recurse on w, worst case we go (word.length/2)-deep
            rearrange(word, defectIndex + 1, end - uLength + 1);
            break;
        }
    }
}

Шаг 3: Существует взаимно-однозначное отображение из правильно сформированных слов скобок в двоичные деревья: каждая пара совпадающих скобок является узломвсе внутри - это левое поддерево, а все после - это правое поддерево:

// There is probably a smarter way to do this
public static TreeNode buildTree(boolean[] word, int key) {
    Deque<TreeNode> stack = new ArrayDeque<>();
    boolean insertRight = false;
    TreeNode root = null;
    TreeNode currentNode = null;
    for (int i = 0; i < word.length; i++) {
        if (word[i]) {
            TreeNode previousNode = currentNode;
            currentNode = new TreeNode(key);

            if (root == null) {
                root = currentNode;
            } else if (insertRight) {
                previousNode.setRight(currentNode);
                insertRight = false;
            } else {
                previousNode.setLeft(currentNode);
            }

            stack.push(currentNode);
        } else {
            currentNode = stack.pop();
            insertRight = true;
        }
    }
    return root;
}

Некоторые служебные функции :

public static boolean[] buildRandomWellFormedWord(int n) {
    boolean[] word = buildRandomBalancedWord(n);
    rearrange(word, 0, word.length);
    return word;
}

public static String toString(boolean[] word) {
    StringBuilder str = new StringBuilder();
    for (boolean b : word)
        str.append(b ? "(" : ")");
    return str.toString();
}

Test : Давайте распечатаем фактическое распределение по 10 миллионам прогонов размером 3:

public static void main(String[] args) throws Exception {
    Map<String, Integer> counts = new HashMap<String, Integer>();
    int N = 10000000, n = 3;
    for (int i = 0; i < N; i++) {
        boolean[] word = buildRandomWellFormedWord(n);
        String str = toString(word);
        Integer count = counts.get(str);
        if (count == null)
            count = 0;
        counts.put(str, count + 1);
    }

    counts.entrySet().stream().forEach(e -> 
        System.out.println("P[" + e.getKey() + "] = " + e.getValue().doubleValue() / N));
}

Вывод должен выглядеть примерно так:

P[()()()] = 0.200166
P[(()())] = 0.200451
P[()(())] = 0.199894
P[((()))] = 0.199006
P[(())()] = 0.200483

Так что buildTree(buildRandomWellFormedWord(n), key) даст двоичное дереворазмер n после равномерного распределения по всем возможным деревьям.

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