Задача от яндекса "Генерация скобочных последовательностей"
Задача сформулирована следующим образом:
Дано целое число 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 шт):
В общем яндекс крайне странная контора, которая сделала очень странные задания и много лет не может нормально описать условия) всё упёрлось в операции ввода/вывода. Следующий вариант работает всегда 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--;
}
}
}
Какая здесь связь с алгоритмами - непонятно.
А вот это рабочий вариант при итеративном алгоритме:
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;
}
}
Да, задача оказалась про скорость вывода.
Для 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 |
