Правильная скобочная последовательность

от автора

Когда-то однажды я встретил классическую задачу с правильной скобочной последовательностью. Задача звучала как-то так: «Сгенерировать k-ю в лексикографическом порядке правильную скобочную последовательность длины 2n». Эта была одна из первых задач на алгоритмы, которую я встретил. До сих пор не понимаю общепринятое решение, потому придумал свое. Эта статья про это самое решение.

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

Для длины n = 2

()

Для длины n = 4

(())
()()

Для длины n = 6

((()))
(()())
(())()
()(())
()()()

Для длины n = 8

(((())))
((()()))
((())())
((()))()
(()(()))
(()()())
(()())()
(())(())
(())()()
()((()))
()(()())
()(())()
()()(())
()()()()

Условимся называть скобочную последовательность просто СП, а правильную скобочную последовательность ПСП. Допустим, СП длины 2n содержитC_nПСП. Тогда заметим, что для длины 2(n+1) в лексикографическом порядке последние C_nПСП в точности равны ПСП длины 2n в начало которых добавлен (). Если еще немного поднапрячься, можно заметить, что, если предыдущая за ней группа начинается с (()…, а если исключить оттуда (), получаемая группа соответствует ПСП длине 2n. Приходим к гипотезе, в которой следующая группа получается добавлением () между групп, начинающихся с (((…(((.

Данная статья не о ее доказательстве, так что этот момент я пропущу и сразу приступлю к реализации. Моя реализация учитывает обратный порядок сортировки ПСП, но это не сложно будет исправить, изменив «k-тый с начала» на «k-тый с конца», учитывая посчитанное количество всех ПСП. Посчитаем, сколько групп начинаются с одной (, двумя ((, тремя ((( и так далее скобками и впишем в матрицу.

1: 000 001
2: 000 001 002
3: 000 002 004 005
4: 000 005 010 013 014
5: 000 014 028 037 041 42
6: 000 042 084 112 126 131 132
7: 000 132 264 354 402 422 428 429

Эта информация поможет узнать по числу k со скольких скобочек он начинается. А точнее, по какому индексу произошла последняя «вставка» элементарной СП (). Тогда мы можем откатиться на массив вверх и проделать тоже самое для ПСП меньшего размера. После завершения программы у нас будет массив «вставок», с помощью которого легко решить вопрос за O(n^2).

Сразу же находим алгоритм поиска этих чисел:
f[n+1][0]=0
f[n+1][1]=f[n].last
f[n+1][k]= f[n + 1][k-1] + f[n + 1][1] — f[n][k-2]

Код на swift:

var array = (1...n + 1).map { Array(repeating: 0, count: $0) } array[1][1] = 1 for i in (2...n) {     array[i][1] = array[i - 1][i - 1]     for j in 2...i {         array[i][j] = array[i][1] + array[i][j - 1] - array[i - 1][j - 2]     } }
Алгоритм с линейной памятью:

Немного видоизменив алгоритм, результата можно добиться используя один массив. Так как мы обращаемся всегда только к 2 числам из предыдущего массива, заменяя при этом только одно, мы можем хранить их в памяти. К примеру right переменная будет хранить последнее число «предыдущего» массива, а left переменная вычитаемое число. Так же по индексу 0 мы будем хранить заменяемое число в итерации. Код на swift будет выглядеть так

var array = Array(repeating: 0, count: n + 1) var (l, r) = (0, 1) for i in 2...n {     (l, array[1]) = (0, r)     for j in 2...i {         let m = r - l + array[j - 1]         l = array[0]         array[0] = array[j]         array[j] = m     }     (array[0], r) = (array[1], array[i]) } array[0] = 0

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

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

var (l, r) = (-1, n + 1) while r - l > 1 {     let m = l + (r - l) / 2     if array[n][m] <= k {         l = m     } else {         r = m     } }

Чтобы откатиться на СП меньшего размера, нужно так же откатить и значение k. Если на этой итерации мы поняли, что требуется l-тая вставка, те, имеется хотя бы l открывающих скобок, то на следующей итерации будет хотя бы l-1 открывающих скобок, значит, нужно исключить array[n][l] и учитывать array[n — 1][l — 1] скобок. Возможно, будет так, что l = 0, тогда k не должен изменяться.

var inserts = Array(repeating: 0, count: n) for i in (1...n).reversed() {     var (l, r) = (-1, i + 1)     while r - l > 1 {         let m = l + (r - l) / 2         if array[i][m] <= k {             l = m         } else {             r = m         }     }     inserts[i - 1] = l     k = k - (l > 0 ? array[i][l] - array[i - 1][l - 1] : 0) }

Бинарный поиск можно ускорить, если начинать не с 0, а с l, прервать внешний цикл, если попасть в последний элемент и другое. Но здесь я лишь пытаюсь продемонстрировать идею. По-этому, так же продемонстрирую тривиальное решение преобразования массива вставок в СП за O(n^2):

var res: [Character] = [] for i in inserts {     res.insert(contentsOf: ["(", ")"], at: i) }  print(String(res))

Можно сделать, конечно же, лучше. Если создать массив открытых скобок длины 2n и закрывать скобки по мере возможности, создание СП выполнится за O(n).

var res: [Character] = Array(repeating: "(", count: 2 * n) var j = 0 var c = 0 for i in (1..<2 * n).reversed() {     if inserts[j] == c {         res[i] = ")"         j += 1         c += 1     } else {         c -= 1     } }

В итоге общая сложность оценивается в O(n^2) на память и O(n log n) на выполнение(если не учитывать время на генерацию таблицы, например, если заданы не пара (n, k), а множество таких пар). Можно так же реализовать за O(n) на память и O(n^2log n) на выполнение, что хуже классического результата. Используя этот алгоритм, основанный на массиве вставок, для нахождения номера по ПСП, а не наоборот, получается уже лучший результат, чем классический: O(n^2) на память и O(n) на выполнение или O(n) на память и O(n^2) на выполнение. Причем памяти требуется меньше даже в случае O(n^2).

Генерация индекса по ПСП:
let vbs = Array("(()(()())...())") let n = vbs.count / 2  var array = (1...n + 1).map { Array(repeating: 0, count: $0) } array[1][1] = 1 for i in (2...n) {     array[i][1] = array[i - 1][i - 1]     for j in 2...i {         array[i][j] = array[i][1] + array[i][j - 1] - array[i - 1][j - 2]     } }  var inserts = Array(repeating: 0, count: n)  var k = 0 var j = n - 1 for i in 0..<2 * n {     if vbs[i] == "(" {         k += 1     } else {         k -= 1         inserts[j] = k         j -= 1     } }  var m = 0 for i in (1...n).reversed() {     let ii = inserts[i - 1]     m += ii > 0 ? array[i][ii] - array[i - 1][ii - 1] : 0 }  print(array[n][n] - (m + 1))


ссылка на оригинал статьи https://habr.com/ru/articles/895632/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *