Seite 1 von 1

Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 09:51
von erdbeerblut
Hallo an alle!

Da ich keinen Block "Anfängerfragen" gefunden habe, versuche ich es hier.
Bitte nicht meckern, dass man das irgendwo nachlesen kann. Ich habe echt gesucht und keine zufriedenstellende Antwort gefunden.

Problem:
Ich möchte eine Aufgabe rekursiv lösen. Es können sich unüberschaubare Anzahlen möglicher Kombinationen ergeben, was scheinbar nicht ungewöhnlich ist.
Ich möchte dazu eine Funktion nutzen, die sich selbst aufruft, ähnlich wie das Turtle-Beispiel "Tree". Dabei soll sich aber der auszuwertende Parameter in jeder Rekursionsebene ändern. Der Parameter ist eine Liste und lässt sich nicht aus dem vorhergehenden erzeugen, sondern wird vom Anwender vorgegeben.

Wenn ich jetzt die Funktion mit den Parametern (a, b, ... n) definiere, wird für "a" ein Algorithmus abgearbeitet, der an verschiedenen Stellen eine Verzweigung auslöst. Dort müsste dann die Funktion erneut aufgerufen werden, die allerdings jetzt den Parameter "b" bearbeitet und bei Erfolg die Funktion für den Parameter "c" ausführen soll usw. Die Rekursion endet nach Ausführung von f(n)

Wie konkret erfolgt der Aufruf der Funktion an dieser Stelle? Sind die Parameter indiziert, so dass ich einen Zähler für die Tiefe mitlaufen lassen könnte?

Ziel soll es sein, alle Lösungen zu finden für alle mögliche Kombination aller Parameter.

Danke für Hilfe und ggf. Denkanstöße für eine elegante Lösungsmöglichkeit.

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 10:31
von sparrow
Ohne Beispiel schlecht nachvollziehen, aber:
Du brauchst keine rekursive Funktion mit einer dynamischen Anzahl von Parametern. Du brauchst eine rekursive Funktion, der du eine Datenstruktur als Parameter übergibst. Und wenn du als zweiten Parameter den Index mitgibst, dann brauchst du die Liste nicht einmal manipulieren.

Ob eine Rekursion sinnvoll ist, hängt aber davon ab, ob du die Rückgabe der Funktion vernünftig verarbeiten kannst. Das geht aus deiner Erklärung nämlich nicht hervor. Möglicherweise reicht auch eine einfache Iteration über die Elemente der Liste.

Oder anders, dass das hier in Python geht ist dir bekannt?

Code: Alles auswählen

>>> elements = ["Ham", "Spam", "Hurricane"]
>>> def do_somewhat_with(something):
    print(something)

>>> for element in elements:
    do_somewhat_with(element)

	
Ham
Spam
Hurricane
>>> 

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 10:44
von __blackjack__
Simples Beispiel das einfach nur die ganzen Elemente ausgibt:

Code: Alles auswählen

def f(items, index=0):
    if index < len(items):
        print(items[index])
        f(items, index + 1)

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 10:49
von erdbeerblut
Ja, sorry. Problemformulierung ist immer schwierig. Beispiel habe ich eben nicht zur Hand, nur etwas Zeit zum Nachdenken.

Das mit der Datenstruktur: Ich übergebe die Parameter nicht einzeln, sondern als EINE Liste von Listen (von Listen). Ich kann dann einfach die Liste in jedem Schritt vorn kürzen - bis sie leer ist (dann habe ich auch gleich ein schönes Rekursionsende). Oder ich spreche den jeweiligen Listenpunkt über den jeweiligen Index an, den ich dann aber mitzählen müsste.

Habe ich das richtig interpretiert? - Das probiere ich heute Abend jedenfalls mal aus :-)

Danke!

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 10:50
von sparrow
Datenstrukturen über die man iteriert - auch bei einer Rekursion - ändert man nicht. Nimm den Index mit.
Und nochmal meine Frage: Bist du sicher, dass du Rekursion brauchst und keine einfache Iteration über die Elemente reicht?

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 10:59
von erdbeerblut
Ja, das mit dem Index sieht super aus. Danke auch an _blackjack_

Ich möchte das Problem in einer einfachen und nachprüfbaren Dimension entwerfen und dann einfach hochskalieren können, indem ich die Anzahl der Parameter erhöhe.
Iterativ scheint mir das zu komplex zu werden. Nach meiner Anfängereinschätzung ist das rekursiv am einfachsten umzusetzen.

Das Beispiel dient auch nur für mich als Übungsbeispiel. Ich werde verschiedene Lösungswege probieren.

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Freitag 28. August 2020, 11:40
von Sirius3
"Alle möglichen Kombinationen" hört sich so an, als ob Du irgendetwas aus itertools benutzen solltest. Dann läßt sich das auch einfach durch eine iterative Schleife lösen.

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Mittwoch 2. September 2020, 14:42
von erdbeerblut
Hello again :-)

Ich habe mich tatsächlich an einer iterativen Lösung versucht, bekomme es aber nicht auf die Reihe. Wäre aber ein anderes Thema.

Die rekursive Lösung steht - ist aber sehr langsam. Ich kann aber die Rechengeschwindigkeit schlecht einordnen.
Das Problem ist eigentlich recht simpel: Der Algorithmus ordnet Spielsteine auf einem Spielbrett an. Konkret habe ich mal das Beispiel auf dem Bild unten abgebildet.
12 Puzzelteile auf einem Brett 5x11. Die Zahl der Kombinationen ist trotzdem schon enorm groß. Ich lasse jeden Spielstein alle möglichen Positionen auf dem Feld ausprobieren, bis er irgendwo passt und rufe dann die Funktion erneut auf, mit dem nächsten Stein auf dem um die vom Vorgänger reduzierten Spielfeld. Das ganze funktioniert soweit gut. Sind einige Steine vorgegeben, erhalten innerhalb von ca. 15 Sekunden die Lösung. Wenn ich aber das leere Brett für alle Steine vorgebe, rechnet er die ganze Nacht (mit IDLE auf einem nicht eben taufrischen Laptop) und findet nicht einmal die erste der x möglichen Lösungen. Habe dann abgebrochen.

Wie kann die Sache beschleunigt werden? Gibt es da allgemeine Handlungsempfehlungen? Liegt es an der Rekursion oder schlicht an der großen Datenmenge oder ist das Problem für Interpretersprachen nicht so geeignet? Mir geht es nicht um Höchstgeschwindigkeit. Wenn ein paar Minuten gerechnet werden muss ist das okay. Aber 10 Stunden ohne Teillösung ist schon heftig.


Bild

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Mittwoch 2. September 2020, 15:31
von Sirius3
@erdbeerblut: ein paar 10^x Möglichkeiten gibt es schon, das kann dauern. Zeigt doch mal Deinen bisherigen Code, dann kann man besser sehen, ob es eine geschicktere Möglichkeit gibt.

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Mittwoch 2. September 2020, 18:30
von erdbeerblut
Hallo sirius!

Danke für das Angebot. An dem Code ist sicher einiges zu verbessern. Bin für konstruktive Tipps dankbar. Beim Hantieren mit den Listen muss man sicher nicht alles mit einer for-Schleife lösen...

Im Code-Beispiel sind Steine und Spielfeld wie im Beispiel 26 angelegt. Langfrsitig will ich noch eine vernünftige Eingabe und grafische Ausgabe ergänzen. Lohnt sich aber nur, wenn die Rechnerei in angemessener Zeit erfolgen kann. Das leere Brett mit allen Steinen sprengt bis dato meine Geduld bei der Rechenzeit.
Bild

Code: Alles auswählen

def dreh(a):
  """Dreht den Stein um 90 Grad nach links"""
  b = []
  for t in range(len(a)):
    b.append([a[t][1]-a[0][1],a[0][0]-a[t][0]])
  return(b)

def spgl(a):
  """Spiegelt den Stein durch Vertauschen von x und y"""
  b = []
  for t in range(len(a)):
    b.append([a[t][1],a[t][0]])
  return(b)

def null(a):
  """Verschiebt den Stein in den Nullpunkt"""
  dx=(min(a)[0])
  dy=min([a[i][1] for i in range(len(a))])
  if dx != 0:
    for i in range(len(a)):
      a[i][0] -= dx
  if dy != 0:
    for i in range(len(a)):
      a[i][1] -= dy
  a=sorted(a)
  return(a)

def var(a):
  """Erstellt alle möglichen Variarionen, den Stein abzulegen"""
  a1=null(a)
  a2=null(dreh(a1))
  a3=null(dreh(a2))
  a4=null(dreh(a3))
  a5=null(spgl(a))
  a6=null(dreh(a5))
  a7=null(dreh(a6))
  a8=null(dreh(a7))
  b=[a1,a2,a3,a4,a5,a6,a7,a8]
  c=[]
  for i in range(len(b)):
    if b[i] not in c:
      c.append(b[i])
  return(c)

def zeichne(a):
  """Zur grafischen Kontrolle eines Steines"""
  for y in range(5):
    for x in range(5):
      if [x,y] in a:
        print("O",end="")
      else:
        print("-",end="")
    print("")
  print("")

def prlsg(loesung,feld):
  """Gibt eine Lösung in der Konsole aus aus"""
  for y in range(5):
    for x in range(11):
       for i in range(len(loesung)):
        if [x,y] in loesung[i][0]:
          print(loesung[i][1],end="")
        else:
          print("",end="")
       if not [x,y] in feld:
         print("+",end="")
    print("")

def testfeld(steine,feld,startfeld,index=0,loesung=[]):
  """Prüft, ob ein einzelner Stein innerhalb des Feldes liegt;
     'feld' wird innerhalb der Rekursion verändert;
     'startfeld' bleibt unverändert, um es in die Lösungsfunktion geben zu können
     'die einzelnen Lösungen werden in der Liste 'gesamt' gesammelt"""
  for k in range(len(var(steine[index]))):
    stein=var(steine[index])[k] #aus der Liste 'steine' wird ein stein entnommen und seine Variationen erzeugt
    for i in range(len(feld)):
      dx=feld[i][0]-stein[0][0]
      dy=feld[i][1]-stein[0][1]
      for j in range(len(stein)):
        if not [stein[j][0]+dx,stein[j][1]+dy] in feld:
          break #wenn ein Teil des Steines nicht in feld liegt, wird hier unterbrochen
        else:
          if j+1==len(stein): #der Stein liegt vollständig in feld
            steinpos=[]
            for q in range(len(stein)): #Steinposition für Übergabe in Lösung ermittelt
              steinpos.append([stein[q][0]+dx,stein[q][1]+dy])
            feldneu = feld[:] #verbleibendes Spielfeld für nächsten Rekursionsschritt
            [feldneu.remove(element) for element in steinpos]
            
            for t in range(len(steine)): #springt die Rekursion zurück werden ungültige Steinpositionen gelöscht
              if len(loesung)>0:
                if loesung[-1][1]>=index:
                  del loesung[-1]
            loesung.append([steinpos,index]) #aktuelle Steinposition wird als mögliche Lösung gespeichert
            
            if index+1 < len(steine): 
              testfeld(steine,feldneu,startfeld,index+1) #nächster Rekursionsschritt
            elif index+1 == len(steine): #wenn der Index gleich der Zahl der Steine ist, ist eine Lösung gefunden
              print("Lösung:")
              prlsg(loesung,startfeld)
              
              gesamt.append(loesung) #muss hier die globale Variable nutzen - mit return bekomme ichs nicht vernünftig raus
 
    
def main():
  #feld=[] #gesamtes Spielbrett erzeugen (Spielfeld komplett leer)
  #for x in range(11):
    #for y in range(5):
      #feld.append([x,y])

  #Steine definieren
  pink00=[[0,0],[0,1],[1,1],[1,2],[1,3]]
  lind01=[[0,0],[0,1],[0,2],[1,0],[1,2]]
  oran02=[[0,0],[1,0],[1,1],[1,2],[2,1]]
  blau03=[[0,0],[0,1],[0,2],[1,0],[2,0]]
  petr04=[[0,0],[0,1],[0,2],[1,1]]
  rot005=[[0,0],[0,1],[0,2],[0,3],[1,0]]
  mari06=[[0,0],[0,1],[0,2],[1,0]]
  mint07=[[0,0],[0,1],[0,2],[1,0],[1,1]]
  lila08=[[0,0],[0,1],[1,1],[1,2],[2,2]]
  gelb09=[[0,0],[0,1],[0,2],[0,3],[1,1]]
  wein10=[[0,0],[0,1],[1,1],[1,2]]
  azur11=[[0,0],[0,1],[1,0]]
   
  
  #Liste *aller* Steine
  steine=[pink00,lind01,oran02,blau03,petr04,rot005,mari06,mint07,lila08,gelb09,wein10,azur11]
  
  #Liste der fehlenden Steine Beispiel 26
  steine= [pink00,lind01,oran02,blau03,petr04,rot005,mari06]

  #verbleibendes Spielfeld für Beispiel 26
  feld=[                [0, 2], [0, 3], [0, 4],
                                [1, 3], [1, 4], 
                                [2, 3], [2, 4],
                                [3, 3], [3, 4],
                        [4, 2], [4, 3], [4, 4],
                                [5, 3], [5, 4],
                                [6, 3], [6, 4],
                [7, 1], [7, 2], [7, 3], [7, 4],
                        [8, 2], [8, 3], [8, 4],
        [9, 0], [9, 1], [9, 2], [9, 3], [9, 4],
        [10,0], [10,1], [10,2], [10,3], [10,4]]                
  print("Start...")
  testfeld(steine,feld,feld)
  print(len(gesamt) ,"Lösungen")
  

gesamt=[] #global als Hilslösung bis jetzt - vielleicht geht es auch anders?
if __name__ == "__main__":
  main()


Re: Parameterübergabe in rekursiver Funktion

Verfasst: Mittwoch 2. September 2020, 19:54
von __blackjack__
@erdbeerblut: Nicht nur `gesamt` ist globale, sondern auch das ”locale” `loesung` in `testfeld` ist eine globale Liste. Zudem wird die *selbe* Liste für jede Lösung verwendet die an `gesamt` angehängt wird. Das ist ein Programmierfehler!

`gesamt` würde man sehr einfach von der globalen Ebene weg bekommen in dem man die Liste einfach als weiteres Argument mitgibt. Die Änderungen würde der Aufrufer am Ende ja sehen.

Letztlich will man hier aber eine Generatorfunktion schreiben, so das man die Ergebnisse einfach per ``yield`` liefert und dann per ``yield from`` bei den rekursiven Aufrufen nach oben durchreicht. Ein Aufrufer kann sich dann entscheiden ob er alle Ergebnisse will, oder nur das erste von dem Generator abfragt.

Eingerückt wird in Python mit vier Leerzeichen pro Ebene. Es braucht auch einiges an Leerzeichen damit das lesbar(er) wird. Um Gleichheitszeichen bei Zuweisungen ausserhalb von Argumentlisten, um binäre Operatoren, und nach Kommas.

An Zeilenenden kommen nur kurze Kommentare. Wenn die länger werden, insbesondere wenn damit die maximale Zeilenlänge überschritten wird (79 bis 120 Zeichen) kommen Kommentare vor den Code den sie kommentieren. Ich sehe das mit der Zeilenlänge konservativ: 79/80 Zeichen pro Zeile.

``return`` ist keine Funktion und sollte auch nicht so geschrieben werden als wäre es eine.

`spgl()` und `prlsg()` gehen als Funktionsnamen *gar nicht*. Namen sollen dem Leser vermitteln was der Wert bedeutet, nicht zum rätseln nötigen. `var()` ist auch nicht gut, zum einen weil auch der Name nicht verrät was gemacht wird, weil kryptische Abkürzung, zum anderen weil es bereits eine eingebaute `var()`-Funktion gibt die dadurch verdeckt wird. Insgesamt sollte man das Programm durchgehen und sinnvolle Namen vergeben. Einbuchstabige Namen, Abkürzungen, und irgendwelche angehängten Nummern sind schlechte Namen. Und Namen sind keine Kosmetik, die sind wichtig zum verständnis eines Programms und haben damit auch einfluss darauf wie leicht man Fehler macht oder wie leicht man Fehler finden kann. (Einbuchstabige Namen gehen bei ganzzahligen Laufindexen und so Sachen wie Koordinaten.)

Koordinaten sind Tupel und keine Listen.

Wenn man das Brett durch die Menge der unbelegten Koordinaten repräsentiert, dann durch eine Menge (`set`) und nicht durch eine Liste. Da braucht man dann auch die Koordinaten als Tupel.

Es ist entschieden zu oft das „anti pattern“ ``for i in range(len(sequence)):`` im Code. Man kann in Python *direkt* über die Elemente von Sequenzwerten wie Listen iterieren, ohne den Umweg über einen Index.

Besonders gruselig ist in diesem Zusammenhang der Aufruf der `var()`-Funktion. Die wird einmal aufgerufen, wobei sie alle Varianten erzeugt, nur um die Länge der Liste mit den Varianten zu nehmen — die Varianten selbst werden wieder verworfen. Und dann wird für jedes Element der verworfenen Liste wieder die ganze Liste berechnet, nur um davon jeweils *ein* Element zu nehmen und die Liste dann wieder zu verwerfen. Das tut beim lesen des Codes schon fast physisch weh. :-)

Man kann mehr „list comprehensions“ verwenden. Und eine weniger, denn die sind nicht dazu da ``for``-Schleifen zu ersetzen, sondern dazu da Listen zu erzeugen.

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Mittwoch 2. September 2020, 20:09
von Sirius3
Eingerückt wird generell mit 4 Leerzeichen pro Ebene, nicht 2 oder 1 oder 3.
Benutze keine aussagekräftige Namen und keine kryptischen Abkürzungen.
Über einen Index zu iterieren, ist ein Anti-Pattern, weil man auch direkt über die Elemente der Liste iterieren kann.
Statt Indizes benutze sprechende Namen. `return` ist keine Funktion, sollte also auch nicht wie eine geschrieben werden:

Code: Alles auswählen

def drehe_stein(stein):
    """Dreht den Stein um 90 Grad nach links"""
    ergebnis = []
    for x, y in stein:
        ergebnis.append((y, -x))
    return ergebnis
oder kurz:

Code: Alles auswählen

def drehe_stein(stein):
    """Dreht den Stein um 90 Grad nach links"""
    return [(y, -x) for x, y in stein]

def spiegle_stein(stein):
    """Spiegelt den Stein durch Vertauschen von x und y"""
    return [(y, x) for x, y in stein]

def normiere_stein(stein):
    """Verschiebt den Stein in den Nullpunkt"""
    dx = min(x for x, y in stein)
    dy = min(y for x, y in stein)
    return sorted((x - dx,y - dy) for x, y in stein)

def erzeuge_varianten(stein):
    """Erstellt alle möglichen Variarionen, den Stein abzulegen"""
    varianten = [stein]
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    stein = spiegle_stein(stein)
    varianten.append(stein)
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    ergebnis = []
    for stein in varianten:
        stein = normiere_stein(stein)
        if stein not in ergebnis:
            ergebnis.append(stein)
    return ergebnis

def zeichne_stein(stein):
    """Zur grafischen Kontrolle eines Steines"""
    for y in range(5):
        print("".join(
            "O" if (y,x) in stein else "-"
            for x in range(5)))
Die Funktion testfeld ist viel zu lang und zu komplex und sollte in mehrere Funktionen aufgeteilt werden.
Benutze niemals veränderliche Werte als Defaultparameter. Denn die Veränderung ist global.
Genauso benutze keine globalen Variablen.
Statt Milliarden mal die Variationen der Steine zu berechnen, solltest Du das außerhalb einmal machen.
`steine` ist also eine Liste von Listen.
Dass das Setzen eines Steines innerhalb der for-Schleife zum Prüfen liegt, macht den Code schwer verständlich.
Eine List-Comprehension ist kein Ersatz für eine for-Schleife, wenn die Ergebnisliste gar nicht benutzt wird.
Sets sind deutlich einfacher in der Programmerung und deutlich effizienter.

Code: Alles auswählen

def verschiebe_stein(stein, dx, dy):
    return [(x+dx, y+dy) for x, y in stein]

def testfeld_schritt(steine, feld, index=0, loesung=None):
    """Prüft, ob ein einzelner Stein innerhalb des Feldes liegt;
      'feld' wird innerhalb der Rekursion verändert;
      'startfeld' bleibt unverändert, um es in die Lösungsfunktion geben zu können
      'die einzelnen Lösungen werden in der Liste 'gesamt' gesammelt"""
    if loesung is None:
        loesung = {}
    for stein in steine[index]:
        for fx, fy in feld:
            dx = fx - stein[0][0]
            dy = fy - stein[0][1]
            verschobener_stein = verschiebe_stein(stein, dx, dy)
            neues_feld = feld.difference(verschobener_stein)
            # testen, ob alle Steine entfernt wurden
            if len(feld) - len(stein) == len(neues_feld):
                loesung.update(dict.fromkeys(verschobener_stein, index))
                if index + 1 == len(steine):
                    # Lösung gefunden
                    print_feld(loesung)
                else:
                    testfeld_schritt(steine, neues_feld, index + 1, loesung)

def testfeld(steine, feld):
    # erzeuge Steinvariationen
    steine = [erzeuge_varianten(stein) for stein in steine]
    testfeld_schritt(steine, feld, 0)

def print_feld(loesung):
    """Gibt eine Lösung in der Konsole aus aus"""
    for y in range(5):
        print("".join(
            chr(65 + loesung.get((x,y), -22))
            for x in range(11)
        ))
    print()

def main():
    # Steine definieren
    pink00 = [[0,0],[0,1],[1,1],[1,2],[1,3]]
    lind01 = [[0,0],[0,1],[0,2],[1,0],[1,2]]
    oran02 = [[0,0],[1,0],[1,1],[1,2],[2,1]]
    blau03 = [[0,0],[0,1],[0,2],[1,0],[2,0]]
    petr04 = [[0,0],[0,1],[0,2],[1,1]]
    rot005 = [[0,0],[0,1],[0,2],[0,3],[1,0]]
    mari06 = [[0,0],[0,1],[0,2],[1,0]]
    mint07 = [[0,0],[0,1],[0,2],[1,0],[1,1]]
    lila08 = [[0,0],[0,1],[1,1],[1,2],[2,2]]
    gelb09 = [[0,0],[0,1],[0,2],[0,3],[1,1]]
    wein10 = [[0,0],[0,1],[1,1],[1,2]]
    azur11 = [[0,0],[0,1],[1,0]]
    
    # gesamtes Spielbrett erzeugen (Spielfeld komplett leer)
    feld =set((x,y) for x in range(11) for y in range(5))
    # Liste *aller* Steine
    steine = [pink00, lind01, oran02, blau03,
        petr04, rot005, mari06, mint07,
        lila08, gelb09, wein10, azur11]
    
    # Liste der fehlenden Steine Beispiel 26
    steine = [pink00, lind01, oran02, blau03,
        petr04, rot005, mari06]
    #verbleibendes Spielfeld für Beispiel 26
    feld = {
                        (0, 2), (0, 3), (0, 4),
                                (1, 3), (1, 4), 
                                (2, 3), (2, 4),
                                (3, 3), (3, 4),
                        (4, 2), (4, 3), (4, 4),
                                (5, 3), (5, 4),
                                (6, 3), (6, 4),
                (7, 1), (7, 2), (7, 3), (7, 4),
                        (8, 2), (8, 3), (8, 4),
        (9, 0), (9, 1), (9, 2), (9, 3), (9, 4),
        (10,0), (10,1), (10,2), (10,3), (10,4)
    }
    testfeld(steine, feld)

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Donnerstag 3. September 2020, 13:49
von erdbeerblut
__blackjack__ hat geschrieben: Mittwoch 2. September 2020, 19:54 Nicht nur `gesamt` ist globale, sondern auch das ”locale” `loesung` in `testfeld` ist eine globale Liste. Zudem wird die *selbe* Liste für jede Lösung verwendet die an `gesamt` angehängt wird. Das ist ein Programmierfehler!
'loesung' ist in diesem Fall global, weil ich die Variable stehen lasse und einfach weiternutze statt sie wieder neu zu übergeben? Wenn ich sie als Argument mitgebe, wäre es okay?
`gesamt` würde man sehr einfach von der globalen Ebene weg bekommen in dem man die Liste einfach als weiteres Argument mitgibt. Die Änderungen würde der Aufrufer am Ende ja sehen.
Das hatte ich probiert. Wie erhalte ich dann aber Zugriff auf die Variable außerhalb der Funktion? Mit Return geht es nicht - also yield...
Letztlich will man hier aber eine Generatorfunktion schreiben, so das man die Ergebnisse einfach per ``yield`` liefert und dann per ``yield from`` bei den rekursiven Aufrufen nach oben durchreicht. Ein Aufrufer kann sich dann entscheiden ob er alle Ergebnisse will, oder nur das erste von dem Generator abfragt.
Das Kapitel über Generatoren muss ich erst noch lesen, wird erledigt :)

Die Formatierungshinweise werde ich berücksichtigen. Dies und die anderen Optimierungen hat

@Sirius3 ja schon freundlicherweise erledigt. - Danke!
Ich schaue alles heute Abend in Ruhe durch und melde mich mit dem was ich nicht verstehe zurück. Ich sehe noch nicht, dass das mit dem generieren der Lösung funktioniert. Überschreibt "update" automatisch die falschen vorher eingefügten? Ich muss das mal in Ruhe testen und sicher noch vervollständigen(?)

Bis hier:
Das Feld ist eine Menge von Tupeln. Habe den Vorteil verstanden.
Die Steine sind nach wie vor Listen von Listen, weil Tupel nicht veränderbar sind und die Steine ja verschoben etc. werden sollen?

Würde ein Array Vorteile bringen? Ich benötige ja nur ganze Zahlen. Irgendwo habe ich gelesen, dass es Rechenzeit kostet wenn Python den Typ Variable bestimmen muss. In einem Array wäre er vordefiniert. Ist das richtig?

Seht ihr hier -ganz grundsätzlich- eine sinnvolle Möglichkeit ohne Rekursion auszukommen und damit einen Geschwindigkeitsvorteil zu erreichen? Würdet ihr die Lösung des Problems insgesamt anders angehen?
Jetzt bitte keine fertigen Lösungen posten – nur Denkanstöße geben. Wenn ich es selbst erarbeite, begreife ich es besser und behalte es länger. :geek:

Danke nochmal euch beiden!

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Donnerstag 3. September 2020, 22:38
von bords0
Eine Beschleunigung ist mit einem anderen Algorithmus möglich. Im Moment ist es z.B. so, wenn ein Platz gar nicht mehr belegt werden kann, probiert der Algorithmus trotzdem, alle Steine irgendwo zu platzieren, und der Problemplatz verhindert dann das Legen des letzten Steins - nach ganz viel Rechenaufwand.

Exact Set Cover
Dieses Problem, eine gegebene Fläche ganz zu bedecken mit jeweils genau einem Teil, hat einen Namen: Exact Set Cover. Das kann man in vielen Fällen recht effizient lösen mit dem "Algorithm X" von Donald Knuth. (Die ebenfalls von ihm stammende Implementierung "Dancing Links", die super-effizient ist, ist in python leider nicht direkt umsetzbar, aber der Algorithm X schon.)

Algorithm X Implementierung
Es lohnt sich, dazu mal einen Artikel im Internet zu lesen. Ich habe für mich eine sehr simple Variante aus dem Internet verwendet, "Algorithm X in 30 lines": https://www.cs.mcgill.ca/~aassaf9/pytho ... thm_x.html.

Anwendung auf das Problem
Man muss - wie häufig - allerdings die Teile, mit denen man die Fläche bedecken will, geeignet interpretieren. Hier habe ich das so gemacht:
  1. Jeder Stein wird in jeder Variante (Drehung und Spiegelung) und jeder Verschiebung als eigener Stein betrachtet.
  2. Damit jeder Stein nur einmal auftaucht, belegt jede dieser Varianten des Original-Steins noch einen virtuellen, nicht-sichtbaren Platz. Jeder Original-Stein hat einen eigenen solchen virtuellen Platz, alle seine Varianten haben auch den vom Original. Da der virtuelle Platz nur einmal belegt werden kann, kann automatisch jeder Stein nur einmal auftauchen.
  3. Ein Stein in einer bestimmten Orientierung an einem bestimmten Ort wird im Programm als Tupel (i, j, dx, dy) gespeichert: i = Nummer des Steins, j = Nummer der Variante, dx und dy die Verschiebung.
  4. Ein Platz wird als (x, y) dargestellt (bedarf wohl keiner Erklärung), ein virtueller Platz einfach als i.
Dann muss man also "nur" noch die dicts X und Y aufschreiben:
  • In X ist jedem Platz (auch den virtuellen) ein set von allen Steinen zugeordnet, die diesen Platz belegen.
  • In Y ist jedem (auch bewegten) Stein eine Liste aller belegten Plätze (inklusive der virtuellen) zugeordnet.
Ein bisschen was hab ich auch selbst gemacht
Die Erzeugung von X und Y habe ich selbst geschrieben. Das sieht kurz aus, ist aber weniger trivial als man denkt, wenn man sich erst einmal die obige Anwendung mit allen Details einfallen lassen und sauber umsetzen muss.
Den Algorithmus X habe ich aus der o.g. Quelle, mit leichter Modifikation zur Optimierung (s. Kommentar). Dadurch wurde es immerhin nochmal 40 % schneller.
Den Rest habe ich i.W. von Sirius3 übernommen, aber mit Modifikation bei print_feld, weil meine Lösungsstruktur anders ist.

Zur Performance:
Mit dem Programm von Sirius3 brauche ich auf meinem Rechner etwa 4,6 Sekunden für die Lösung des teilgefüllten Feldes (ohne Ausgabe):

Code: Alles auswählen

In [8]: %timeit testfeld(steine, feld)
4.6 s ± 26.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
"Mein" Programm braucht für die ersten 100 Lösungen vom nicht gefüllten Feld (also mit allen 12 Teilen) etwa 3.3 Sekunden:

Code: Alles auswählen

In [14]: %timeit X, Y = berechne_bedingungen(); list(itertools.islice(solve(X, Y), 100))
3.24 s ± 35.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
(Ich habe in einer Variante die Lösung von teilgefüllten Feldern programmiert, aber das sieht nicht schön aus, und vielleicht wird es ja nicht gebraucht, wenn man auch die "ganze" Lösung haben kann?)

Lösungen
Allerdings gibt es wohl sehr viele Lösungen, heute morgen habe ich bei über 1 Mio.abgebrochen.
Nach meinen Tests waren die tatsächlich alle verschieden, und stichprobenweise auch korrekt.

Lässt man das Programm einfach so laufen, werden die ersten 10 Lösungen ausgegeben:

Code: Alles auswählen

LHHHGDDDBBB
LLHHGGGDBCB
AAJJJJIDCCC
FAAAJIIEKKC
FFFFIIEEEKK

LHHHGDDDBBB
LLHHGGGDBCB
AAJJJJIDCCC
FAAAJIIKKEC
FFFFIIKKEEE

LHHHGDDDBBB
LLHHGGGDBCB
AAJJJJIDCCC
FAAAJEIIKKC
FFFFEEEIIKK

LHHHJJJJIIK
LLHHGEJIIKK
AAGGGEEICKD
FAAABEBCCCD
FFFFBBBCDDD

LHHHEEEIIKK
LLHHGEIIKKJ
AAGGGCIDBBJ
FAAACCCDBJJ
FFFFCDDDBBJ

LHHHIIJJJJE
LLHHGIIJKEE
AAGGGCIDKKE
FAAACCCDBKB
FFFFCDDDBBB

LHHHIIEEEKK
LLHHGIIEKKJ
AAGGGCIDBBJ
FAAACCCDBJJ
FFFFCDDDBBJ

LHHHIIKKEEE
LLHHGIIKKEJ
AAGGGCIDBBJ
FAAACCCDBJJ
FFFFCDDDBBJ

LHHHCDDDBBJ
LLHHCCCDBJJ
AAGGGCIDBBJ
FAAAGIIEKKJ
FFFFIIEEEKK

LHHHCDDDBBB
LLHHCCCDBKB
AAGGGCIDKKE
FAAAGIIJKEE
FFFFIIJJJJE
Und hier nun endlich das Programm - viel Spaß damit!

Code: Alles auswählen

import itertools
from collections import defaultdict


# Algorithm X in 30 lines, von
# https://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html

def solve(X, Y, solution=[]):
    if not X:
        yield list(solution)
    else:
        # c = min(X, key=lambda c: len(X[c]))  # original
        # for r in list(X[c]):  # original
        for r in min(X.values(), key=len).copy():  # neu
            solution.append(r)
            cols = select(X, Y, r)
            for s in solve(X, Y, solution):
                yield s
            deselect(X, Y, r, cols)
            solution.pop()

def select(X, Y, r):
    cols = []
    for j in Y[r]:
        for i in X[j]:
            for k in Y[i]:
                if k != j:
                    X[k].remove(i)
        cols.append(X.pop(j))
    return cols

def deselect(X, Y, r, cols):
    for j in reversed(Y[r]):
        X[j] = cols.pop()
        for i in X[j]:
            for k in Y[i]:
                # if k != j:  # überflüssig, add fragt das schneller ab
                    X[k].add(i)
                    

# von Sirius3:
# https://www.python-forum.de/viewtopic.php?f=1&t=49502#p372390

def drehe_stein(stein):
    """Dreht den Stein um 90 Grad nach links"""
    return [(y, -x) for x, y in stein]

def spiegle_stein(stein):
    """Spiegelt den Stein durch Vertauschen von x und y"""
    return [(y, x) for x, y in stein]

def normiere_stein(stein):
    """Verschiebt den Stein in den Nullpunkt"""
    dx = min(x for x, y in stein)
    dy = min(y for x, y in stein)
    return sorted((x - dx,y - dy) for x, y in stein)

def erzeuge_varianten(stein):
    """Erstellt alle möglichen Variarionen, den Stein abzulegen"""
    varianten = [stein]
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    stein = spiegle_stein(stein)
    varianten.append(stein)
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    ergebnis = []
    for stein in varianten:
        stein = normiere_stein(stein)
        if stein not in ergebnis:
            ergebnis.append(stein)
    return ergebnis

# leicht modifiziert, weil die Struktur der Lösung anders aussieht
def print_feld(loesung, Y):
    """Gibt eine Lösung in der Konsole aus"""
    feld = dict()
    for i, j, dx, dy in loesung:
        for xy in Y[i, j, dx, dy]:
            feld[xy] = i
    for y in range(5):
        print("".join(
            chr(65 + feld.get((x,y), -22))
            for x in range(11)
        ))
    print()


# Die Steine-Erstellung etc. habe ich eine Funktion gepackt
def berechne_bedingungen():

    pink00 = [[0,0],[0,1],[1,1],[1,2],[1,3]]
    lind01 = [[0,0],[0,1],[0,2],[1,0],[1,2]]
    oran02 = [[0,0],[1,0],[1,1],[1,2],[2,1]]
    blau03 = [[0,0],[0,1],[0,2],[1,0],[2,0]]
    petr04 = [[0,0],[0,1],[0,2],[1,1]]
    rot005 = [[0,0],[0,1],[0,2],[0,3],[1,0]]
    mari06 = [[0,0],[0,1],[0,2],[1,0]]
    mint07 = [[0,0],[0,1],[0,2],[1,0],[1,1]]
    lila08 = [[0,0],[0,1],[1,1],[1,2],[2,2]]
    gelb09 = [[0,0],[0,1],[0,2],[0,3],[1,1]]
    wein10 = [[0,0],[0,1],[1,1],[1,2]]
    azur11 = [[0,0],[0,1],[1,0]]
    
    # Liste *aller* Steine
    steine = [pink00, lind01, oran02, blau03, petr04, rot005, 
              mari06, mint07, lila08, gelb09, wein10, azur11]

    # Jetzt endlich mein eigener Teil :-)
    # (Ist aber doch ein bisschen schwieriger gewesen als es aussieht.)

    X = defaultdict(set)
    Y = defaultdict(list)
    
    for i, stein in enumerate(steine):
        for j, variante in enumerate(erzeuge_varianten(stein)):
            for dx in range(11 - max(xy[0] for xy in variante)):
                for dy in range(5 - max(xy[1] for xy in variante)):
                    # stein kann nur an einer Stelle sein, daher bekommen alle 
                    # Varianten eine künstliche überlappende Stelle dazu
                    X[i].add((i, j, dx, dy))  # virtueller Platz i
                    Y[i, j, dx, dy].append(i)
                    for x, y in variante:
                        # Der Platz (x, y) wird vom Stein i in Variante j 
                        # mit Verschiebung (dx, dy) überdeckt
                        X[x + dx, y + dy].add((i, j, dx, dy))  
                        Y[i, j, dx, dy].append((x + dx, y + dy))
                        
    return X, Y

    
if __name__ == "__main__":
    X, Y = berechne_bedingungen()
    for loesung in itertools.islice(solve(X, Y), 10):
        print_feld(loesung, Y)


Re: Parameterübergabe in rekursiver Funktion

Verfasst: Samstag 5. September 2020, 12:54
von erdbeerblut
Hallo bords0!

Es sah mir nach einem trivialen Übungsbeispiel aus. Ich habe die Dimension total unterschätzt.
Gerechnet hätte ich mit so etwa 50 Lösungen. Durch die Drehungen und Spiegelungen dann also 200 in der Ausgabe. Ich lasse gerade mal bis zum Ende durchlaufen, um zu sehen wie viele es wirklich sind...

Deine Lösung ist im Vergleich wirklich unglaublich schnell. - Danke! Die Lösungsidee ist einigermaßen klar, das hast du ja ganz gut beschrieben, aber wie das konkret umgesetzt wird, begreife ich nicht. Da muss ich mal intensiver einsteigen. Was genau ist da in X und Y hinterlegt? Die 'solve' Funktion arbeitet auch rekursiv, oder?

Wie würde man denn ein nicht-rechteckges Feld definieren? Interessant sind nämlich durchaus auch andere Formen, oder Auslassungen durch bereits platzierte Steine. Aber dazu kann ich ja die Vorversion nutzen, falls das nicht geht. Da würde sich auch die Zahl der Lösungen drastisch reduzieren. Es gibt auch Aufgabenstellungen mit nur einem vorbelegten Stein auf dem Brett, die dann nur noch eine einzige Lösung zur Vervollständigung haben! Unglaublich angesichts der riesigen Kombinationszahl für das leere Brett. (Aktuell sind hier knapp 3,7 Mio berechnet)

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Samstag 5. September 2020, 19:14
von bords0
Es macht ja auch Spaß, etwas auszuprobieren... Anbei ein Programm, mit dem man beliebige Felder und Steine vorgeben kann, und mit dem man auch "zu viele" Steine vorgeben kann, und dann nur ein Teil verwendet wird - da kann man dann herausfinden, ob es mit irgendwelchen Steinen eine Lösung gibt.

Leider keine Zeit mehr, um noch mehr zu erklären.

Code: Alles auswählen

import itertools
from collections import defaultdict

# Die Steine werden global definiert, auch die Liste aller Steine (weil praktisch)
# Ebenso das vollständige Feld
pink00 = [[0,0],[0,1],[1,1],[1,2],[1,3]]
lind01 = [[0,0],[0,1],[0,2],[1,0],[1,2]]
oran02 = [[0,0],[1,0],[1,1],[1,2],[2,1]]
blau03 = [[0,0],[0,1],[0,2],[1,0],[2,0]]
petr04 = [[0,0],[0,1],[0,2],[1,1]]
rot005 = [[0,0],[0,1],[0,2],[0,3],[1,0]]
mari06 = [[0,0],[0,1],[0,2],[1,0]]
mint07 = [[0,0],[0,1],[0,2],[1,0],[1,1]]
lila08 = [[0,0],[0,1],[1,1],[1,2],[2,2]]
gelb09 = [[0,0],[0,1],[0,2],[0,3],[1,1]]
wein10 = [[0,0],[0,1],[1,1],[1,2]]
azur11 = [[0,0],[0,1],[1,0]]

# Liste *aller* Steine
STEINE = [pink00, lind01, oran02, blau03, petr04, rot005, 
          mari06, mint07, lila08, gelb09, wein10, azur11]

FELD = set(itertools.product(range(11), range(5)))

# Liste der fehlenden Steine Beispiel 26
STEINE26 = [pink00, lind01, oran02, blau03, petr04, rot005, mari06]
# verbleibendes Spielfeld für Beispiel 26
FELD26 = {
                    (0, 2), (0, 3), (0, 4),
                            (1, 3), (1, 4), 
                            (2, 3), (2, 4),
                            (3, 3), (3, 4),
                    (4, 2), (4, 3), (4, 4),
                            (5, 3), (5, 4),
                            (6, 3), (6, 4),
            (7, 1), (7, 2), (7, 3), (7, 4),
                    (8, 2), (8, 3), (8, 4),
    (9, 0), (9, 1), (9, 2), (9, 3), (9, 4),
    (10,0), (10,1), (10,2), (10,3), (10,4)
}


# Algorithm X in 30 lines, von
# https://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html

def solve(X, Y, solution=[]):
    if not X:
        yield list(solution)
    else:
        # c = min(X, key=lambda c: len(X[c]))  # original
        # for r in list(X[c]):  # original
        for r in min(X.values(), key=len).copy():  # neu
            solution.append(r)
            cols = select(X, Y, r)
            for s in solve(X, Y, solution):
                yield s
            deselect(X, Y, r, cols)
            solution.pop()

def select(X, Y, r):
    cols = []
    for j in Y[r]:
        for i in X[j]:
            for k in Y[i]:
                if k != j:
                    X[k].remove(i)
        cols.append(X.pop(j))
    return cols

def deselect(X, Y, r, cols):
    for j in reversed(Y[r]):
        X[j] = cols.pop()
        for i in X[j]:
            for k in Y[i]:
                # if k != j:  # überflüssig, add fragt das schneller ab
                    X[k].add(i)
                    

def drehe_stein(stein):
    """Dreht den Stein um 90 Grad nach links"""
    return [(y, -x) for x, y in stein]

def spiegle_stein(stein):
    """Spiegelt den Stein durch Vertauschen von x und y"""
    return [(y, x) for x, y in stein]

def normiere_stein(stein):
    """Verschiebe den Stein in den Nullpunkt.
    
    Der Stein mit dem kleinsten x und dem kleinsten y wird nach 0 verschoben.
    """
    dx, dy = min(stein)
    return tuple(sorted((x - dx,y - dy) for x, y in stein))

def bewege_stein(stein, dx, dy):
    return tuple((x + dx, y + dy) for x, y in stein)

def erzeuge_varianten(stein):
    """Erstellt alle Variationen, den Stein abzulegen.
    
    Nur normierte Varianten.
    """
    varianten = [stein]
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    stein = spiegle_stein(stein)
    varianten.append(stein)
    for _ in range(3):
        stein = drehe_stein(stein)
        varianten.append(stein)
    return set(map(normiere_stein, varianten))


def aabb(plätze):
    """Berechne die "axis aligned bounding box" der angegebenen Plätze.
    
    Gib die Extremwerte (min und max) für x und y zurück.
    """
    x_min = min(xy[0] for xy in plätze)
    x_max = max(xy[0] for xy in plätze)
    y_min = min(xy[1] for xy in plätze)
    y_max = max(xy[1] for xy in plätze)
    
    return x_min, x_max, y_min, y_max
    
    
def zeige_feld(loesung, Y, feld_gesamt=()):
    """Stelle die (Teil-)Lösung als Zeichenkette dar.
    
    Für eine Teillösung kann das gesamte Feld mitgegeben werden,
    ggf. werden dann Punkte dargestellt.
    """
    # Das gesamte Feld wird erst mal mit Punkten dargestellt
    feld = dict.fromkeys(feld_gesamt, ".")
    # Stein 0 = A, Stein 1 = B, usw. überschreibt ggf. die Punkte
    for i, j, dx, dy in loesung:
        for xy in Y[i, j, dx, dy]:
            if isinstance(xy, tuple):  # die virtuellen Plätze wollen wir nicht sehen
                feld[xy] = chr(65 + i)
            
    x_min, x_max, y_min, y_max = aabb(feld)
    zeilen = ["".join(feld.get((x, y), " ") for x in range(x_min, x_max + 1))
                       for y in range(y_min, y_max + 1)]
                 
    return "\n".join(zeilen)


def zeige_benutze_steine(steine, loesung):
    """Stelle die benutzten und unbenutzten Steine dar."""
    
    benutzt = set(stein[0] for stein in loesung if stein[1] is not None)
    return "".join(chr(65 + i) if i in benutzt else "."
                   for i in range(len(steine)))


def berechne_bedingungen(steine=STEINE, feld=FELD, alle=True):
    """Berechne die Bedingungen X und Y.
    
    X enthält als Schlüssel die möglichen Plätze. Die Werte sind alle
    Steine (inkl. Bewegungen), die diesen Platz bedecken.
    
    Y ist genau andersherum: Die Schlüssel sind alle Steine (inkl. Bewegungen),
    die Werte sind alle Plätze, die davon bedeckt werden.
    
    Wird alle=False gesetzt, dann müssen nicht alle Steine verwendet werden.
    Das wird umgesetzt durch einen zusätzlich Stein, der nur den virtuellen Platz
    belegt.
    """

    X = defaultdict(set)
    # damit ein Platz auch dann in X vorhanden ist, wenn ihn kein Stein überdeckt
    for xy in feld:
        X[xy] = set()  
    Y = defaultdict(list)
    
    # stein: originaler Stein (unbewegt)
    # variante: auch gedreht und gespiegelt, aber nicht verschoben
    for i, stein in enumerate(steine):  # stein ist unbewegt
        if not alle:
            X[i].add((i, None, None, None))  # virtueller Platz i wird durch virtuellen Stein i belegt
            Y[(i, None, None, None)].append(i)
        for j, variante in enumerate(erzeuge_varianten(stein)):
            for dx, dy in feld:
                stein_bewegt = bewege_stein(variante, dx, dy)
                if not feld.issuperset(stein_bewegt):
                    continue
                # stein kann nur an einer Stelle sein, daher bekommen alle 
                # Varianten einen künstlichen überlappenden Platz dazu
                X[i].add((i, j, dx, dy))  # virtueller Platz i
                Y[i, j, dx, dy].append(i)
                for xy in stein_bewegt:
                    # Der Platz (x, y) wird vom Stein i in Variante j 
                    # mit Verschiebung (dx, dy) überdeckt
                    X[xy].add((i, j, dx, dy))  
                    Y[i, j, dx, dy].append(xy)
                        
    return X, Y


def str2feld(s):
    """Erzeuge aus einem string ein feld.
    
    (0, 0) = links oben.
    Erzeugt wird ein set.
    Felder sind mit "#" zu kennzeichnen.
    """
    feld = set()
    for y, line in enumerate(s.splitlines()):
        for x, c in enumerate(line):
            if c == "#":
                feld.add((x, y))
    return feld


def loese_und_zeige(steine, feld, max_loesungen=10, alle=False, zeige_benutzt=False):
    """Berechne die Bedingungen, Lösungen, und zeige sie.
    
    Höchstens max_loesungen Lösungen werden berechnet und gezeigt.
    Der Parameter alle wird durchgereicht.
    Ist zeige_benutzt=True, dann werden die benutzten Steine ausgegeben.
    """
    print(zeige_feld((), (), feld))
    X, Y = berechne_bedingungen(steine, feld, alle=alle)
    for n, loesung in enumerate(itertools.islice(solve(X, Y), max_loesungen), 1):
        print("Lösung", n)
        print(zeige_feld(loesung, Y, feld))
        print()
        if zeige_benutzt:
            print(zeige_benutze_steine(STEINE, loesung))
            print()


if __name__ == "__main__":
    # gesamtes Feld mit allen Steinen (Standardfall)
    print("Vollständige Lösung")
    X, Y = berechne_bedingungen()
    for loesung in itertools.islice(solve(X, Y), 10):
        print(zeige_feld(loesung, Y))
        print()

    # Beispiel 26
    print("Beispiel 26")
    loese_und_zeige(STEINE26, FELD26)

    # Beispiel 26, aber alle Steine dürfen verwendet werden
    print("Feld von Beispiel 26, aber alle Steine dürften verwendet werden")
    # "alle=False" muss angegeben werden, weil nicht alle Steine verwendet
    # werden müssen (können)
    loese_und_zeige(STEINE, FELD26, alle=False)
    
    # # Beispiel für eine besondere Form
    print("besondere Form")
    feldstr = """
      #########
    ####### ###
    ###########
      #########
    """
    feld = str2feld(feldstr)
    loese_und_zeige(STEINE, feld, alle=False)

    # Beispiel für eine andere besondere Form, dauert etwas länger
    # nur 2 Lösungen, und zwar mit den gleichen benutzen Teilen!
    print("besondere Form, nur zwei Lösungen")
    feldstr = """
      #########
    ####   ####
    ####   ####
      #########
    """
    feld = str2feld(feldstr)
    loese_und_zeige(STEINE, feld, alle=False, zeige_benutzt=True)

    # Beispiel für noch eine andere besondere Form - dauert noch länger
    print("Form mit vielen Lösungen, verschiedene benutzte Steine")
    feldstr = """
    ######
      ######
      ######
        ######
        ######
          ######
          ######
            ######
    """
    feld = str2feld(feldstr)
    loese_und_zeige(STEINE, feld, alle=False, max_loesungen=10, zeige_benutzt=True)

Re: Parameterübergabe in rekursiver Funktion

Verfasst: Sonntag 6. September 2020, 20:47
von bords0
Sorry, da ist noch ein Bug drin. Die Funktion solve aus dem Algorithm X verwendet ein mutable default, solution=[].
Wenn man nicht alle Lösungen ausgibt, dann ist solution nicht leer. solution enthält dann beim nächsten Aufruf von solve noch zusätzliche alte Werte, was spätestens bei der Ausgabe der benutzten Steine zu Fehlern führt.

Also muss man beim Aufruf von solve am besten immer selbst die leere Liste mit übergeben. Zum Beispiel in loese_und_zeige:

Code: Alles auswählen

def loese_und_zeige(steine, feld, max_loesungen=10, alle=False, zeige_benutzt=False):
    """Berechne die Bedingungen, Lösungen, und zeige sie.
    
    Höchstens max_loesungen Lösungen werden berechnet und gezeigt.
    Der Parameter alle wird durchgereicht.
    Ist zeige_benutzt=True, dann werden die benutzten Steine ausgegeben.
    """
    print(zeige_feld((), (), feld))
    X, Y = berechne_bedingungen(steine, feld, alle=alle)
    global loesung
    for n, loesung in enumerate(itertools.islice(solve(X, Y, []), max_loesungen), 1):
        print("Lösung", n)
        print(zeige_feld(loesung, Y, feld))
        print()
        if zeige_benutzt:
            print(zeige_benutze_steine(STEINE, loesung))
            print()


Re: Parameterübergabe in rekursiver Funktion

Verfasst: Sonntag 20. September 2020, 08:10
von erdbeerblut
Super. Vielen Dank!

Ich werde eine Weile brauchen, bis ich alles verstanden habe...