Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров3.1K

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

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

Для длины n = 2

()

Для длины n = 4

(())
()()

Для длины n = 6

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

Для длины n = 8

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

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

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

1: 0 1
2: 0 1 2
3: 0 2 4 5
4: 0 5 10 13 14
5: 0 14 28 37 41 42
6: 0 42 84 112 126 131 132
7: 0 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))

Теги:
Хабы:
+6
Комментарии20

Публикации

Истории

Ближайшие события

4 – 5 апреля
Геймтон «DatsCity»
Онлайн
8 апреля
Конференция TEAMLY WORK MANAGEMENT 2025
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область