Skip to Content

Secuencias de paréntesis balanceadas

Una secuencia de paréntesis balanceada (balanced bracket sequence) es un string que consiste solo de paréntesis, tal que esta secuencia, al insertarle ciertos números y operaciones matemáticas, da una expresión matemática válida. Formalmente se puede definir una secuencia de paréntesis balanceada con:

  • ee (el string vacío) es una secuencia de paréntesis balanceada.
  • si ss es una secuencia de paréntesis balanceada, entonces también lo es (s)(s).
  • si ss y tt son secuencias de paréntesis balanceadas, entonces también lo es sts t.

Por ejemplo (())()(())() es una secuencia de paréntesis balanceada, pero ())(())( no lo es.

Por supuesto también se pueden definir otras secuencias de paréntesis con varios tipos de paréntesis de manera similar.

En este artículo discutimos algunos problemas clásicos que involucran secuencias de paréntesis balanceadas (por simplicidad solo las llamaremos secuencias): validación, número de secuencias, hallar la siguiente secuencia en orden lexicográfico, generar todas las secuencias de un cierto tamaño, hallar el índice de una secuencia, y generar la kk-ésima secuencia. También discutiremos dos variantes de los problemas, la versión más simple cuando solo se permite un tipo de paréntesis, y el caso más difícil cuando hay varios tipos.

Validación de balance

Queremos comprobar si un string dado está balanceado o no.

Primero supongamos que hay solo un tipo de paréntesis. Para este caso existe un algoritmo muy sencillo. Sea depth\text{depth} el número actual de paréntesis abiertos. Inicialmente depth=0\text{depth} = 0. Iteramos sobre todos los caracteres del string; si el carácter de paréntesis actual es un paréntesis de apertura, entonces incrementamos depth\text{depth}, en caso contrario lo decrementamos. Si en cualquier momento la variable depth\text{depth} se vuelve negativa, o al final es distinta de 00, entonces el string no es una secuencia balanceada. En caso contrario lo es.

Si hay varios tipos de paréntesis involucrados, entonces el algoritmo necesita cambiarse. En lugar de un contador depth\text{depth} creamos una pila, en la que guardaremos todos los paréntesis de apertura que encontremos. Si el carácter de paréntesis actual es de apertura, lo ponemos en la pila. Si es de cierre, entonces comprobamos si la pila no está vacía, y si el elemento tope de la pila es del mismo tipo que el paréntesis de cierre actual. Si se cumplen ambas condiciones, entonces quitamos el paréntesis de apertura de la pila. Si en cualquier momento una de las condiciones no se cumple, o al final la pila no está vacía, entonces el string no está balanceado. En caso contrario lo está.

Número de secuencias balanceadas

Fórmula

El número de secuencias de paréntesis balanceadas con solo un tipo de paréntesis se puede calcular usando los números de Catalan. El número de secuencias de paréntesis balanceadas de longitud 2n2n (nn pares de paréntesis) es:

1n+1(2nn)\frac{1}{n+1} \binom{2n}{n}

Si permitimos kk tipos de paréntesis, entonces cada par puede ser de cualquiera de los kk tipos (independientemente de los demás), así que el número de secuencias de paréntesis balanceadas es:

1n+1(2nn)kn\frac{1}{n+1} \binom{2n}{n} k^n

Programación dinámica

Por otro lado estos números se pueden computar usando programación dinámica. Sea d[n]d[n] el número de secuencias de paréntesis regulares con nn pares de paréntesis. Nótese que en la primera posición siempre hay un paréntesis de apertura. Y en algún lugar más adelante está el paréntesis de cierre correspondiente del par. Está claro que dentro de este par hay una secuencia de paréntesis balanceada, y de manera similar después de este par hay una secuencia de paréntesis balanceada. Así que para computar d[n]d[n], veremos cuántas secuencias balanceadas de ii pares de paréntesis hay dentro de este primer par de paréntesis, y cuántas secuencias balanceadas con n1in-1-i pares hay después de este par. En consecuencia la fórmula tiene la forma:

d[n]=i=0n1d[i]d[n1i]d[n] = \sum_{i=0}^{n-1} d[i] \cdot d[n-1-i]

El valor inicial de esta recurrencia es d[0]=1d[0] = 1.

Hallar la siguiente secuencia balanceada en orden lexicográfico

Aquí solo consideramos el caso con un tipo de paréntesis válido.

Dada una secuencia balanceada, tenemos que hallar la siguiente secuencia balanceada (en orden lexicográfico).

Debería ser obvio que tenemos que hallar el paréntesis de apertura más a la derecha, que podemos reemplazar por un paréntesis de cierre sin violar la condición de que hay más paréntesis de cierre que de apertura hasta esta posición. Después de reemplazar esta posición, podemos llenar la parte restante del string con la lexicográficamente mínima: es decir, primero con tantos paréntesis de apertura como sea posible, y luego llenar las posiciones restantes con paréntesis de cierre. En otras palabras, intentamos dejar un prefijo tan largo como sea posible sin cambios, y el sufijo se reemplaza por el lexicográficamente mínimo.

Para hallar esta posición, podemos iterar sobre los caracteres de derecha a izquierda, y mantener el balance depth\text{depth} de paréntesis de apertura y de cierre. Cuando encontramos un paréntesis de apertura, decrementaremos depth\text{depth}, y cuando encontramos un paréntesis de cierre, lo aumentamos. Si en algún punto encontramos un paréntesis de apertura, y el balance después de procesar este símbolo es positivo, entonces hemos hallado la posición más a la derecha que podemos cambiar. Cambiamos el símbolo, computamos el número de paréntesis de apertura y de cierre que tenemos que añadir al lado derecho, y los disponemos de la manera lexicográficamente mínima.

Si no hallamos una posición adecuada, entonces esta secuencia ya es la máxima posible, y no hay respuesta.

bool next_balanced_sequence(string & s) { int n = s.size(); int depth = 0; for (int i = n - 1; i >= 0; i--) { if (s[i] == '(') depth--; else depth++; if (s[i] == '(' && depth > 0) { depth--; int open = (n - i - 1 - depth) / 2; int close = n - i - 1 - open; string next = s.substr(0, i) + ')' + string(open, '(') + string(close, ')'); s.swap(next); return true; } } return false; }

Esta función computa en tiempo O(n)O(n) la siguiente secuencia de paréntesis balanceada, y devuelve false si no hay una siguiente.

Hallar todas las secuencias balanceadas

A veces se requiere hallar y emitir todas las secuencias de paréntesis balanceadas de una longitud específica nn.

Para generarlas, podemos empezar con la secuencia lexicográficamente más pequeña (((())))((\dots(())\dots)), y luego continuar hallando las siguientes secuencias en orden lexicográfico con el algoritmo descrito en la sección anterior.

Sin embargo, si la longitud de la secuencia no es muy grande (p. ej. nn menor que 1212), entonces también podemos generar todas las permutaciones convenientemente con la función de la STL de C++ next_permutation, y comprobar cada una por balanceo.

También se pueden generar usando las ideas que usamos para contar todas las secuencias con programación dinámica. Discutiremos las ideas en las siguientes dos secciones.

Índice de una secuencia

Dada una secuencia de paréntesis balanceada con nn pares de paréntesis. Tenemos que hallar su índice en la lista ordenada lexicográficamente de todas las secuencias balanceadas con nn pares de paréntesis.

Definamos un arreglo auxiliar d[i][j]d[i][j], donde ii es la longitud de la secuencia de paréntesis (semibalanceada: cada paréntesis de cierre tiene un paréntesis de apertura correspondiente, pero no todo paréntesis de apertura tiene necesariamente un paréntesis de cierre correspondiente), y jj es el balance actual (diferencia entre paréntesis de apertura y de cierre). d[i][j]d[i][j] es el número de tales secuencias que encajan en los parámetros. Calcularemos estos números con solo un tipo de paréntesis.

Para el valor inicial i=0i = 0 la respuesta es obvia: d[0][0]=1d[0][0] = 1, y d[0][j]=0d[0][j] = 0 para j>0j > 0. Ahora sea i>0i > 0, y miremos el último carácter de la secuencia. Si el último carácter fue un paréntesis de apertura ((, entonces el estado anterior era (i1,j1)(i-1, j-1); si fue un paréntesis de cierre )), entonces el estado anterior era (i1,j+1)(i-1, j+1). Así obtenemos la fórmula de recurrencia:

d[i][j]=d[i1][j1]+d[i1][j+1]d[i][j] = d[i-1][j-1] + d[i-1][j+1]

d[i][j]=0d[i][j] = 0 vale obviamente para jj negativo. Así podemos computar este arreglo en O(n2)O(n^2).

Ahora generemos el índice de una secuencia dada.

Primero sea que hay solo un tipo de paréntesis. Usaremos el contador depth\text{depth} que nos dice cuán anidados estamos actualmente, e iteraremos sobre los caracteres de la secuencia. Si el carácter actual s[i]s[i] es igual a ((, entonces incrementamos depth\text{depth}. Si el carácter actual s[i]s[i] es igual a )), entonces debemos sumar d[2ni1][depth+1]d[2n-i-1][\text{depth}+1] a la respuesta, teniendo en cuenta todos los finales posibles que empiezan con un (( (que son secuencias lexicográficamente menores), y luego decrementar depth\text{depth}.

Ahora sea que hay kk tipos distintos de paréntesis.

Así, cuando miramos el carácter actual s[i]s[i] antes de recomputar depth\text{depth}, tenemos que recorrer todos los tipos de paréntesis que son menores que el carácter actual, e intentar colocar este paréntesis en la posición actual (obteniendo un nuevo balance ndepth=depth±1\text{ndepth} = \text{depth} \pm 1), y sumar el número de formas de terminar la secuencia (longitud 2ni12n-i-1, balance ndepthndepth) a la respuesta:

d[2ni1][ndepth]k2ni1ndepth2d[2n - i - 1][\text{ndepth}] \cdot k^{\frac{2n - i - 1 - ndepth}{2}}

Esta fórmula se puede derivar como sigue: Primero “olvidamos” que hay varios tipos de paréntesis, y simplemente tomamos la respuesta d[2ni1][ndepth]d[2n - i - 1][\text{ndepth}]. Ahora consideramos cómo cambiará la respuesta si tenemos kk tipos de paréntesis. Tenemos 2ni12n - i - 1 posiciones indefinidas, de las cuales ndepth\text{ndepth} ya están predeterminadas por los paréntesis de apertura. Pero todos los demás paréntesis ((2ni1ndepth)/2(2n - i - 1 - \text{ndepth})/2 pares) pueden ser de cualquier tipo, por lo tanto multiplicamos el número por tal potencia de kk.

Hallar la kk-ésima secuencia {data-toc-label=“Hallar la k-ésima secuencia”}

Sea nn el número de pares de paréntesis en la secuencia. Tenemos que hallar la kk-ésima secuencia balanceada en la lista ordenada lexicográficamente de todas las secuencias balanceadas para un kk dado.

Como en la sección anterior computamos el arreglo auxiliar d[i][j]d[i][j], el número de secuencias de paréntesis semibalanceadas de longitud ii con balance jj.

Primero, empezamos con solo un tipo de paréntesis.

Iteraremos sobre los caracteres del string que queremos generar. Como en el problema anterior guardamos un contador depth\text{depth}, la profundidad de anidamiento actual. En cada posición, tenemos que decidir si colocar un paréntesis de apertura o de cierre. Para colocar un paréntesis de apertura, debe cumplirse d[2ni1][depth+1]kd[2n - i - 1][\text{depth}+1] \ge k. Si es así, incrementamos el contador depth\text{depth}, y pasamos al siguiente carácter. En caso contrario, decrementamos kk en d[2ni1][depth+1]d[2n - i - 1][\text{depth}+1], colocamos un paréntesis de cierre, y seguimos.

string kth_balanced(int n, int k) { vector<vector<int>> d(2*n+1, vector<int>(n+1, 0)); d[0][0] = 1; for (int i = 1; i <= 2*n; i++) { d[i][0] = d[i-1][1]; for (int j = 1; j < n; j++) d[i][j] = d[i-1][j-1] + d[i-1][j+1]; d[i][n] = d[i-1][n-1]; } string ans; int depth = 0; for (int i = 0; i < 2*n; i++) { if (depth + 1 <= n && d[2*n-i-1][depth+1] >= k) { ans += '('; depth++; } else { ans += ')'; if (depth + 1 <= n) k -= d[2*n-i-1][depth+1]; depth--; } } return ans; }

Ahora sea que hay kk tipos de paréntesis. La solución solo diferirá ligeramente en que tenemos que multiplicar el valor d[2ni1][ndepth]d[2n-i-1][\text{ndepth}] por k(2ni1ndepth)/2k^{(2n-i-1-\text{ndepth})/2} y tener en cuenta que puede haber distintos tipos de paréntesis para el siguiente carácter.

Aquí hay una implementación usando dos tipos de paréntesis: redondos y cuadrados:

string kth_balanced2(int n, int k) { vector<vector<int>> d(2*n+1, vector<int>(n+1, 0)); d[0][0] = 1; for (int i = 1; i <= 2*n; i++) { d[i][0] = d[i-1][1]; for (int j = 1; j < n; j++) d[i][j] = d[i-1][j-1] + d[i-1][j+1]; d[i][n] = d[i-1][n-1]; } string ans; int shift, depth = 0; stack<char> st; for (int i = 0; i < 2*n; i++) { // '(' shift = ((2*n-i-1-depth-1) / 2); if (shift >= 0 && depth + 1 <= n) { int cnt = d[2*n-i-1][depth+1] << shift; if (cnt >= k) { ans += '('; st.push('('); depth++; continue; } k -= cnt; } // ')' shift = ((2*n-i-1-depth+1) / 2); if (shift >= 0 && depth && st.top() == '(') { int cnt = d[2*n-i-1][depth-1] << shift; if (cnt >= k) { ans += ')'; st.pop(); depth--; continue; } k -= cnt; } // '[' shift = ((2*n-i-1-depth-1) / 2); if (shift >= 0 && depth + 1 <= n) { int cnt = d[2*n-i-1][depth+1] << shift; if (cnt >= k) { ans += '['; st.push('['); depth++; continue; } k -= cnt; } // ']' ans += ']'; st.pop(); depth--; } return ans; }