Правильные скобочные последовательности — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Количество правильных скобочных последовательностей. Числа Каталана)
Строка 1: Строка 1:
 
== Определения ==
 
== Определения ==
<wikitex>
+
 
 
{{Определение
 
{{Определение
 
|id = def1
 
|id = def1
 
|definition ='''Скобочная последовательность''' {{---}} класс комбинаторных объектов, представляющих собой последовательность скобочных символов.}}
 
|definition ='''Скобочная последовательность''' {{---}} класс комбинаторных объектов, представляющих собой последовательность скобочных символов.}}
 
'''Примеры скобочных последовательностей'''
 
'''Примеры скобочных последовательностей'''
*$(())))($
+
*<tex>(())))(</tex>
*$)()()))()(()())$
+
*<tex>)()()))()(()())</tex>
 
{{Определение
 
{{Определение
 
|id = def1
 
|id = def1
Строка 12: Строка 12:
  
 
*"" (пустая строка) есть правильная скобочная последовательность;
 
*"" (пустая строка) есть правильная скобочная последовательность;
*пусть $S$ {{---}} правильная скобочная последовательность, тогда $(S)$ есть правильная скобочная последовательность;
+
*пусть <tex>S</tex> {{---}} правильная скобочная последовательность, тогда <tex>(S)</tex> есть правильная скобочная последовательность;
*пусть $S1$, $S2$ {{---}} правильные скобочные последовательности, тогда $S1S2$ есть правильная скобочная последовательность;
+
*пусть <tex>S1</tex>, <tex>S2</tex> {{---}} правильные скобочные последовательности, тогда <tex>S1S2</tex> есть правильная скобочная последовательность;
 
}}
 
}}
 
'''Примеры правильных скобочный последовательностей'''
 
'''Примеры правильных скобочный последовательностей'''
*$((()()()()))$
+
*<tex>((()()()()))</tex>
*$(())(()())$
+
*<tex>(())(()())</tex>
</wikitex>
+
 
 
== Алгоритм проверки правильности скобочной последовательности ==
 
== Алгоритм проверки правильности скобочной последовательности ==
<wikitex>
+
 
Пусть нам дана скобочная последовательность, записанная в строку $s$. Возьмем переменную $counter$, $counter = 0$. Будем последовательно перебирать все символы этой строки. Если мы встречаем открывающуюся скобку, то увеличиваем $counter$ на $1$, закрывающую {{---}} уменьшаем на $1$. Если на протяжении всего перебора $counter$ было неотрицательным и после завершения осталось нулем, то скобочная последовательность правильна.
+
Пусть нам дана скобочная последовательность, записанная в строку <tex>s</tex>. Возьмем переменную <tex>counter</tex>, <tex>counter = 0</tex>. Будем последовательно перебирать все символы этой строки. Если мы встречаем открывающуюся скобку, то увеличиваем <tex>counter</tex> на <tex>1</tex>, закрывающую {{---}} уменьшаем на <tex>1</tex>. Если на протяжении всего перебора <tex>counter</tex> было неотрицательным и после завершения осталось нулем, то скобочная последовательность правильна.
</wikitex>
+
 
 
===Псевдокод===
 
===Псевдокод===
<wikitex>
+
 
   check(s)
+
   '''boolean''' check(s: '''string''')
 
     counter = 0
 
     counter = 0
     for i = 1 to length(s)
+
     '''for''' i = 1 '''to''' length(s)
       if s[i] == '('
+
       '''if''' s[i] == '('
 
         counter++
 
         counter++
       else
+
       '''else'''
 
         counter--  
 
         counter--  
       if counter < 0
+
       '''if''' counter < 0
 
         return false  
 
         return false  
     if counter == 0
+
     '''if''' counter == 0
 
       return true
 
       return true
     else
+
     '''else'''
 
       return false
 
       return false
  
 
Надо отметить, что скобочные последовательности могут состоять не только из одного типа скобок. При этом недопустимо такое расположение, когда один тип скобок закрывает другой:
 
Надо отметить, что скобочные последовательности могут состоять не только из одного типа скобок. При этом недопустимо такое расположение, когда один тип скобок закрывает другой:
</wikitex>
+
 
 
===Примеры скобочных последовательностей с несколькими типами скобок===
 
===Примеры скобочных последовательностей с несколькими типами скобок===
<wikitex>
+
 
*$()[()]\{()()[]\}$ {{---}} верно
+
*<tex>()[()]\{()()[]\}</tex> {{---}} верно
*$[(]\{\})$ {{---}} неверно
+
*<tex>[(]\{\})</tex> {{---}} неверно
  
 
В этом случае для проверки надо будет использовать стек.
 
В этом случае для проверки надо будет использовать стек.
</wikitex>
+
 
 
== Лексикографический порядок порядок правильных скобочных последовательностей ==
 
== Лексикографический порядок порядок правильных скобочных последовательностей ==
<wikitex>
+
 
Для того, чтобы определить лексикографический порядок для правильных скобочных последовательностей, надо установить порядок на алфавите, например так '$($' < '$)$'.
+
Для того, чтобы определить лексикографический порядок для правильных скобочных последовательностей, надо установить порядок на алфавите, например так <tex>(\ <\ )</tex>. Для последовательностей с разным типом скобок надо определять свой порядок в зависимости от числа скобок, причем любая открывающаяся скобка должна быть меньше закрывающейся, например <tex>(\ <\ [\ <\ )\ <\ ]</tex>.
Для последовательностей с разным типом скобок надо определять свой порядок в зависимости от числа скобок, причем любая открывающаяся скобка должна быть меньше закрывающейся, например "$($" < "$[$" < "$)$" < "$]$".
+
 
</wikitex>
+
===Примеры лексикографического порядка для <tex>n</tex> и <tex>k</tex>, где <tex>n</tex> {{---}} число открывающихся скобок, а <tex>k</tex> {{---}} число видов скобок===
===Примеры лексикографического порядка для $n$ и $k$, где $n$ {{---}} число открывающихся скобок, а $k$ {{---}} число видов скобок===
+
 
<wikitex>
+
{| class="wikitable"
{| border="1" cellpadding="3"
+
!colspan="2" style="padding:7px"| <tex>n = 3</tex>
  |$n = 3$||$k = 1$
+
  !colspan="3" style="padding:7px"| <tex>k = 1</tex>
 
  |-
 
  |-
  |$((()))$||$(()())$||$(())()$||$()(())$||$()()()$
+
  !style="padding:7px"|<tex>((()))</tex>
 +
!style="padding:7px"|<tex>(()())</tex>
 +
!style="padding:7px"|<tex>(())()</tex>
 +
!style="padding:7px"|<tex>()(())</tex>
 +
!style="padding:7px"|<tex>()()()</tex>
 
  |}
 
  |}
  
{| border="1" cellpadding="3"
+
{| class="wikitable" cellpadding="3"
  |$n = 2$||$k = 2$
+
  !colspan="2" style="padding:7px"|<tex>n = 2</tex>
 +
!colspan="2" style="padding:7px"|<tex>k = 2</tex>
 
  |-
 
  |-
  |$()[]$||$([])$||$[()]$||$[]()$
+
  !style="padding:7px"|<tex>()[]</tex>
 +
!style="padding:7px"|<tex>([])</tex>
 +
!style="padding:7px"|<tex>[()]</tex>
 +
!style="padding:7px"|<tex>[]()</tex>
 
  |}
 
  |}
  
 
Алгоритм генерации лексикографического порядка будет предложен ниже.
 
Алгоритм генерации лексикографического порядка будет предложен ниже.
</wikitex>
 
== Количество правильных скобочных последовательностей. Числа Каталана ==
 
<wikitex>
 
Количество правильных скобочных последовательностей со скобками одного типа совпадает с числами Каталана.
 
{{Определение
 
|id = def1
 
|definition =Числа Каталана {{---}} последовательность чисел, выражающих:
 
*количество не изоморфных упорядоченных бинарных деревьев с корнем и $n + 1$ листьями;
 
*количество способов соединения $2n$ точек на окружности $n$ не пересекающимися хордами;
 
*количество разбиений выпуклого $(n + 2)$-угольника на треугольники не пересекающимися диагоналями;
 
*количество способов полностью разделить скобками $n + 1$ множитель;
 
*количество корректных скобочных последовательностей, состоящих из $n$ открывающих и $n$ закрывающих скобок;}}
 
</wikitex>
 
===Рекурентная формула===
 
<wikitex>
 
$C_n = \sum_{i = 0}^{n - 1} C_i C_{n - 1 - i}$
 
  
Рекуррентную формулу легко вывести из задачи о правильных скобочных последовательностях.
+
== Алгоритмы генерации ==
  
Самой левой открывающей скобке $l$ соответствует определённая закрывающая скобка $r$, которая разбивает формулу на две части, каждая из которых в свою очередь является правильной скобочной последовательностью. Поэтому, если мы обозначим $i = r - l - 1$, то для любого фиксированного $r$ будет ровно $C_i C_{n-1-i}$ способов. Суммируя это по всем допустимым i, мы и получаем рекуррентную зависимость на $C_n$.
+
===Рекурсивный алгоритм получения лексикографического порядка===
</wikitex>
+
Пусть нам известно число <tex>n</tex>. Надо вывести все правильные скобочные последовательности в лексикографическом порядке с <tex>n</tex> открывающимися скобками:
===Аналитическая формула===
 
<wikitex>
 
$ C_n = \frac{1}{n+1} C_{2n}^{n} $
 
  
(здесь через $C_n^k$ обозначен, как обычно, биномиальный коэффициент).
+
Для запуска алгоритма необходимо сделать вызов gen(n, 0, 0, "").
 +
*''ans'' - строка, в которой мы считаем ответ
 +
*''counter_open'' - количество открывающих скобок в данный момент
 +
*''counter_open'' - количество закрывающих скобок в данный момент
 +
  '''function''' gen (n: '''int''', counter_open: '''int''', counter_close: '''int''', ans: '''string''')
 +
    '''if''' counter_open + counter_close == 2 <tex>\cdot</tex> n
 +
      print ans
 +
      return
 +
    '''if''' counter_open < n
 +
      gen(n, counter_open + 1, counter_close, ans + "(")
 +
    '''if''' counter_open > counter_close
 +
      gen(n, counter_open, counter_close + 1, ans + ")")
  
Эту формулу проще всего вывести из задачи о монотонных путях. Общее количество монотонных путей в решётке размером $n \times n$ равно $C_{2n}^{n}$. Теперь посчитаем количество монотонных путей, пересекающих диагональ. Рассмотрим какой-либо из таких путей, и найдём первое ребро, которое стоит выше диагонали. Отразим относительно диагонали весь путь, идущий после этого ребра. В результате получим монотонный путь в решётке $(n - 1) \times (n + 1)$. Но, с другой стороны, любой монотонный путь в решётке $(n - 1) \times (n + 1)$ обязательно пересекает диагональ, следовательно, он получен как раз таким способом из какого-либо (причём единственного) монотонного пути, пересекающего диагональ, в решётке $n \times n$. Монотонных путей в решётке $(n - 1) \times (n + 1)$ имеется $C_{2n}^{n-1}$. В результате получаем формулу:
+
===Генерация следующей скобочной последовательности===
 
 
$ C_n = C_{2n}^{n} - C_{2n}^{n-1} = \frac{1}{n+1} C_{2n}^{n}$
 
</wikitex>
 
  
== Алгоритмы генерации ==
+
Пусть нам известна строка <tex>s</tex>, представляющая собой правильную скобочную последовательность. Нам необходимо вывести следующую скобочную последовательность, а если ее нет, то вывести "No solution". Чтобы получить следующую скобочную последовательность надо найти последнюю открывающуюся скобку, которую можно заменить, заменить ее на закрывающуюся, а оставшиеся в конце скобки (если они есть) заменить на минимально возможную последовательность скобок:
<wikitex>
 
===Генерация следующей скобочной последовательности===
 
<wikitex>
 
Пусть нам известна строка $s$, представляющая собой правильную скобочную последовательность. Нам необходимо вывести следующую скобочную последовательность, а если ее нет, то {{---}} "No solution". Чтобы получить следующую скобочную последовательность надо найти последнюю открывающуюся скобку, которую можно заменить, заменить ее на закрывающуюся, а оставшиеся в конце скобки (если они есть) заменить на минимально возможную последовательность скобок:
 
  
   next(s)
+
   '''string''' next(s: '''string''')
 
     counter_close = 0
 
     counter_close = 0
 
     counter_open = 0
 
     counter_open = 0
     for i = length(s) downto 1
+
     '''for''' i = length(s) '''downto''' 1
         if s[i] == '('
+
         '''if''' s[i] == '('
 
           counter_open++  
 
           counter_open++  
           if counter_close > counter_open
+
           '''if''' counter_close > counter_open
 
             break
 
             break
         else  
+
         '''else'''
 
           counter_close++
 
           counter_close++
 
     delete(s, length(s) - counter_open - counter_close + 1, counter_close + l)
 
     delete(s, length(s) - counter_open - counter_close + 1, counter_close + l)
     if s == ""
+
     '''if''' s == ""
 
       return "No Solution"
 
       return "No Solution"
     else
+
     '''else'''
 
     s = s +')'
 
     s = s +')'
     for j = 1 to counter_open
+
     '''for''' j = 1 '''to''' counter_open
 
       s = s + '('
 
       s = s + '('
     for j = 1 to counter_close - 1
+
     '''for''' j = 1 '''to''' counter_close - 1
 
       s = s + ')'
 
       s = s + ')'
 
     return s;
 
     return s;
  
Если эта функция после выполнения выводит $true$, тогда надо напечатать полученную строку $s$, если $false$, то следует вывести "No solution".
+
Если эта функция после выполнения выводит <tex>true</tex>, тогда надо напечатать полученную строку <tex>s</tex>, если <tex>false</tex>, то следует вывести "No solution".
</wikitex>
+
 
 
===Получение лексикографического порядка===
 
===Получение лексикографического порядка===
<wikitex>
 
Пусть нам известно число $n$. Надо вывести все правильные скобочные последовательности в лексикографическом порядке с $n$ открывающимися скобками:
 
  
   order(n)
+
Пусть нам известно число <tex>n</tex>. Надо вывести все правильные скобочные последовательности в лексикографическом порядке с <tex>n</tex> открывающимися скобками:
 +
 
 +
   '''function''' order(n: '''int''')
 
     s = ""
 
     s = ""
     if n == 0
+
     '''for''' j = 1 '''to''' n
      result(s) 
+
      s = s + '('
    else
+
    '''for''' j = 1 '''to''' n
      for j = 1 to n
+
      s = s + ')'
        s = s + '('
+
    print s
      for j = 1 to n
+
    t = next(s)
        s = s + ')'
+
    '''while''' t != false
      result(s)
+
      print s
 
       t = next(s)
 
       t = next(s)
      while t != false
 
        result(s)
 
        t = next(s)
 
 
     return
 
     return
  
Также с помощью этого алгоритма можно получить скобочную последовательность по номеру и номер по скобочной последовательности, добавив сравнение с нужной последовательностью и счетчик. Но это далеко не самый оптимальный алгоритм для подобного типа задач и он не будет нормально работать для больших $n$.
+
Также с помощью этого алгоритма можно получить скобочную последовательность по номеру и номер по скобочной последовательности, добавив сравнение с нужной последовательностью и счетчик. Но это далеко не самый оптимальный алгоритм для подобного типа задач и он не будет нормально работать для больших <tex>n</tex>.
</wikitex>
+
 
 
===Получение номера последовательности===
 
===Получение номера последовательности===
<wikitex>
 
Пусть $n$ — количество пар скобок в последовательности. Требуется по заданной правильной скобочной последовательности найти её номер в списке лексикографически упорядоченных правильных скобочных последовательностей.
 
  
Научимся считать вспомогательную динамику $d[i][j]$, где $i$ — длина скобочной последовательности (она "полуправильная": всякой закрывающей скобке соответствует парная открывающая, но не все открытые скобки закрыты), $j$ — баланс (т.е. разность между количеством открывающих и закрывающих скобок), $d[i][j]$ — количество таких последовательностей. При подсчёте этой динамики мы считаем, что скобки бывают только одного типа.
+
Пусть <tex>n</tex> — количество пар скобок в последовательности. Требуется по заданной правильной скобочной последовательности найти её номер в списке лексикографически упорядоченных правильных скобочных последовательностей.
 +
 
 +
Научимся считать вспомогательную динамику <tex>d[i][j]</tex>, где <tex>i</tex> — длина скобочной последовательности (она "полуправильная": всякой закрывающей скобке соответствует парная открывающая, но не все открытые скобки закрыты), <tex>j</tex> — баланс (т.е. разность между количеством открывающих и закрывающих скобок), <tex>d[i][j]</tex> — количество таких последовательностей. При подсчёте этой динамики мы считаем, что скобки бывают только одного типа.
  
Считать эту динамику можно следующим образом. Пусть $d[i][j]$ — величина, которую мы хотим посчитать. Если $i = 0$, то ответ понятен сразу: $d[0][0] = 1$, все остальные $d[0][j] = 0$. Пусть теперь $i > 0$, тогда переберём, чему мог быть равен последний символ этой последовательности. Если он был равен '(', то до этого символа мы находились в состоянии $(i-1,j-1)$. Если он был равен ')', то предыдущим было состояние $(i-1,j+1)$. Таким образом, получаем формулу:
+
Считать эту динамику можно следующим образом. Пусть <tex>d[i][j]</tex> — величина, которую мы хотим посчитать. Если <tex>i = 0</tex>, то ответ понятен сразу: <tex>d[0][0] = 1</tex>, все остальные <tex>d[0][j] = 0</tex>. Пусть теперь <tex>i > 0</tex>, тогда переберём, чему мог быть равен последний символ этой последовательности. Если он был равен '(', то до этого символа мы находились в состоянии <tex>(i-1,j-1)</tex>. Если он был равен ')', то предыдущим было состояние <tex>(i-1,j+1)</tex>. Таким образом, получаем формулу:
  
$d[i][j] = d[i-1][j-1] + d[i-1][j+1]$
+
<tex>d[i][j] = d[i-1][j-1] + d[i-1][j+1]</tex>
  
(считается, что все значения $d[i][j]$ при отрицательном $j$ равны нулю). Таким образом, эту динамику мы можем посчитать за $O(n^2)$.
+
(считается, что все значения <tex>d[i][j]</tex> при отрицательном <tex>j</tex> равны нулю). Таким образом, эту динамику мы можем посчитать за <tex>O(n^2)</tex>.
  
 
Перейдём теперь к решению самой задачи. Сначала пусть допустимы только скобки одного типа:
 
Перейдём теперь к решению самой задачи. Сначала пусть допустимы только скобки одного типа:
  
   getNumber(s)
+
   '''int''' getNumber(s: '''string''')
 
     num = 0
 
     num = 0
 
     depth = 0
 
     depth = 0
     for i = 0 to 2 * n - 1
+
     '''for''' i = 0 '''to''' 2 * n - 1
       if s[i] == '('
+
       '''if''' s[i] == '('
 
         depth++
 
         depth++
       else
+
       '''else'''
 
         num += d[2 * n - i - 1][depth + 1]
 
         num += d[2 * n - i - 1][depth + 1]
 
         depth--
 
         depth--
 
     return num
 
     return num
  
Пусть теперь разрешены скобки $k$ типов. Тогда при рассмотрении текущего символа $s[i]$ до пересчёта $\rm depth$ мы должны перебирать все скобки, которые меньше текущего символа в установленном ранее порядке, пробовать ставить эту скобку в текущую позицию (получая тем самым новый баланс $\rm ndepth = \rm depth \pm 1$), и прибавлять к ответу количество соответствующих "хвостов" - завершений (которые имеют длину $2n - i - 1$, баланс $\rm ndepth$ и $k$ типов скобок). Утверждается, что формула для этого количества имеет вид:
+
Пусть теперь разрешены скобки <tex>k</tex> типов. Тогда при рассмотрении текущего символа <tex>s[i]</tex> до пересчёта <tex>\rm depth</tex> мы должны перебирать все скобки, которые меньше текущего символа в установленном ранее порядке, пробовать ставить эту скобку в текущую позицию (получая тем самым новый баланс <tex>\rm ndepth = \rm depth \pm 1</tex>), и прибавлять к ответу количество соответствующих "хвостов" - завершений (которые имеют длину <tex>2n - i - 1</tex>, баланс <tex>\rm ndepth</tex> и <tex>k</tex> типов скобок). Утверждается, что формула для этого количества имеет вид:
 +
 
 +
<tex>d[2n - i - 1][ndepth] \cdot k^{(2n - i - 1 - ndepth) / 2}</tex>
  
$d[2n - i - 1][ndepth] \cdot k^{(2n - i - 1 - ndepth) / 2}$
+
Эта формула выводится из следующих соображений. Сначала мы "забываем" про то, что скобки бывают нескольких типов, и просто берём ответ из <tex>d[2n - i - 1][{\rm ndepth}]</tex>. Теперь посчитаем, как изменится ответ из-за наличия <tex>k</tex> типов скобок. У нас имеется <tex>2n - i - 1</tex> неопределённых позиций, из которых <tex>\rm ndepth</tex> являются скобками, закрывающими какие-то из открытых ранее, — значит, тип таких скобок мы варьировать не можем. А вот все остальные скобки (а их будет <tex>(2n - i - 1 - {\rm ndepth}) / 2</tex> пар) могут быть любого из <tex>k</tex> типов, поэтому ответ умножается на эту степень числа <tex>k</tex>.
  
Эта формула выводится из следующих соображений. Сначала мы "забываем" про то, что скобки бывают нескольких типов, и просто берём ответ из $d[2n - i - 1][{\rm ndepth}]$. Теперь посчитаем, как изменится ответ из-за наличия $k$ типов скобок. У нас имеется $2n - i - 1$ неопределённых позиций, из которых $\rm ndepth$ являются скобками, закрывающими какие-то из открытых ранее, — значит, тип таких скобок мы варьировать не можем. А вот все остальные скобки (а их будет $(2n - i - 1 - {\rm ndepth}) / 2$ пар) могут быть любого из $k$ типов, поэтому ответ умножается на эту степень числа $k$.
+
Сложность данного алгоритма <tex>O(n^2 + n \cdot k)</tex>.
  
Сложность данного алгоритма $O(n^2 + n \cdot k)$.
 
</wikitex>
 
 
===Получение k-й последовательности===
 
===Получение k-й последовательности===
<wikitex>
 
Пусть $n$ — количество пар скобок в последовательности. В данной задаче по заданному $k$ требуется найти $k$-ую правильную скобочную последовательность в списке лексикографически упорядоченных последовательностей.
 
  
Как и в предыдущем разделе, посчитаем динамику $d[i][j]$ — количество правильных скобочных последовательностей длины $i$ с балансом $j$.
+
Пусть <tex>n</tex> — количество пар скобок в последовательности. В данной задаче по заданному <tex>k</tex> требуется найти <tex>k</tex>-ую правильную скобочную последовательность в списке лексикографически упорядоченных последовательностей.
 +
 
 +
Как и в предыдущем разделе, посчитаем динамику <tex>d[i][j]</tex> — количество правильных скобочных последовательностей длины <tex>i</tex> с балансом <tex>j</tex>.
  
 
Пусть сначала допустимы только скобки одного типа:
 
Пусть сначала допустимы только скобки одного типа:
  
   getSequence(n, k)
+
   '''string''' getSequence(n: '''int''', k: '''int''')
 
     depth = 0
 
     depth = 0
 
     s = ""
 
     s = ""
     for i = 0 to 2 * n - 1
+
     '''for''' i = 0 '''to''' 2 * n - 1
       if d[2 * n - (i + 1)][depth + 1] >= k
+
       '''if''' d[2 * n - (i + 1)][depth + 1] \geqslant k
 
         s += '('
 
         s += '('
 
         depth++
 
         depth++
       else
+
       '''else'''
 
         k -= d[2 * n - (i + 1)][depth + 1]
 
         k -= d[2 * n - (i + 1)][depth + 1]
 
         s += ')'
 
         s += ')'
Строка 205: Строка 195:
 
     return s
 
     return s
  
Пусть теперь разрешён не один, а $k$ типов скобок. Тогда алгоритм решения будет отличаться от предыдущего случая только тем, что мы должны домножать значение $d[i + 1][\rm ndepth]$ на величину $k^{(2n - i - 1 - \rm ndepth) / 2}$, чтобы учесть, что в этом остатке могли быть скобки различных типов, а парных скобок в этом остатке будет только $2n - i - 1 - \rm ndepth$, поскольку $\rm ndepth$ скобок являются закрывающими для открывающих скобок, находящихся вне этого остатка (а потому их типы мы варьировать не можем).
+
Пусть теперь разрешён не один, а <tex>k</tex> типов скобок. Тогда алгоритм решения будет отличаться от предыдущего случая только тем, что мы должны домножать значение <tex>d[i + 1][\rm ndepth]</tex> на величину <tex>k^{(2n - i - 1 - \rm ndepth) / 2}</tex>, чтобы учесть, что в этом остатке могли быть скобки различных типов, а парных скобок в этом остатке будет только <tex>2n - i - 1 - \rm ndepth</tex>, поскольку <tex>\rm ndepth</tex> скобок являются закрывающими для открывающих скобок, находящихся вне этого остатка (а потому их типы мы варьировать не можем).
 +
 
 +
Сложность данного алгоритма <tex>O(n^2 + n \cdot k)</tex>.
 +
 
 +
== Количество правильных скобочных последовательностей. Числа Каталана ==
 +
 
 +
Количество правильных скобочных последовательностей со скобками одного типа совпадает с числами Каталана.
 +
{{Определение
 +
|id = def1
 +
|definition ='''Числа Каталана''' (англ. ''Catalan number'') {{---}} последовательность чисел, выражающих:
 +
*количество не изоморфных упорядоченных бинарных деревьев с корнем и <tex>n + 1</tex> листьями;
 +
*количество способов соединения <tex>2n</tex> точек на окружности <tex>n</tex> не пересекающимися хордами;
 +
*количество разбиений выпуклого <tex>(n + 2)</tex>-угольника на треугольники не пересекающимися диагоналями;
 +
*количество способов полностью разделить скобками <tex>n + 1</tex> множитель;
 +
*количество корректных скобочных последовательностей, состоящих из <tex>n</tex> открывающих и <tex>n</tex> закрывающих скобок;}}
 +
===Рекурентная формула===
 +
 
 +
<tex>C_n = \sum_{i = 0}^{n - 1} C_i C_{n - 1 - i}</tex>
 +
 
 +
Рекуррентную формулу легко вывести из задачи о правильных скобочных последовательностях.
 +
 
 +
Самой левой открывающей скобке <tex>l</tex> соответствует определённая закрывающая скобка <tex>r</tex>, которая разбивает формулу на две части, каждая из которых в свою очередь является правильной скобочной последовательностью. Поэтому, если мы обозначим <tex>i = r - l - 1</tex>, то для любого фиксированного <tex>r</tex> будет ровно <tex>C_i C_{n-1-i}</tex> способов. Суммируя это по всем допустимым i, мы и получаем рекуррентную зависимость на <tex>C_n</tex>.
 +
 
 +
===Аналитическая формула===
 +
 
 +
<tex> C_n = \frac{1}{n+1} C_{2n}^{n} </tex>
 +
 
 +
(здесь через <tex>C_n^k</tex> обозначен, как обычно, биномиальный коэффициент).
 +
 
 +
Эту формулу проще всего вывести из задачи о монотонных путях. Общее количество монотонных путей в решётке размером <tex>n \times n</tex> равно <tex>C_{2n}^{n}</tex>. Теперь посчитаем количество монотонных путей, пересекающих диагональ. Рассмотрим какой-либо из таких путей, и найдём первое ребро, которое стоит выше диагонали. Отразим относительно диагонали весь путь, идущий после этого ребра. В результате получим монотонный путь в решётке <tex>(n - 1) \times (n + 1)</tex>. Но, с другой стороны, любой монотонный путь в решётке <tex>(n - 1) \times (n + 1)</tex> обязательно пересекает диагональ, следовательно, он получен как раз таким способом из какого-либо (причём единственного) монотонного пути, пересекающего диагональ, в решётке <tex>n \times n</tex>. Монотонных путей в решётке <tex>(n - 1) \times (n + 1)</tex> имеется <tex>C_{2n}^{n-1}</tex>. В результате получаем формулу:
  
Сложность данного алгоритма $O(n^2 + n \cdot k)$.
+
<tex> C_n = C_{2n}^{n} - C_{2n}^{n-1} = \frac{1}{n+1} C_{2n}^{n}</tex>
</wikitex>
+
 
 +
== См. также ==
 +
*[[Комбинаторные объекты]]
 +
*[[Лексикографический порядок]]
 +
*[[Генерация комбинаторных объектов в лексикографическом порядке]]
 +
*[[Получение номера по объекту]]
 +
*[[Получение объекта по номеру]]
 +
*[[Получение следующего объекта]]
  
 
== Источники ==
 
== Источники ==
<wikitex>
+
 
 
* [http://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8 Скобочные последовательности, Материал из Википедии — свободной энциклопедии]
 
* [http://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8 Скобочные последовательности, Материал из Википедии — свободной энциклопедии]
  
Строка 223: Строка 249:
  
 
[[Категория: Комбинаторика ]]
 
[[Категория: Комбинаторика ]]
</wikitex>
 

Версия 23:26, 22 ноября 2014

Определения

Определение:
Скобочная последовательность — класс комбинаторных объектов, представляющих собой последовательность скобочных символов.

Примеры скобочных последовательностей

  • [math](())))([/math]
  • [math])()()))()(()())[/math]
Определение:
Правильная скобочная последовательность — частный случай скобочной последовательности, определяющийся следующими образами:
  • "" (пустая строка) есть правильная скобочная последовательность;
  • пусть [math]S[/math] — правильная скобочная последовательность, тогда [math](S)[/math] есть правильная скобочная последовательность;
  • пусть [math]S1[/math], [math]S2[/math] — правильные скобочные последовательности, тогда [math]S1S2[/math] есть правильная скобочная последовательность;

Примеры правильных скобочный последовательностей

  • [math]((()()()()))[/math]
  • [math](())(()())[/math]

Алгоритм проверки правильности скобочной последовательности

Пусть нам дана скобочная последовательность, записанная в строку [math]s[/math]. Возьмем переменную [math]counter[/math], [math]counter = 0[/math]. Будем последовательно перебирать все символы этой строки. Если мы встречаем открывающуюся скобку, то увеличиваем [math]counter[/math] на [math]1[/math], закрывающую — уменьшаем на [math]1[/math]. Если на протяжении всего перебора [math]counter[/math] было неотрицательным и после завершения осталось нулем, то скобочная последовательность правильна.

Псевдокод

 boolean check(s: string)
   counter = 0
   for i = 1 to length(s)
     if s[i] == '('
       counter++
     else
       counter-- 
     if counter < 0
       return false 
   if counter == 0
     return true
   else
     return false

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

Примеры скобочных последовательностей с несколькими типами скобок

  • [math]()[()]\{()()[]\}[/math] — верно
  • [math][(]\{\})[/math] — неверно

В этом случае для проверки надо будет использовать стек.

Лексикографический порядок порядок правильных скобочных последовательностей

Для того, чтобы определить лексикографический порядок для правильных скобочных последовательностей, надо установить порядок на алфавите, например так [math](\ \lt \ )[/math]. Для последовательностей с разным типом скобок надо определять свой порядок в зависимости от числа скобок, причем любая открывающаяся скобка должна быть меньше закрывающейся, например [math](\ \lt \ [\ \lt \ )\ \lt \ ][/math].

Примеры лексикографического порядка для [math]n[/math] и [math]k[/math], где [math]n[/math] — число открывающихся скобок, а [math]k[/math] — число видов скобок

[math]n = 3[/math] [math]k = 1[/math]
[math]((()))[/math] [math](()())[/math] [math](())()[/math] [math]()(())[/math] [math]()()()[/math]
[math]n = 2[/math] [math]k = 2[/math]
[math]()[][/math] [math]([])[/math] [math][()][/math] [math][]()[/math]

Алгоритм генерации лексикографического порядка будет предложен ниже.

Алгоритмы генерации

Рекурсивный алгоритм получения лексикографического порядка

Пусть нам известно число [math]n[/math]. Надо вывести все правильные скобочные последовательности в лексикографическом порядке с [math]n[/math] открывающимися скобками:

Для запуска алгоритма необходимо сделать вызов gen(n, 0, 0, "").

  • ans - строка, в которой мы считаем ответ
  • counter_open - количество открывающих скобок в данный момент
  • counter_open - количество закрывающих скобок в данный момент
 function gen (n: int, counter_open: int, counter_close: int, ans: string)
   if counter_open + counter_close == 2 [math]\cdot[/math] n
     print ans
     return
   if counter_open < n
     gen(n, counter_open + 1, counter_close, ans + "(")
   if counter_open > counter_close
     gen(n, counter_open, counter_close + 1, ans + ")")

Генерация следующей скобочной последовательности

Пусть нам известна строка [math]s[/math], представляющая собой правильную скобочную последовательность. Нам необходимо вывести следующую скобочную последовательность, а если ее нет, то вывести "No solution". Чтобы получить следующую скобочную последовательность надо найти последнюю открывающуюся скобку, которую можно заменить, заменить ее на закрывающуюся, а оставшиеся в конце скобки (если они есть) заменить на минимально возможную последовательность скобок:

 string next(s: string)
   counter_close = 0
   counter_open = 0
   for i = length(s) downto 1
       if s[i] == '('
         counter_open++ 
         if counter_close > counter_open
           break
       else 
         counter_close++
   delete(s, length(s) - counter_open - counter_close + 1, counter_close + l)
   if s == ""
     return "No Solution"
   else
    s = s +')'
    for j = 1 to counter_open
      s = s + '('
    for j = 1 to counter_close - 1
      s = s + ')'
    return s;

Если эта функция после выполнения выводит [math]true[/math], тогда надо напечатать полученную строку [math]s[/math], если [math]false[/math], то следует вывести "No solution".

Получение лексикографического порядка

Пусть нам известно число [math]n[/math]. Надо вывести все правильные скобочные последовательности в лексикографическом порядке с [math]n[/math] открывающимися скобками:

 function order(n: int)
   s = ""
   for j = 1 to n
     s = s + '('
   for j = 1 to n
     s = s + ')'
   print s
   t = next(s)
   while t != false
     print s
     t = next(s)
   return

Также с помощью этого алгоритма можно получить скобочную последовательность по номеру и номер по скобочной последовательности, добавив сравнение с нужной последовательностью и счетчик. Но это далеко не самый оптимальный алгоритм для подобного типа задач и он не будет нормально работать для больших [math]n[/math].

Получение номера последовательности

Пусть [math]n[/math] — количество пар скобок в последовательности. Требуется по заданной правильной скобочной последовательности найти её номер в списке лексикографически упорядоченных правильных скобочных последовательностей.

Научимся считать вспомогательную динамику [math]d[i][j][/math], где [math]i[/math] — длина скобочной последовательности (она "полуправильная": всякой закрывающей скобке соответствует парная открывающая, но не все открытые скобки закрыты), [math]j[/math] — баланс (т.е. разность между количеством открывающих и закрывающих скобок), [math]d[i][j][/math] — количество таких последовательностей. При подсчёте этой динамики мы считаем, что скобки бывают только одного типа.

Считать эту динамику можно следующим образом. Пусть [math]d[i][j][/math] — величина, которую мы хотим посчитать. Если [math]i = 0[/math], то ответ понятен сразу: [math]d[0][0] = 1[/math], все остальные [math]d[0][j] = 0[/math]. Пусть теперь [math]i \gt 0[/math], тогда переберём, чему мог быть равен последний символ этой последовательности. Если он был равен '(', то до этого символа мы находились в состоянии [math](i-1,j-1)[/math]. Если он был равен ')', то предыдущим было состояние [math](i-1,j+1)[/math]. Таким образом, получаем формулу:

[math]d[i][j] = d[i-1][j-1] + d[i-1][j+1][/math]

(считается, что все значения [math]d[i][j][/math] при отрицательном [math]j[/math] равны нулю). Таким образом, эту динамику мы можем посчитать за [math]O(n^2)[/math].

Перейдём теперь к решению самой задачи. Сначала пусть допустимы только скобки одного типа:

 int getNumber(s: string)
    num = 0
    depth = 0
    for i = 0 to 2 * n - 1
      if s[i] == '('
        depth++
      else
        num += d[2 * n - i - 1][depth + 1]
        depth--
    return num

Пусть теперь разрешены скобки [math]k[/math] типов. Тогда при рассмотрении текущего символа [math]s[i][/math] до пересчёта [math]\rm depth[/math] мы должны перебирать все скобки, которые меньше текущего символа в установленном ранее порядке, пробовать ставить эту скобку в текущую позицию (получая тем самым новый баланс [math]\rm ndepth = \rm depth \pm 1[/math]), и прибавлять к ответу количество соответствующих "хвостов" - завершений (которые имеют длину [math]2n - i - 1[/math], баланс [math]\rm ndepth[/math] и [math]k[/math] типов скобок). Утверждается, что формула для этого количества имеет вид:

[math]d[2n - i - 1][ndepth] \cdot k^{(2n - i - 1 - ndepth) / 2}[/math]

Эта формула выводится из следующих соображений. Сначала мы "забываем" про то, что скобки бывают нескольких типов, и просто берём ответ из [math]d[2n - i - 1][{\rm ndepth}][/math]. Теперь посчитаем, как изменится ответ из-за наличия [math]k[/math] типов скобок. У нас имеется [math]2n - i - 1[/math] неопределённых позиций, из которых [math]\rm ndepth[/math] являются скобками, закрывающими какие-то из открытых ранее, — значит, тип таких скобок мы варьировать не можем. А вот все остальные скобки (а их будет [math](2n - i - 1 - {\rm ndepth}) / 2[/math] пар) могут быть любого из [math]k[/math] типов, поэтому ответ умножается на эту степень числа [math]k[/math].

Сложность данного алгоритма [math]O(n^2 + n \cdot k)[/math].

Получение k-й последовательности

Пусть [math]n[/math] — количество пар скобок в последовательности. В данной задаче по заданному [math]k[/math] требуется найти [math]k[/math]-ую правильную скобочную последовательность в списке лексикографически упорядоченных последовательностей.

Как и в предыдущем разделе, посчитаем динамику [math]d[i][j][/math] — количество правильных скобочных последовательностей длины [math]i[/math] с балансом [math]j[/math].

Пусть сначала допустимы только скобки одного типа:

 string getSequence(n: int, k: int)
   depth = 0
   s = ""
   for i = 0 to 2 * n - 1
     if d[2 * n - (i + 1)][depth + 1] \geqslant k
       s += '('
       depth++
     else
       k -= d[2 * n - (i + 1)][depth + 1]
       s += ')'
       depth--
   return s

Пусть теперь разрешён не один, а [math]k[/math] типов скобок. Тогда алгоритм решения будет отличаться от предыдущего случая только тем, что мы должны домножать значение [math]d[i + 1][\rm ndepth][/math] на величину [math]k^{(2n - i - 1 - \rm ndepth) / 2}[/math], чтобы учесть, что в этом остатке могли быть скобки различных типов, а парных скобок в этом остатке будет только [math]2n - i - 1 - \rm ndepth[/math], поскольку [math]\rm ndepth[/math] скобок являются закрывающими для открывающих скобок, находящихся вне этого остатка (а потому их типы мы варьировать не можем).

Сложность данного алгоритма [math]O(n^2 + n \cdot k)[/math].

Количество правильных скобочных последовательностей. Числа Каталана

Количество правильных скобочных последовательностей со скобками одного типа совпадает с числами Каталана.

Определение:
Числа Каталана (англ. Catalan number) — последовательность чисел, выражающих:
  • количество не изоморфных упорядоченных бинарных деревьев с корнем и [math]n + 1[/math] листьями;
  • количество способов соединения [math]2n[/math] точек на окружности [math]n[/math] не пересекающимися хордами;
  • количество разбиений выпуклого [math](n + 2)[/math]-угольника на треугольники не пересекающимися диагоналями;
  • количество способов полностью разделить скобками [math]n + 1[/math] множитель;
  • количество корректных скобочных последовательностей, состоящих из [math]n[/math] открывающих и [math]n[/math] закрывающих скобок;

Рекурентная формула

[math]C_n = \sum_{i = 0}^{n - 1} C_i C_{n - 1 - i}[/math]

Рекуррентную формулу легко вывести из задачи о правильных скобочных последовательностях.

Самой левой открывающей скобке [math]l[/math] соответствует определённая закрывающая скобка [math]r[/math], которая разбивает формулу на две части, каждая из которых в свою очередь является правильной скобочной последовательностью. Поэтому, если мы обозначим [math]i = r - l - 1[/math], то для любого фиксированного [math]r[/math] будет ровно [math]C_i C_{n-1-i}[/math] способов. Суммируя это по всем допустимым i, мы и получаем рекуррентную зависимость на [math]C_n[/math].

Аналитическая формула

[math] C_n = \frac{1}{n+1} C_{2n}^{n} [/math]

(здесь через [math]C_n^k[/math] обозначен, как обычно, биномиальный коэффициент).

Эту формулу проще всего вывести из задачи о монотонных путях. Общее количество монотонных путей в решётке размером [math]n \times n[/math] равно [math]C_{2n}^{n}[/math]. Теперь посчитаем количество монотонных путей, пересекающих диагональ. Рассмотрим какой-либо из таких путей, и найдём первое ребро, которое стоит выше диагонали. Отразим относительно диагонали весь путь, идущий после этого ребра. В результате получим монотонный путь в решётке [math](n - 1) \times (n + 1)[/math]. Но, с другой стороны, любой монотонный путь в решётке [math](n - 1) \times (n + 1)[/math] обязательно пересекает диагональ, следовательно, он получен как раз таким способом из какого-либо (причём единственного) монотонного пути, пересекающего диагональ, в решётке [math]n \times n[/math]. Монотонных путей в решётке [math](n - 1) \times (n + 1)[/math] имеется [math]C_{2n}^{n-1}[/math]. В результате получаем формулу:

[math] C_n = C_{2n}^{n} - C_{2n}^{n-1} = \frac{1}{n+1} C_{2n}^{n}[/math]

См. также

Источники