Задача от яндекса "Генерация скобочных последовательностей"

Задача сформулирована следующим образом:

Дано целое число n. Требуется вывести все правильные скобочные последовательности длины 2 ⋅ n, упорядоченные лексикографически (см. https://ru.wikipedia.org/wiki/Лексикографический_порядок).

В задаче используются только круглые скобки.

Желательно получить решение, которое работает за время, пропорциональное общему количеству правильных скобочных последовательностей в ответе, и при этом использует объём памяти, пропорциональный n.

Формат ввода Единственная строка входного файла содержит целое число n, 0 ≤ n ≤ 11

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

введите сюда описание изображения

В итоге я получил следующий код:

public class BracesGenerator5 {
    static char[] input;

    public static void main(String[] args) throws IOException {
        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
        int size = Integer.parseInt(r.readLine());
        input = new char[size * 2];
        generateBraces(0, 0, 0);
    }

    public static void generateBraces(int nextPositionToSetup, int openBrackets, int closeBrackets) {
        if (nextPositionToSetup == input.length) {
            System.out.println(new String(input));
            return;
        }
        if (openBrackets == 0 || openBrackets < input.length / 2 && nextPositionToSetup != input.length - 1) {
            input[nextPositionToSetup] = '(';
            generateBraces(nextPositionToSetup + 1, openBrackets + 1, closeBrackets);
        }
        if (closeBrackets < openBrackets) {
            input[nextPositionToSetup] = ')';
            generateBraces(nextPositionToSetup + 1, openBrackets, closeBrackets + 1);
        }
    }
}

Но яндекс говорит, что я не попадаю в Memory Limit(если много раз запускать, то иногда всё-таки попадаю) Что здесь ещё можно оптимизировать ?

Update

Вот такой вариант проходит иногда

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class BracesGenerator51 {
    static char[] input;

    public static void main(String[] args) throws IOException {
        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
        input = new char[Byte.parseByte(r.readLine()) * 2];
        generateBraces(0, 0, 0);
    }

    public static void generateBraces(int nextPositionToSetup, int openBrackets, int closeBrackets) {
        if (nextPositionToSetup == input.length) {
            System.out.println(input);
            return;
        }
        if (openBrackets < input.length / 2 && nextPositionToSetup != input.length - 1) {
            input[nextPositionToSetup] = '(';
            generateBraces(nextPositionToSetup + 1, openBrackets + 1, closeBrackets);
        }
        if (closeBrackets < openBrackets) {
            input[nextPositionToSetup] = ')';
            generateBraces(nextPositionToSetup + 1, openBrackets, closeBrackets + 1);
        }
    }
}

Update 2

Так вариант тоже не всегда проходит:

package yandex.algo.easy;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class BracesGenerator5 {
    static char[] input;
    static byte nextPositionToSetup = 0;

    public static void main(String[] args) throws IOException {
        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
        input = new char[Byte.parseByte(r.readLine()) * 2];
        generateBraces(0, 0);
    }

    public static void generateBraces(int openBrackets, int closeBrackets) {
        if (nextPositionToSetup == input.length) {
            System.out.println(input);
            return;
        }
        if (openBrackets == 0 || (openBrackets < input.length / 2 && nextPositionToSetup != input.length - 1)) {
            input[nextPositionToSetup] = '(';
            nextPositionToSetup++;
            generateBraces(openBrackets + 1, closeBrackets);
            nextPositionToSetup--;
        }
        if (closeBrackets < openBrackets) {
            input[nextPositionToSetup] = ')';
            nextPositionToSetup++;
            generateBraces(openBrackets, closeBrackets + 1);
            nextPositionToSetup--;
        }

    }
}

Update 3

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class BracesGenerator5 {
    static char[] input;
    static byte nextPositionToSetup = 1;

    public static void main(String[] args) throws IOException {
        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
        byte n = Byte.parseByte(r.readLine());
        if(n == 0) return;
        input = new char[n * 2];
        input[0] = '(';
        generateBraces(1, 0);
    }

    public static void generateBraces(int openBrackets, int closeBrackets) {
        if (nextPositionToSetup == input.length) {
            System.out.println(input);
            return;
        }
        if (openBrackets == 0 || (openBrackets < input.length / 2 && nextPositionToSetup != input.length - 1)) {
            input[nextPositionToSetup] = '(';
            nextPositionToSetup++;
            generateBraces(openBrackets + 1, closeBrackets);
            nextPositionToSetup--;
        }
        if (closeBrackets < openBrackets) {
            input[nextPositionToSetup] = ')';
            nextPositionToSetup++;
            generateBraces(openBrackets, closeBrackets + 1);
            nextPositionToSetup--;
        }
    }
}



Ответы (3 шт):

Автор решения: gstackoverflow

В общем яндекс крайне странная контора, которая сделала очень странные задания и много лет не может нормально описать условия) всё упёрлось в операции ввода/вывода. Следующий вариант работает всегда c запасом:

import java.io.*;

public class BracesGenerator5 {
    static final String inputFile = "input.txt";
    static final String outputFile = "output.txt";
    static BufferedReader br;
    static BufferedWriter bw;

    static char[] input;
    static byte nextPositionToSetup = 1;

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new FileReader(inputFile));
        bw = new BufferedWriter(new FileWriter(outputFile));
        byte n = Byte.parseByte(br.readLine());
        if(n == 0) return;
        input = new char[n * 2];
        input[0] = '(';
        generateBraces(1, 0);

        br.close();
        bw.close();
    }

    public static void generateBraces(int openBrackets, int closeBrackets) throws IOException {
        if (nextPositionToSetup == input.length) {
            bw.write(input);
            bw.newLine();
            return;
        }
        if (openBrackets == 0 || (openBrackets < input.length / 2)) {
            input[nextPositionToSetup] = '(';
            nextPositionToSetup++;
            generateBraces(openBrackets + 1, closeBrackets);
            nextPositionToSetup--;
        }
        if (closeBrackets < openBrackets) {
            input[nextPositionToSetup] = ')';
            nextPositionToSetup++;
            generateBraces(openBrackets, closeBrackets + 1);
            nextPositionToSetup--;
        }
    }
}

Какая здесь связь с алгоритмами - непонятно.

→ Ссылка
Автор решения: gstackoverflow

А вот это рабочий вариант при итеративном алгоритме:

import java.io.*;

public class BraceGeneratorIterative {
    static final String inputFile = "input.txt";
    static final String outputFile = "output.txt";
    static BufferedReader br;
    static BufferedWriter bw;
    static char[] result;

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new FileReader(inputFile));
        bw = new BufferedWriter(new FileWriter(outputFile));
        byte n = Byte.parseByte(br.readLine());
        if (n == 0) return;
        result = new char[2 * n];
        for (int i = 0; i < 2 * n; i++) {
            result[i] = (i < n) ? '(' : ')';
        }
        bw.write(result);
        bw.newLine();
        boolean sequenceWasGenerated = true;
        while (sequenceWasGenerated) {
            sequenceWasGenerated = generateNextSeq(n);
            if (sequenceWasGenerated) {
                bw.write(result);
                bw.newLine();
            }
        }
        br.close();
        bw.close();
    }

    public static boolean generateNextSeq(int n) {
        int open = 0;
        int closed = 0;
        for (int i = 2 * n - 1; i >= 0; i--) {
            if (result[i] == '(') {
                open++;
                if (closed > open) {
                    closed--;
                    result[i] = ')';
                    for (int k = 1; k <= open; k++) {
                        result[k + i] = '(';
                    }
                    for (int c = 1; c <= closed; c++) {
                        result[c + i + open] = ')';
                    }
                    return true;
                }
            } else {
                closed++;
            }
        }
        return false;
    }
}

→ Ссылка
Автор решения: Stanislav Volodarskiy

Да, задача оказалась про скорость вывода.

Для n = 15 рекурсивный алгоритм делает 48760365 изменений в буфере и печатает 9694845 строк по 31 байту каждая. В среднем меняется пять символов в массиве символов, после чего массив отправляется на печать.

Печать переводит каждый символ из массива из UNICODE в набор байт для записи в выходной поток. Это требует времени. Я и вы знаем что в массиве лежат ASCII символы, которые отображаются в байты выходного потока один к одному. Но Java этого не знает и применяет универсальные алгоритмы для обработки UNICODE.

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

Кроме того System.out.println рассчитан на интерактивное использование, он сбрасывает буфер после каждой строки, что дополнительно замедляет вывод.

Решение без оптимизации вывода:

import java.util.Scanner;

public class Temp {
    static int n;
    static char[] buffer;

    public static void main(String... args) {
        n = new java.util.Scanner(System.in).nextInt();
        buffer = new char[2 * n];
        enumerate(0, 0);
    }

    static void enumerate(int left, int right) {
        if (left == n && right == n) {
            System.out.println(buffer);
            return;
        }
        if (left < n) {
            buffer[left + right] = '(';
            enumerate(left + 1, right);
        }
        if (right < left) {
            buffer[left + right] = ')';
            enumerate(left, right + 1);
        }
    }
}

Запуск для n = 15 работает дольше пяти секунд (строка real). Большое время в строке sys связано с задержками при сбросе буфера:

$ time -p echo 15 | java Temp | wc -l
9694845
real 5.22
user 2.55
sys 6.68

Чтобы ускорить печать, перейдём от символов к байтам и уберём из процесса преобразование UNICODE. Перевод строки \n добавим в конец буфера, это в два раза сократит количество операций вывода. На System.out наденем буфер в 64 килобайта, он не имеет своего буфера.

Сам алгоритм не изменился, только вывод:

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Temp {
    static int n;
    static byte[] buffer;
    static BufferedOutputStream output = new BufferedOutputStream(System.out, 64 * 1024);

    public static void main(String... args) throws IOException {
        n = Integer.parseInt(new BufferedReader(new InputStreamReader(System.in)).readLine());
        buffer = new byte[2 * n + 1];
        buffer[2 * n] = '\n';
        enumerate(0, 0);
        output.flush();
    }

    static void enumerate(int left, int right) throws IOException {
        if (left == n && right == n) {
            output.write(buffer);
            return;
        }
        if (left < n) {
            buffer[left + right] = '(';
            enumerate(left + 1, right);
        }
        if (right < left) {
            buffer[left + right] = ')';
            enumerate(left, right + 1);
        }
    }
}

Скорость выросла в двадцать раз. Задержки при выводе почти исчезли:

$ time -p echo 15 | java Temp | wc -l
9694845
real 0.25
user 0.24
sys 0.08

Результаты тестирования на https://contest.yandex.ru/contest/8458/problems/D/. Слева программа с простым выводом, справа с оптимизированным. Простой вывод переполняет память в тринадцатом тесте. Могу предположить что UNICODE обработка порождает много временных объектов и сборщик мусора не успевает их убрать.

Вердикт Ресурсы Вердикт Ресурсы
1 ok 140ms / 10.83Mb 1 ok 69ms / 8.51Mb
2 ok 140ms / 10.83Mb 2 ok 69ms / 8.64Mb
3 ok 139ms / 10.83Mb 3 ok 72ms / 8.25Mb
4 ok 139ms / 10.83Mb 4 ok 71ms / 8.38Mb
5 ok 139ms / 10.83Mb 5 ok 71ms / 8.38Mb
6 ok 139ms / 10.83Mb 6 ok 72ms / 8.25Mb
7 ok 139ms / 10.96Mb 7 ok 72ms / 8.25Mb
8 ok 140ms / 10.83Mb 8 ok 72ms / 8.25Mb
9 ok 144ms / 10.83Mb 9 ok 71ms / 8.38Mb
10 ok 156ms / 11.34Mb 10 ok 71ms / 8.38Mb
11 ok 179ms / 11.99Mb 11 ok 80ms / 9.29Mb
12 ok 200ms / 14.83Mb 12 ok 80ms / 9.29Mb
13 memory-limit-exceeded 271ms / 22.49Mb 13 ok 81ms / 9.15Mb
14 ok 85ms / 9.15Mb
→ Ссылка