Balancieren Sie die Klammern

24

Ihr Ziel: Ausgeben des minimalen Damerau-Levenshtein-Abstands für eine Reihe von Klammern, damit aus der Eingabe-Zeichenfolge eine Zeichenfolge mit ausgeglichenen Klammern wird.

Eingang

Die Eingabezeichenfolge enthält nur Klammern und keine anderen Zeichen. Das heißt, es ist eine Kombination aller Zeichen in (){}[]<>. Sie können Eingaben entweder als Zeichenfolge oder als Array von Zeichen annehmen. Sie dürfen keine anderen Annahmen über die Eingabezeichenfolge treffen. Es kann beliebig lang sein (bis zu der von Ihrer Sprache unterstützten Maximalgröße), leer sein, die Klammern bereits ausgeglichen sein usw.

Damerau-Levenshtein Entfernung

Der Damerau-Levenshtein-Abstand zwischen zwei Zeichenfolgen ist die minimale Anzahl von Einfügungen, Löschungen, Ersetzungen einzelner Zeichen und Transpositionen (Vertauschen) zweier benachbarter Zeichen.

Ausgabe

Die Ausgabe sollte der minimale Damerau-Levenshtein-Abstand zwischen der Eingabezeichenfolge und einer Zeichenfolge sein, in der die Klammern übereinstimmen. Die Ausgabe sollte eine Zahl sein , nicht die resultierende ausgeglichene Zeichenfolge.

Ein Klammerpaar wird als "übereinstimmend" betrachtet, wenn die öffnende und schließende Klammer in der richtigen Reihenfolge sind und keine Zeichen enthalten, z

()
[]{}

Oder wenn jedes Unterelement in ihm auch übereinstimmt.

[()()()()]
{<[]>}
(()())

Unterelemente können auch mehrere Ebenen tief verschachtelt werden.

[(){<><>[()]}<>()]
<[{((()))}]>

(Danke an @DJMcMayhem für die Definition)

Testfälle

Input                   Possible Balanced       Output

Empty                   Empty                   0
[](){}<>                [](){}<>                0           
[(){}<>                 [(){}<>]                1           
[(])                    []()                    1           
[[[[[[[[                [][][][]                4
(](<>}[>(}>><(>(({}]    ()(<>)[(<><>){}]        7
>]{])<                  []{()}                  3
([)}}>[                 (){}<>                  4
{<((<<][{{}>[<)         <>(<<[]>{}>[])          5
{><({((})>}}}{(}}       {<><({()})>}{}{()}      4
(](<)>}[>(}>>{]<<(]]    (<()<><<>()>>[])<()>    9
}})(                    {}()                    2

(Danke an @WheatWizard für die Lösung der Hälfte der Testfälle)

Das ist , die wenigsten Bytes gewinnen!

Ihre Einsendungen sollten testbar sein, das heißt, sie sollten in nicht mehr als einer Stunde ein Ergebnis für jeden Testfall ausgeben.

Pavel
quelle
9
Balancieren Sie Ihre eigenen Klammern: P
Christopher
3
Ich werde überrascht sein, ob wir überhaupt eine richtige Antwort ohne Bruteforce auf diese Herausforderung sehen werden.
Orlp
5
@SIGSEGV die Antwort darauf ist 1. Es spielt keine Rolle , ob Sie es wie Balance [<>]oder []<>oder<>
Nathan Merrill
3
@Bijan Nah, es wird nicht viel einfacher, und außerdem steht der Geburtstag von Brain-Flak bald an!
Pavel
3
@qwr Warum ein Limit haben? Das Zeitlimit gilt nur für die angegebenen Testfälle, bei großen Eingaben kann Ihr Programm die ganze Zeit der Welt dauern.
Pavel

Antworten:

13

Retina, 254 252 264 248 240 232 267 Bytes

Vielen Dank an @AnthonyPham, @officialaimm und @MistahFiggins für den Hinweis auf Fehler

T`[]()`:;'"
+`'-*"|:-*;|{-*}|<-*>
-
+`'(\W+)"|:(\W+);|{(\W+)}|<(\W+)>
A$1$2$3$+B
+`'(\D+)"|:(\D+);|{(\D+)}|<(\D+)>
6$1$2$3$+9
(.*)(}{|"'|;:|><)
1$1
-

A6B9|6A9B
1
A6+B9+|A6+.B9+.|A+6.B+9
11
T`':{";}`<<<>
(.*)(<\W|\W>)
1$1
+`<(.*A.*B.*)?\W|\W(.*A.*B.*)?>
1$1$2
\W|6B|1

Probieren Sie es online!

Brute-Force-Lösung! Es funktioniert für alle Testfälle und hat sogar einen Fehler gefunden.

-2 Bytes dank @MartinEnder ( ${4}to $+)

+12 Bytes, um zusätzliche Auslagerungsfälle zu berücksichtigen

-16 Bytes durch bessere Nutzung von Zeichenklassen

-8 Bytes durch Entfernen einer unnötigen Einschränkung beim Auslagern. Dies hat auch einen Fehler behoben :)

-10 Bytes durch Kombinieren der Auslagerungslogik in einem einzigen regulären Ausdruck

+2 Bytes, um aufeinanderfolgende Auslagerungen zu berücksichtigen

+ viele für verschiedene Bugfixes **

Erläuterung:

T`[]()`:;'"wird verwendet, um spezielle Halterungsarten zu ersetzen. Erstens haben wir alle abgestimmt Klammern rekursiv ersetzen -, ABoder 69je nachdem , ob sie benachbart sind oder nicht.

Dann wird ein nützlicher "Austausch" durchgeführt, indem neu passende Klammern entfernt und ein 1an den Anfang der Zeichenkette angefügt werden. Wir ersetzen sie auch durch -die leere Zeichenfolge, da sie nur für den obigen Austausch verwendet wurde.

Als Nächstes versuchen wir "Ersetzungen", indem wir Paare nicht übereinstimmender Klammern entfernen, die sich nicht mit bereits übereinstimmenden Klammern überschneiden, und 1der Zeichenfolge ein a hinzufügen .

Schließlich \W|6B|1zählt alle verbleibenden Einzel Klammern plus die Anzahl der 1s.

** Ich arbeite derzeit an einer kürzeren Version, die die Funktion zum Aufteilen von Leitungen von Retina verwendet, obwohl ich auf ein beträchtliches Problem gestoßen bin, das eine Weile dauern kann.

Mathe-Junkie
quelle
Das ${4}kann man auf verkürzen $+. Ich bin sehr gespannt, warum das garantiert funktioniert.
Martin Ender
@ MartinEnder Danke! Ich bin nicht sicher, ob es immer funktioniert, aber es funktioniert zumindest für die bereitgestellten Testfälle und ein paar Randfälle, die ich mir
Math Junkie
2
Gegeben [{][}] [] [[][][][][][]] [][][][][][][][][][][][], Sie können einfach das }Innere des ersten Klammerpaares vertauschen und es ausbalancieren lassen. Der Abstand wird verwendet, um die Eingabe ein wenig lesbarer zu machen. Sie haben 3 ausgegeben, aber es sollte wirklich eine sein.
Anthony Pham
@AnthonyPham Danke! Ich glaube, ich weiß, warum das nicht funktioniert. Ich werde versuchen, eine clevere Möglichkeit zu finden, dies zu beheben
Math Junkie
Noch seltsamer ist, dass [{]}1, aber 2 [{][]}zurückgibt.
Anthony Pham
12

Brain-Flak , 1350 Bytes

{({}(())(<>))<>({(()()()())<{({}[()])<>}{}>}{}<>({<({}[()])>{()(<{}>)}}{}{}<>))<>}<>([[]]){([[]({}()<>)]<>)<>{(({}())<<>(({})<(({}(<()>))<>({}))([(())()()]){<>({}())}{}{<>{}<>({}()){(((({}<(({}<>)<{({}()<([(){}])>)}{}>)<>(({}(<>))<{({}()<([(){}])>)}{}<>>)><>({}))(<(((({}({})[()])[()()]<>({}))<>[({})({}){}]({}<>))<>[(({}<>)<>({}<>)<>)])<>>)))[()](<()>)<<>(({})<({}{}()){({}()<({}<>)<>>)}{}<>(({})<<>(({}<>))>)<>(())>){({}[()()]<(<([({[{}]<(({})()<>[({})]<>)>{()(<{}>)}}{}<(({})<>[()({}<(({}<<>({}<>)<>(({})<>)>)<>[(){}])<>>)]<>)>{()(<{}>)}{}(){[()](<{}>)}<<>{({}<>)<>}{}>)]({}{}))>)<>{({}<>)<>}>)}{}{}<>{}{}{({}<>)<>}{}{}(<>)<>{({}<>)<>}{}{(<{}>)<>{({}<>)<>}<>({}<{}>){({}<>)<>}}{}((({}<({}({})({})<{{}<>{}(<>)}{}(((({}<({}<>)>)<>)))<>>)<>>)<><({}<({}<<>(()())>)>)>)<<>({}<{}{({}<>)([()()()]){((({}()()<>))[()]<(({()(<{}>)}{})<>({}<(({}<<>({}[()()](()[({})({})]({[()](<{}>)}{}<>{}<(({})<>)>)<>))>)<>)>)<>)<>({}<({}<({}<({}<>)>)>)>)>)}{}{}<>}<>{}{}{}{}{}{}{}{}>)>)>)}{}({}<({}<{({}<(({}){({}())}{}{}<(({}){({}())}{}{}<>)>)>)<>}<>{((({}(()()){([{}](<({}(<()>)<>){({}<({}<>)>(())<>)}{}>({})<<>{{}({}<>)<>}{}>))([{}()]{})}{})))<>(({}))<>{<>({}[()])}{}({}<<>{}{}{<>}>)<>{}}<>(({}<>){[()](<{}>)}{})(<>)>)>)<>(<({}<>)>)<>}<>{}({}<(({}){({}())}{}{}){({}<({}<>)>(())<>)}{}{}>)<>{{}({}<>)<>}{}>)<>>)}{}<>([[]{}])}{}(([]){<{}{}>([])}{}<>){({}[()]<{}>)}{}({}<>)

Probieren Sie es online!

Bei Vergleichen mit konstanter Geschwindigkeit und Zeiger-Dereferenzierung ist dieser Algorithmus O (n 3 ). Leider hat Brain-Flak beides nicht, so dass dieses Programm stattdessen in O (n 5 ) -Zeit ausgeführt wird. Der längste Testfall dauert ca. 15 Minuten.

Ergebnisse vereinfachen

Um zu sehen, dass mein Algorithmus funktioniert, müssen wir einige Ergebnisse anzeigen, die den Suchraum erheblich reduzieren. Diese Ergebnisse beruhen auf der Tatsache, dass das Ziel eine ganze Sprache ist und nicht nur eine bestimmte Zeichenfolge.

  • Es sind keine Einfügungen erforderlich. Stattdessen können Sie einfach die Klammer entfernen, mit der das eingefügte Zeichen möglicherweise übereinstimmt.

  • Sie müssen niemals eine Halterung entfernen und dann die beiden Nachbarn austauschen. Um dies zu sehen, gehen davon aus, dass oBdA die entfernte Konsole ist (, so dass wir wandeln a(cauf cain zwei Schritten. Durch Ändern cund Einfügen einer Kopie können wir ca()in zwei Schritten ohne einen Tausch erreichen. (Diese Einfügung kann dann durch die obige Regel entfernt werden.)

  • Dieselbe Halterung muss niemals zweimal ausgetauscht werden. Dies ist eine Standardtatsache über die Damerau-Levenshtein-Distanz im Allgemeinen.

Ein weiteres vereinfachtes Ergebnis, das ich nicht verwendet habe, weil die Berücksichtigung dieser Daten Byte kosten würde:

  • Wenn zwei Klammern ausgetauscht werden und sie nicht übereinstimmen, wird die eventuelle Übereinstimmung mit jeder dieser Klammern niemals geändert oder ausgetauscht.

Der Algorithmus

Wenn eine Zeichenfolge auf eine ausgeglichene Zeichenfolge reduziert wird, ist eine der folgenden Bedingungen erfüllt:

  • Die erste Klammer wird gelöscht.
  • Die erste Klammer bleibt dort, wo sie ist, und stimmt an einer bestimmten Position mit der Klammer überein k(möglicherweise nachdem eine oder beide geändert wurden).
  • Die erste Halterung wird gegen die zweite ausgetauscht, die wiederum mit der Halterung an der Position übereinstimmt k.

Im zweiten Fall hat die Halterung an der Position kmöglicherweise mit einem ihrer Nachbarn getauscht. In beiden letzteren Fällen muss die Zeichenfolge zwischen der (möglicherweise neuen) ersten Klammer und der in Position beginnenden Klammer kzu einer ausgeglichenen Zeichenfolge bearbeitet werden, ebenso wie die Zeichenfolge, die aus allen nachfolgenden Elementen besteht k.

Dies bedeutet, dass ein dynamischer Programmieransatz verwendet werden kann. Da eine ausgetauschte Klammer nicht erneut ausgetauscht werden muss, müssen nur zusammenhängende Teilzeichenfolgen sowie Teilfolgen berücksichtigt werden, die durch Entfernen des zweiten Zeichens und / oder des vorletzten Zeichens aus einer solchen Teilzeichenfolge gebildet werden. Daher gibt es nur O (n 2 ) Untersequenzen, die wir betrachten müssen. Jedes von diesen hat O (n) mögliche Möglichkeiten, die erste Klammer abzugleichen (oder zu löschen), so dass der Algorithmus unter den obigen Bedingungen O (n 3 ) wäre .

Die Datenstruktur

Der rechte Stapel enthält die Klammern der ursprünglichen Zeichenfolge mit zwei Bytes pro Klammer. Der erste Eintrag bestimmt die gesamte Klammer und wird so ausgewählt, dass übereinstimmende Klammern einen Unterschied von genau 1 aufweisen. Der zweite Eintrag bestimmt nur, ob es sich um eine öffnende Klammer oder eine schließende Klammer handelt: Dies bestimmt, wie viele Änderungen erforderlich sind, damit zwei Klammern übereinstimmen gegenseitig. Darunter werden keine impliziten Nullen explizit angegeben, sodass wir []die Gesamtlänge dieser Zeichenfolge ermitteln können.

Jeder betrachtete Teilstring wird durch zwei Zahlen im Bereich von 0 bis 2n dargestellt: eine für die Anfangsposition und eine für das Ende. Die Interpretation ist wie folgt:

  • Ein Teilstring, der bei beginnt 2k, beginnt an der Position k(0-indiziert), und das zweite Zeichen wird nicht entfernt.
  • Ein Teilstring, der bei beginnt 2k+1, beginnt an der Position k, und das zweite Zeichen wird entfernt, da es nach links ausgetauscht wurde.
  • Eine Teilzeichenfolge, die am endet 2k, endet kurz vor der Position k(dh der Bereich ist von links nach rechts inklusive und von rechts exklusiv).
  • Eine Teilzeichenfolge, die am endet 2k-1, endet kurz vor der Position k, und das vorletzte Zeichen wird entfernt, da es nach rechts vertauscht wurde.

Einige Bereiche ( kzu k+1, 2k+1zu 2k+1, 2k+1zu 2k+3und 2k+1zu 2k+5) ergeben keinen physischen Sinn. Einige davon werden ohnehin als Zwischenwerte angezeigt, da es einfacher ist, zusätzliche Überprüfungen hinzuzufügen, um sie zu vermeiden.

Der linke Stapel speichert die Anzahl der Bearbeitungen, die erforderlich sind, um jede Teilzeichenfolge in eine ausgeglichene Zeichenfolge zu konvertieren. Die Bearbeitungsentfernung für das Intervall (x,y)wird in der Tiefe gespeichert x + y(y-1)/2.

Während der inneren Schleife werden Einträge über dem linken Stapel hinzugefügt, um anzuzeigen, welche Bewegungen möglich sind. Diese Einträge sind 5 Byte lang. Zählen von oben, die Zahlen sind d+1, y1, x1, y2, x2, wobei die Bewegung kostet dBearbeitungsschritte und unterteilt den Teilstring in (x1,y1)und (x2,y2).

Der Code

Beschreibung zu kommen. Im Moment ist hier meine Arbeitskopie des Codes. Einige Kommentare stimmen möglicherweise nicht mit der Terminologie überein.

# Determine bracket type for each byte of input
{({}(())(<>))<>({(()()()())<{({}[()])<>}{}>}{}<>({<({}[()])>{()(<{}>)}}{}{}<>))<>}

# For every possible interval length:
<>([[]]){

  # Compute actual length
  ([[]({}()<>)]<>)

  # Note: switching stacks in this loop costs only 2 bytes.
  # For each starting position:
  # Update/save position and length
  <>{(({}())<<>(({})<

    # Get endpoints
    (({}(<()>))<>({}))

    # If length more than 3:
    ([(())()()]){<>({}())}{}{

      # Clean up length-3 left over from comparison
      <>{}<>

      # Initialize counter at 2
      # This counter will be 1 in the loop if we're using a swap at the beginning, 0 otherwise
      ({}())

      # For each counter value:
      {

        # Decrement counter and put on third stack
        (((({}<

          # Do mod 2 for end position
          (({}<>)<{({}()<([(){}])>)}{}>)<>

          # Do mod 2 for start position
          (({}(<>))<{({}()<([(){}])>)}{}<>>)

        # Subtract 1 from counter if swap already happened
        ><>({}))(<

          # Compute start position of substrings to consider
          (((({}({})[()])[()()]<>({}))

            # Compute start position of matches to consider
            <>[({})({}){}]({}<>))<>

            # Compute end position of matches to consider
            [(({}<>)<>({}<>)<>)]

          # Push total distance of matches
          )

        # Push counter as base cost of moves
        # Also push additional copy to deal with length 5 intervals starting with an even number
        <>>)))[()](<()>)<

          # With match distance on stack
          <>(({})<

            # Move to location in input data
            ({}{}()){({}()<({}<>)<>>)}{}

            # Make copy of opening bracket to match
            <>(({})<<>(({}<>))>)

          # Mark as first comparison (swap allowed)
          <>(())>)

          # For each bracket to match with:
          {({}[()()]<

            (<([(

              # If swap is allowed in this position:
              {

                # Subtract 1 from cost
                [{}]

                # Add 1 back if swap doesn't perfectly match
                <(({})()<>[({})]<>)>{()(<{}>)}

              }{}

              # Shift copy of first bracket over, while computing differences
              <(({})<>[()({}<(({}<<>({}<>)<>(({})<>)>)<>[(){}])<>>)]<>)>

              # Add 1 if not perfectly matched
              {()(<{}>)}{}

              # Add 1 if neither bracket faces the other
              # Keep 0 on stack to return here
              (){[()](<{}>)}

              # Return to start of brackets
              <<>{({}<>)<>}{}>

            # Add to base cost and place under base cost
            )]({}{}))>)

            # Return to spot in brackets
            # Zero here means swap not allowed for next bracket
            <>{({}<>)<>}

          >)}

          # Cleanup and move everything to right stack
          {}{}<>{}{}{({}<>)<>}{}

          # Remove one copy of base cost, and move list of costs to right stack
          {}(<>)<>{({}<>)<>}{}

          # If swap at end of substring, remove second-last match
          {(<{}>)<>{({}<>)<>}<>({}<{}>){({}<>)<>}}{}

          # Put end of substring on third stack
          ((({}<({}({})({})<

            # If swap at beginning of substring, remove first match
            {{}<>{}(<>)}{}

            # Move start of substring to other stack for safekeeping
            (((({}<({}<>)>)<>)))

          # Create "deletion" record, excluding cost
          <>>)<>>)<>

          # Move data to left stack
          <({}<({}<<>

            # Add cost to deletion record
            (()())

          >)>)>)

          # Put start position on third stack under end position
          <<>({}<

            # For each matching bracket cost:
            {}{

              # Move cost to left stack
              ({}<>)

              # Make three configurations
              ([()()()]){

                # Increment counter
                ((({}()()<>))[()]<

                  # Increment cost in first and third configurations
                  (({()(<{}>)}{})<>({}<

                    # Keep last position constant
                    (({}<

                      # Beginning of second interval: 1, 2, 1 past end of first
                      <>({}[()()]

                        # End of first interval: -3, -1, 1 plus current position
                        (()[({})({})]

                          # Move current position in first and third configurations
                          ({[()](<{}>)}{}<>{}<

                            (({})<>)

                          >)

                        <>)

                      )

                    >)<>)

                  >)<>)

                  # Move data back to left stack
                  <>({}<({}<({}<({}<>)>)>)>)

                >)

              }{}

            {}<>}

            # Eliminate last entry
            # NOTE: This could remove the deletion record if no possible matches.  This is no loss (probably).
            <>{}{}{}{}{}{}{}{}

        # Restore loop variables
        >)>)>)

      }{}

      # With current endpoints on third stack:
      ({}<({}<

        # For all entries
        {

          # Compute locations and move to right stack
          ({}<(({}){({}())}{}{}<(({}){({}())}{}{}<>)>)>)<>

        }

        # For all entries (now on right stack):
        <>{

          # Cost of match
          ((({}

            # Do twice:
            (()()){([{}](

              # Add cost of resulting substrings
              <({}(<()>)<>){({}<({}<>)>(())<>)}{}>({})<<>{{}({}<>)<>}{}>

            # Evaluate as sum of two runs
            ))([{}()]{})}{}

          )))

          # Find smaller of cost and current minimum
          <>(({}))<>{<>({}[()])}{}

          # Push new minimum in place of old minimum
          ({}<<>{}{}{<>}>)

          <>{}

        }

        # Subtract 1 if nonzero
        <>(({}<>){[()](<{}>)}{})(<>)

      >)>)

      <>(<({}<>)>)<>

    # Otherwise (length 3 or less), use 1 from earlier as cost.
    # Note that length 0-1 is impossible here.
    }<>{}

    # With cost on third stack:
    ({}<

      # Find slot number to store cost of interval
      (({}){({}())}{}{})

      # Move to slot
      {({}<({}<>)>(())<>)}{}

    # Store new cost
    {}>)

    # Move other slots back where they should be
    <>{{}({}<>)<>}{}

  Restore length/position for next iteration
  >)<>>)}

  # Clear length/position from inner loop
  {}<>([[]{}])

}{}

(([]){<{}{}>([])}{}<>){({}[()]<{}>)}{}({}<>)
Nitrodon
quelle
2

Haskell , 797 Bytes

import Data.Array;import Data.Function;import Data.List;
e=length;f=fst;o=map;s=listArray;u=minimum;b p=let{m=e p;x=s(1,m)p;
v=s(1,m)(listArray('(','}')[0,0..]:[v!i//[(x!i,i)]|i<-[1..m-1]]);
d q=let{n=e q;y=s(1,n)q;t(a,b)=listArray((a,b),(m,n));
c=t(1,1)[sum[1|x!i/=y!j]|i<-[1..m],j<-[1..n]];
d=t(-1,-1)[if i<0||j<0then m+n else 
if i*j<1then(i+j)else u[1+d!(i-1,j),1+d!(i,j-1),c!(i,j)+d!(i-1,j-1),
let{k=v!i!(y!j)-1;l=w!(i,j-1)-1}in-3+i+j-k-l+d!(k,l)]|i<-[-1..m],j<-[-1..n]];
w=t(1,0)[if j>0&&c!(i,j)>0then w!(i,j-1)else j|i<-[1..m],j<-[0..n]]}in d!(m,n);
a=s(0,div m 2)([(m,"")]:[(concat.take 2.groupBy(on(==)f).sort.o(\q->(d q,q)))(
[b:c++[d]|[b,d]<-words"() <> [] {}",(_,c)<-a!(l-1)]++
concat[[b++d,d++b]|k<-[1..div l 2],(_,b)<-a!k,(_,d)<-a!(l-k)])|l<-[1..div m 2]]);
}in u(o(f.head)(elems a))

Probieren Sie es online!

Roman Czyborra
quelle
Gestern habe ich hier gelesen, dass das Kopfgeld nicht vor morgen enden würde, deshalb wollte ich bestreiten, dass eine Implementierung, die den en.wikipedia.org/wiki/… -Algorithmus anwendet, korrektere Werte berechnet als die aktuelle, sehr viel schnellere Retina-Heuristik!
Roman Czyborra
Nein, das ist den Preis doch nicht wert, weil ich fälschlicherweise extrapoliert habe, dass es die 18 Zeichen, die 4 in 2400s bei 800MHz entfernt sind, auch die 22 Zeichen, die 9 entfernt sind, ebenso nah an 3600s bringen würde, was es leider nicht tun würde.
Roman Czyborra