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:
- (el string vacío) es una secuencia de paréntesis balanceada.
- si es una secuencia de paréntesis balanceada, entonces también lo es .
- si y son secuencias de paréntesis balanceadas, entonces también lo es .
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 -é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 el número actual de paréntesis abiertos. Inicialmente . Iteramos sobre todos los caracteres del string; si el carácter de paréntesis actual es un paréntesis de apertura, entonces incrementamos , en caso contrario lo decrementamos. Si en cualquier momento la variable se vuelve negativa, o al final es distinta de , 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 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 ( pares de paréntesis) es:
Si permitimos tipos de paréntesis, entonces cada par puede ser de cualquiera de los tipos (independientemente de los demás), así que el número de secuencias de paréntesis balanceadas es:
Programación dinámica
Por otro lado estos números se pueden computar usando programación dinámica. Sea el número de secuencias de paréntesis regulares con 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 , veremos cuántas secuencias balanceadas de pares de paréntesis hay dentro de este primer par de paréntesis, y cuántas secuencias balanceadas con pares hay después de este par. En consecuencia la fórmula tiene la forma:
El valor inicial de esta recurrencia es .
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 de paréntesis de apertura y de cierre. Cuando encontramos un paréntesis de apertura, decrementaremos , 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 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 .
Para generarlas, podemos empezar con la secuencia lexicográficamente más pequeña , 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. menor que ), 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 pares de paréntesis. Tenemos que hallar su índice en la lista ordenada lexicográficamente de todas las secuencias balanceadas con pares de paréntesis.
Definamos un arreglo auxiliar , donde 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 es el balance actual (diferencia entre paréntesis de apertura y de cierre). 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 la respuesta es obvia: , y para . Ahora sea , 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 ; si fue un paréntesis de cierre , entonces el estado anterior era . Así obtenemos la fórmula de recurrencia:
vale obviamente para negativo. Así podemos computar este arreglo en .
Ahora generemos el índice de una secuencia dada.
Primero sea que hay solo un tipo de paréntesis. Usaremos el contador que nos dice cuán anidados estamos actualmente, e iteraremos sobre los caracteres de la secuencia. Si el carácter actual es igual a , entonces incrementamos . Si el carácter actual es igual a , entonces debemos sumar a la respuesta, teniendo en cuenta todos los finales posibles que empiezan con un (que son secuencias lexicográficamente menores), y luego decrementar .
Ahora sea que hay tipos distintos de paréntesis.
Así, cuando miramos el carácter actual antes de recomputar , 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 ), y sumar el número de formas de terminar la secuencia (longitud , balance ) a la respuesta:
Esta fórmula se puede derivar como sigue: Primero “olvidamos” que hay varios tipos de paréntesis, y simplemente tomamos la respuesta . Ahora consideramos cómo cambiará la respuesta si tenemos tipos de paréntesis. Tenemos posiciones indefinidas, de las cuales ya están predeterminadas por los paréntesis de apertura. Pero todos los demás paréntesis ( pares) pueden ser de cualquier tipo, por lo tanto multiplicamos el número por tal potencia de .
Hallar la -ésima secuencia {data-toc-label=“Hallar la k-ésima secuencia”}
Sea el número de pares de paréntesis en la secuencia. Tenemos que hallar la -ésima secuencia balanceada en la lista ordenada lexicográficamente de todas las secuencias balanceadas para un dado.
Como en la sección anterior computamos el arreglo auxiliar , el número de secuencias de paréntesis semibalanceadas de longitud con balance .
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 , 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 . Si es así, incrementamos el contador , y pasamos al siguiente carácter. En caso contrario, decrementamos en , 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 tipos de paréntesis. La solución solo diferirá ligeramente en que tenemos que multiplicar el valor por 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;
}