Como você pode encontrar todos os parênteses desequilibrados em uma string em tempo linear com memória constante?

11

Recebi o seguinte problema durante uma entrevista:

Dá uma string que contém uma mistura de parênteses (não colchetes ou chaves - somente parênteses) com outros caracteres alfanuméricos, identifica todas as parênteses que não têm parênteses correspondentes.

Por exemplo, na cadeia ") (ab))", os índices 0 e 5 contêm parênteses que não têm parênteses correspondentes.

Eu apresentei uma solução O (n) em funcionamento usando memória O (n), usando uma pilha e passando pela string depois de adicionar parens à pilha e removê-los da pilha sempre que encontrava um parêntese de fechamento e o topo da pilha continha um parêntese de abertura.

Posteriormente, o entrevistador observou que o problema poderia ser resolvido em tempo linear com memória constante (como em, nenhum uso adicional de memória além do que é absorvido pela entrada).

Perguntei como e ela disse algo sobre atravessar a corda uma vez da esquerda identificando todas as parênteses abertas, e depois uma segunda vez da direita identificando todas as parênteses próximas ... ou talvez fosse o contrário. Eu realmente não entendi e não queria pedir que ela me segurasse com ela.

Alguém pode esclarecer a solução que ela sugeriu?

temporary_user_name
fonte
1
Podemos precisar de alguns esclarecimentos sobre você primeiro. O primeiro parênteses ou o segundo parênteses em "(()" é considerado desequilibrado? O último parênteses ou o segundo a penúltimo parênteses em "())" é considerado desequilibrado? Ou é suficiente identificar qualquer conjunto de parênteses com menor cardinalidade, de forma que removê-los deixe os parênteses restantes equilibrados? Ou outra coisa? Ou isso faz parte da entrevista para que uma resposta possa apresentar qualquer especificação justificável?
John L.
Eu diria que não importa, depende de você. Remova qualquer conjunto que deixe o restante equilibrado.
temporary_user_name
5
Em seguida, remova-os todos; P
Veedrac
@Veedrac, é claro (como você sabe) o pôster esqueceu a palavra 'mínimo' em "Remover qualquer conjunto mínimo ...".
precisa saber é
Eu não "esqueci", por si só, mas deixei de fora porque não me parecia uma especificação importante, pois existe apenas um conjunto que pode ser removido para torná-lo equilibrado, além de "todos eles" que é claro que está derrotando o objetivo do exercício.
temporary_user_name

Respostas:

17

Como isso vem de um background de programação e não de um exercício teórico de ciência da computação, suponho que seja necessária uma memória O(1) para armazenar um índice na string. Na ciência da computação teórica, isso significaria usar o modelo de RAM; com máquinas de Turing você não poderia fazer isso e você precisa de Θ(registro(n)) de memória para armazenar um índice para uma cadeia de comprimento n .

Você pode manter o princípio básico do algoritmo usado. Você perdeu uma oportunidade de otimização de memória.

usando uma pilha e passando pela string, adicionando parênteses à pilha e removendo-os da pilha sempre que encontrava um parêntese de fechamento e o topo da pilha continha um parêntese de abertura

Então, o que essa pilha contém? Ele nunca vai conter ()(um parêntese de abertura seguido de um parêntese de fechamento), pois sempre que a )aparência você aparece, em (vez de pressionar a tecla ). Portanto, a pilha sempre tem a forma )…)(…(- um monte de parênteses de fechamento seguido por um monte de parênteses de abertura.

Você não precisa de uma pilha para representar isso. Lembre-se do número de parênteses de fechamento e do número de parênteses de abertura.

Se você processar a sequência da esquerda para a direita, usando esses dois contadores, o que você tem no final é o número de parênteses de fechamento incompatíveis e o número de parênteses de abertura incompatíveis.

Se você deseja relatar as posições dos parênteses incompatíveis no final, precisará lembrar a posição de cada parêntese. Isso exigiria Θ(n) memória no pior caso. Mas você não precisa esperar até o final para produzir a saída. Assim que você encontrar um parêntese de fechamento incompatível, você sabe que ele é incompatível, portanto, imprima-o agora. E você não usará o número de parênteses de fechamento incompatíveis para nada, portanto, mantenha um contador de parênteses de abertura incomparáveis.

Em resumo: processe a sequência da esquerda para a direita. Mantenha um contador de parênteses de abertura incomparáveis. Se você vir um parêntese de abertura, aumente o contador. Se você vir um parêntese de fechamento e o contador for diferente de zero, diminua o contador. Se você vir um parêntese de fechamento e o contador for zero, insira o índice atual como um parêntese de fechamento incompatível.

O valor final do contador é o número de parênteses de abertura incompatíveis, mas isso não indica a posição deles. Observe que o problema é simétrico. Para listar as posições dos parênteses de abertura incompatíveis, basta executar o algoritmo na direção oposta.

Exercício 1: escreva isso em uma notação formal (matemática, pseudocódigo ou sua linguagem de programação favorita).

Exercício 2: convença-se de que esse é o mesmo algoritmo do Apass.Jack , apenas explicado de maneira diferente.

Gilles 'SO- parar de ser mau'
fonte
Oh, muito bom Gilles, muito bem explicado. Eu entendo perfeitamente agora. Faz alguns anos que recebi uma resposta sua sobre uma de minhas perguntas.
temporary_user_name
"Se você deseja relatar as posições dos parênteses incompatíveis no final, precisará se lembrar da posição de cada parêntese." Nem tanto. Tempo linear não significa passe único. Você pode fazer um segundo passe para encontrar os colchetes no lado incompatível e marcá-los.
Mooing Duck
Para o último passo, você não precisa executá-lo ao contrário, basta marcar o último N "(" como incompatibilidade.
Mooing Duck
1
@MooingDuck Isso não funciona. Por exemplo (().
orlp
Embora eu realmente goste desta resposta, algo continua me incomodando. Isso é algo: "De alguma forma, preciso me lembrar da posição. E acho que o problema que tenho com ela é: como você" gera o índice atual "sem consumir memória (ou um contexto bastante específico em que suas saídas são consumidas de tal maneira que a ordem w-de suas saídas não importa).
Édouard
8

Como podemos simplesmente ignorar todos os caracteres alfanuméricos, assumiremos que a sequência contém apenas parênteses a partir de agora. Como na pergunta, existe apenas um tipo de parêntese, "()".

Se continuarmos removendo parênteses balanceados até que nenhum parêntese mais balanceado possa ser removido, todos os parênteses restantes deverão se parecer com ")) ...) ((… (", que são todos parênteses desequilibrados. Esta observação sugere que devemos encontrar primeiro esse ponto de virada) , antes do qual temos apenas parênteses de fechamento desequilibrados e depois dos quais temos apenas parênteses de abertura desequilibrados.

Aqui está o algoritmo. Em poucas palavras, ele calcula o ponto de viragem primeiro. Em seguida, ele gera parênteses de fechamento extra, varrendo a corda do início para a direita até o ponto de virada. Simetricamente, ele gera parênteses de abertura extra, digitalizando do final para a esquerda até o ponto de virada.


strn

Inicialize turning_point=0, maximum_count=0, count=0. Para cada ia partir 0de n-1fazer o seguinte.

  1. Se str[i] = ')', adicione 1 a count; caso contrário, subtraia 1.
  2. Se count > maximum_count, defina turning_point=ie maximum_count=count.

Agora turning_pointé o índice do ponto de virada.

Reset maximum_count=0, count=0. Para cada ia partir 0de turning_pointfazer o seguinte.

  1. Se str[i] = ')', adicione 1 a count; caso contrário, subtraia 1.
  2. Se count > maximum_countdefinir maximum_count = count. Saída icomo o índice de um parêntese de fechamento desequilibrado.

Reset maximum_count=0, count=0. Para cada um ide n-1para turning_point+1baixo, faça o seguinte.

  1. Se str[j] = '(', adicione 1 a count; caso contrário, subtraia 1.
  2. Se count > maximum_countdefinir maximum_count = count. Saída icomo o índice de um parêntese de abertura desequilibrada.

O(n)O(1)O(você)você


Se analisarmos o algoritmo acima, veremos que, de fato, não precisamos encontrar nem usar o ponto de virada. A boa observação de que todos os parênteses de fechamento desequilibrados ocorre antes que todos os parênteses de abertura desequilibrados possam ser ignorados, embora interessantes.

Aqui está o código em Python .

Basta clicar em "executar" para ver vários resultados do teste.


Exercício 1. Mostre que o algoritmo acima produzirá um conjunto de parênteses com a menor cardinalidade, de forma que os parênteses restantes sejam balanceados.

Problema 1. Podemos generalizar o algoritmo para o caso em que a cadeia contém dois tipos de parênteses, como "() []"? Temos que determinar como reconhecer e tratar a nova situação, o caso intercalado, "([)]".

John L.
fonte
Lol, exercício 1 e problema 1, fofo. A lógica do algoritmo que você descreveu é surpreendentemente difícil de visualizar. Eu teria que codificar isso amanhã para obtê-lo.
temporary_user_name
Parece que eu perdi a explicação bastante óbvia, mas mais importante. A lógica é, de fato, muito simples. Primeiro, produzimos cada parêntese de abertura extra. Quando passamos o ponto de virada, produzimos cada parêntese de fechamento extra. Feito.
John L.
Encontrar parênteses de abertura desequilibrados está incorreto. Ou seja, se sua arr é "())", p é 2 ep + 1 fica fora do limite da arr. Apenas uma idéia - para encontrar parênteses de abertura desequilibrados, você pode reverter arr e usar parte do algoritmo para encontrar parênteses de fechamento desequilibrados (é claro, com índices adaptados inversamente).
precisa saber é o seguinte
p+1
Levou-me um pouco para entender isso, mas eu gosto dele, é muito inteligente .. e funciona, pelo menos, para cada caso de eu ter pensado
dquijada