Wie finde ich die minimale Anzahl von Zügen, um einen Gegenstand an eine Position in einem Stapel zu bewegen?


12

Stapel

Wie kann ich bei einem Satz von NXP-Stapeln, wobei N die Anzahl der Stapel und P die Stapelkapazität ist, die minimale Anzahl von Swaps berechnen, die erforderlich sind, um von einem Knoten an Position A zu einem beliebigen Ort B zu gelangen? Ich entwerfe ein Spiel und das Endziel ist es, alle Stapel so zu sortieren, dass sie alle die gleiche Farbe haben.

# Let "-" represent blank spaces, and assume the stacks are
stacks = [
           ['R', 'R', 'R', 'R'], 
           ['Y', 'Y', 'Y', 'Y'], 
           ['G', 'G', 'G', 'G'], 
           ['-', '-', '-', 'B'], 
           ['-', 'B', 'B', 'B']
         ]

Wenn ich ein "B" stacks[1][1]so einfügen möchte, dass stacks[1] = ["-", "B", "Y", "Y"]. Wie kann ich die Mindestanzahl von Zügen bestimmen, die dazu erforderlich sind?

Ich habe mehrere Ansätze untersucht, ich habe genetische Algorithmen ausprobiert, die alle möglichen Bewegungen aus einem Zustand generieren, sie bewerten und dann die besten Bewertungspfade verfolgen. Ich habe auch versucht, den Djikstra-Algorithmus zur Pfadfindung für das Problem auszuführen . Es scheint frustrierend einfach zu sein, aber ich kann keinen Weg finden, es in etwas anderem als exponentieller Zeit zum Laufen zu bringen. Fehlt mir ein Algorithmus, der hier anwendbar ist?

Bearbeiten

Ich habe diese Funktion geschrieben, um die Mindestanzahl der erforderlichen Züge zu berechnen: Stapel: Liste der Zeichen, die die Teile im Stapel darstellen, Stapel [0] [0] ist die Spitze des Stapels [0] stack_ind: Der Index des Stapel, dem das Teil hinzugefügt werden soll need_piece: Das Teil, das dem Stapel hinzugefügt werden soll braucht_index: Der Index, in dem sich das Teil befinden soll

def calculate_min_moves(stacks, stack_ind, needs_piece, needs_index):
    # Minimum moves needed to empty the stack that will receive the piece so that it can hold the piece
    num_removals = 0
    for s in stacks[stack_ind][:needs_index+1]:
        if item != "-":
            num_removals += 1

    min_to_unlock = 1000
    unlock_from = -1
    for i, stack in enumerate(stacks):
        if i != stack_ind:
            for k, piece in enumerate(stack):
                if piece == needs_piece:
                    if k < min_to_unlock:
                        min_to_unlock = k
                        unlock_from = i

    num_free_spaces = 0
    free_space_map = {}

    for i, stack in enumerate(stacks):
        if i != stack_ind and i != unlock_from:
            c = stack.count("-")
            num_free_spaces += c
            free_space_map[i] = c

    if num_removals + min_to_unlock <= num_free_spaces:
        print("No shuffling needed, there's enough free space to move all the extra nodes out of the way")
    else:
        # HERE
        print("case 2, things need shuffled")

Bearbeiten: Testfälle auf Stapeln:

stacks = [
           ['R', 'R', 'R', 'R'], 
           ['Y', 'Y', 'Y', 'Y'], 
           ['G', 'G', 'G', 'G'], 
           ['-', '-', '-', 'B'], 
           ['-', 'B', 'B', 'B']
         ]

Case 1: stacks[4][1] should be 'G'
Move 'B' from stacks[4][1] to stacks[3][2]
Move 'G' from stacks[2][0] to stacks[4][1]
num_removals = 0 # 'G' is directly accessible as the top of stack 2
min_to_unlock = 1 # stack 4 has 1 piece that needs removed
free_spaces = 3 # stack 3 has free spaces and no pieces need moved to or from it
moves = [[4, 3], [2, 4]]
min_moves = 2
# This is easy to calculate
Case 2: stacks[0][3] should be 'B'
Move 'B' from stacks[3][3] to stack[4][0]
Move 'R' from stacks[0][0] to stacks[3][3]
Move 'R' from stacks[0][1] to stacks[3][2]
Move 'R' from stacks[0][2] to stacks[3][1]
Move 'R' from stacks[0][3] to stacks[3][0]
Move 'B' from stacks[4][0] to stacks[0][3]
num_removals = 0 # 'B' is directly accessible 
min_to_unlock = 4 # stack 0 has 4 pieces that need removed
free_spaces = 3 # If stack 3 and 4 were switched this would be 1
moves = [[3, 4], [0, 3], [0, 3], [0, 3], [0, 3], [4, 0]]
min_moves = 6
#This is hard to calculate

Die eigentliche Code-Implementierung ist nicht der Teil, der schwierig ist, sondern bestimmt, wie ein Algorithmus implementiert wird, der das Problem löst, mit dem ich zu kämpfen habe.

Gemäß der Anfrage von @ YonIif habe ich einen Kern für das Problem erstellt.

Wenn es ausgeführt wird, generiert es ein zufälliges Array der Stapel und wählt ein zufälliges Stück aus, das an einer zufälligen Stelle in einen zufälligen Stapel eingefügt werden muss.

Wenn Sie es ausführen, wird etwas von diesem Format auf die Konsole gedruckt.

All Stacks: [['-', '-', 'O', 'Y'], ['-', 'P', 'P', 'O'], ['-', 'P', 'O', 'Y'], ['Y', 'Y', 'O', 'P']]
Stack 0 is currently ['-', '-', 'O', 'Y']
Stack 0 should be ['-', '-', '-', 'P']

Status-Update

Ich bin sehr entschlossen, dieses Problem irgendwie zu lösen .

Beachten Sie, dass es Möglichkeiten gibt, die Anzahl der Fälle zu minimieren, z. B. die in den Kommentaren erwähnten Fälle von @Hans Olsson. Mein jüngster Ansatz für dieses Problem bestand darin, eine Reihe von Regeln zu entwickeln, die den genannten ähnlich sind, und sie in einem Generationsalgorithmus anzuwenden.

Regeln wie:

Mach niemals einen Zug rückgängig. Gehen Sie von 1-> 0 dann 0-> 1 (macht keinen Sinn)

Bewegen Sie niemals ein Stück zweimal hintereinander. Bewegen Sie sich niemals von 0 -> 1, dann von 1 -> 3

Bei einigen Bewegungen von Stapeln [X] zu Stapeln [Y], dann zu einer bestimmten Anzahl von Bewegungen, dann zu einer Bewegung von Stapeln [Y] zu Stapeln [Z], wenn sich die Stapel [Z] im selben Zustand befinden wie zu dem Zeitpunkt, als sie verschoben wurden Von den Stapeln [X] zu den Stapeln [Y] konnte eine Bewegung eliminiert werden, indem von den Stapeln [X] direkt zu den Stapeln [Z] gewechselt wurde.

Derzeit gehe ich dieses Problem mit dem Versuch an, genügend Regeln zu erstellen, um die Anzahl der "gültigen" Züge so gering wie möglich zu halten, damit eine Antwort mithilfe eines Generationsalgorithmus berechnet werden kann. Wenn sich jemand zusätzliche Regeln vorstellen kann, wäre ich daran interessiert, sie in den Kommentaren zu hören.

Aktualisieren

Dank der Antwort von @RootTwo hatte ich einen kleinen Durchbruch, den ich hier skizzieren werde.

Auf den Durchbruch

Definieren Sie die Zielhöhe als die Tiefe, in der das Zielstück im Zielstapel platziert werden muss.

Immer wenn ein Torstück auf Index <= Stack_Höhe - Zielhöhe platziert wird, gibt es über die Methode clear_path () einen kürzesten Weg zum Sieg.

Let S represent some solid Piece.

IE

Stacks = [ [R, R, G], [G, G, R], [-, -, -] ]
Goal = Stacks[0][2] = R
Goal Height = 2.
Stack Height - Goal Height = 0

Bei einem solchen Stapel stack[0] = Rist das Spiel gewonnen.

                       GOAL
[ [ (S | -), (S | -), (S | -) ], [R, S, S], [(S | - ), (S | -), (S | -)] ]

Da bekannt ist, dass immer mindestens Stack_Hight-Leerzeichen verfügbar sind, wäre der schlimmste Fall:

 [ [ S, S, !Goal ], [R, S, S], [-, -, -]

Da wir wissen, dass das Torstück nicht am Zielort sein kann oder das Spiel gewonnen ist. In diesem Fall wären mindestens die Züge erforderlich:

(0, 2), (0, 2), (0, 2), (1, 0)

Stacks = [ [R, G, G], [-, R, R], [-, -, G] ]
Goal = Stack[0][1] = R
Stack Height - Goal Height = 1

Bei einem solchen Stapel stack[1] = Rist das Spiel gewonnen.

              GOAL
[ [ (S | -), (S | -), S], [ (S | -), R, S], [(S | -), (S | -), (S | -)]

Wir wissen, dass mindestens 3 Leerzeichen verfügbar sind. Der schlimmste Fall wäre also:

[ [ S, !Goal, S], [S, R, S], [ -, -, - ]

In diesem Fall wäre die minimale Anzahl von Zügen die Züge:

(1, 2), (0, 2), (0, 2), (1, 0)

Dies gilt für alle Fälle.

Somit wurde das Problem auf das Problem reduziert, die minimale Anzahl von Zügen zu finden, die erforderlich sind, um das Torstück auf oder über der Zielhöhe zu platzieren.

Dies teilt das Problem in eine Reihe von Unterproblemen auf:

  1. Wenn der Zielstapel sein zugängliches Stück hat! = Zielstück, Bestimmen, ob es einen gültigen Ort für dieses Stück gibt oder ob das Stück dort bleiben soll, während ein anderes Stück getauscht wird.

  2. Wenn der Zielstapel sein zugängliches Stück == Zielstück hat, bestimmen Sie, ob es entfernt und auf der erforderlichen Zielhöhe platziert werden kann oder ob das Stück bleiben soll, während ein anderes getauscht wird.

  3. Wenn in den beiden oben genannten Fällen ein anderes Teil ausgetauscht werden muss, muss festgelegt werden, welche Teile ausgetauscht werden müssen, um die Zielhöhe zu erreichen.

Die Fälle des Zielstapels sollten immer zuerst ausgewertet werden.

IE

stacks = [ [-, R, G], [-, R, G], [-, R, G] ]

Goal = stacks[0][1] = G

Das Überprüfen des Zielstapels führt zuerst zu:

(0, 1), (0, 2), (1, 0), (2, 0) = 4 Moves

Ignorieren des Zielstapels:

(1, 0), (1, 2), (0, 1), (0, 1), (2, 0) = 5 Moves

2
Haben Sie A * ausprobiert ? Es ist dem Dijkstra-Algorithmus ziemlich ähnlich, aber manchmal ist es erheblich schneller.
Yonlif

1
Kannst du bitte einen Github Repo Link teilen? Ich würde gerne selbst experimentieren, wenn es in Ordnung ist. @ Tristen
Yonlif

1
Nach einem ersten Blick scheint dieses Problem NP-schwer zu sein. Es liegt wahrscheinlich nicht innerhalb von NP (nicht NP-vollständig), denn selbst wenn ich Ihnen eine optimale Lösung gebe, können Sie dies nicht einfach überprüfen. Dies ist bekannt für Optimierungsprobleme bei Permutationen. Ich würde vorschlagen, das Problem bei CS zu veröffentlichen . Sehen Sie sich die Approximationsalgorithmen für dieses Problem an. Dies ist ein ziemlich schwieriges Problem, aber es sollte eine anständige Annäherung geben. Dies ist ähnlich: Arbitrary Towers of Hanoi
DarioHett

1
@DarioHett Das war es, worüber ich mir Sorgen machte! Ich drückte die Daumen, dass es kein NP-hartes Problem werden würde, aber ich hatte auch das Gefühl, dass es eines sein könnte. Ich hatte besseres Glück mit einem genetischen Algorithmus und einigen speziellen Bewertungsfunktionen, die die Züge bewerten. Ich werde einen Blick auf die Arbitrary Towers von Hanoi werfen! Danke für den Vorschlag.
Tristen

1
Wenn Sie versuchen, das Puzzle zufällig zu generieren, denken Sie daran, offensichtlich redundante Bewegungen zu entfernen (etwas nach einer Vorwärtsbewegung zurück zu bewegen oder eine Bewegung in zwei Schritten auszuführen, wenn dies ausreichen würde; und auch in Kombination mit möglicherweise nicht verwandten Bewegungen, die eingemischt sind).
Hans Olsson

Antworten:


1

Ich habe mir zwei Optionen ausgedacht, aber keine von ihnen ist in der Lage, Fall 2 rechtzeitig zu lösen. Die erste Option verwendet A * mit einem Zeichenfolgenabstandsmaß als h (n), die zweite Option ist IDA *. Ich habe viele Saitenähnlichkeitsmaße getestet und bei meinem Ansatz Smith-Waterman verwendet. Ich habe Ihre Notation geändert, um das Problem schneller zu behandeln. Ich habe am Ende jeder Ziffer Zahlen hinzugefügt, um zu überprüfen, ob ein Stück zweimal bewegt wurde.

Hier sind die Fälle, an denen ich getestet habe:

start = [
 ['R1', 'R2', 'R3', 'R4'], 
 ['Y1', 'Y2', 'Y3', 'Y4'], 
 ['G1', 'G2', 'G3', 'G4'], 
 ['B1'], 
 ['B2', 'B3', 'B4']
]

case_easy = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G'], 
 ['B', 'B'], 
 ['B', 'B', 'G']
]


case_medium = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G', 'G'], 
 ['B'],
 ['B', 'B', 'G', 'Y']
]

case_medium2 = [
 ['R', 'R', 'R' ], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G' ], 
 ['B', 'R', 'G'],
 ['B', 'B', 'G', 'Y']
]

case_hard = [
 ['B'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G', 'G'], 
 ['R','R','R', 'R'], 
 ['B','B', 'B']
]

Hier ist der A * -Code:

from copy import deepcopy
from heapq import *
import time, sys
import textdistance
import os

def a_star(b, goal, h):
    print("A*")
    start_time = time.time()
    heap = [(-1, b)]
    bib = {}
    bib[b.stringify()] = b

    while len(heap) > 0:
        node = heappop(heap)[1]
        if node == goal:
            print("Number of explored states: {}".format(len(bib)))
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            return rebuild_path(node)

        valid_moves = node.get_valid_moves()
        children = node.get_children(valid_moves)
        for m in children:
          key = m.stringify()
          if key not in bib.keys():
            h_n = h(key, goal.stringify())
            heappush(heap, (m.g + h_n, m)) 
            bib[key] = m

    elapsed_time = time.time() - start_time
    print("Execution time {}".format(elapsed_time))
    print('No Solution')

Hier ist der IDA * Code:

#shows the moves done to solve the puzzle
def rebuild_path(state):
    path = []
    while state.parent != None:
        path.insert(0, state)
        state = state.parent
    path.insert(0, state)
    print("Number of steps to solve: {}".format(len(path) - 1))
    print('Solution')

def ida_star(root, goal, h):
    print("IDA*")
    start_time = time.time()
    bound = h(root.stringify(), goal.stringify())
    path = [root]
    solved = False
    while not solved:
        t = search(path, 0, bound, goal, h)
        if type(t) == Board:
            solved = True
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            rebuild_path(t)
            return t
        bound = t

def search(path, g, bound, goal, h):

    node = path[-1]
    time.sleep(0.005)
    f = g + h(node.stringify(), goal.stringify())

    if f > bound: return f
    if node == goal:
        return node

    min_cost = float('inf')
    heap = []
    valid_moves = node.get_valid_moves()
    children = node.get_children(valid_moves)
    for m in children:
      if m not in path:
        heappush(heap, (m.g + h(m.stringify(), goal.stringify()), m)) 

    while len(heap) > 0:
        path.append(heappop(heap)[1])
        t = search(path, g + 1, bound, goal, h)
        if type(t) == Board: return t
        elif t < min_cost: min_cost = t
        path.pop()
    return min_cost

class Board:
  def __init__(self, board, parent=None, g=0, last_moved_piece=''):
    self.board = board
    self.capacity = len(board[0])
    self.g = g
    self.parent = parent
    self.piece = last_moved_piece

  def __lt__(self, b):
    return self.g < b.g

  def __call__(self):
    return self.stringify()

  def __eq__(self, b):
    if self is None or b is None: return False
    return self.stringify() == b.stringify()

  def __repr__(self):
    return '\n'.join([' '.join([j[0] for j in i]) for i in self.board])+'\n\n'

  def stringify(self):
    b=''
    for i in self.board:
      a = ''.join([j[0] for j in i])
      b += a + '-' * (self.capacity-len(a))

    return b

  def get_valid_moves(self):
    pos = []
    for i in range(len(self.board)):
      if len(self.board[i]) < self.capacity:
        pos.append(i)
    return pos

  def get_children(self, moves):
    children = []
    for i in range(len(self.board)):
      for j in moves:
        if i != j and self.board[i][-1] != self.piece:
          a = deepcopy(self.board)
          piece = a[i].pop()
          a[j].append(piece)
          children.append(Board(a, self, self.g+1, piece))
    return children

Verwendungszweck:

initial = Board(start)
final1 = Board(case_easy)
final2 = Board(case_medium)
final2a = Board(case_medium2)
final3 = Board(case_hard)

x = textdistance.gotoh.distance

a_star(initial, final1, x)
a_star(initial, final2, x)
a_star(initial, final2a, x)

ida_star(initial, final1, x)
ida_star(initial, final2, x)
ida_star(initial, final2a, x)

0

In den Kommentaren sagten Sie, es gibt N Stapel mit der Kapazität P und es gibt immer P leere Räume. Wenn dies der Fall ist, scheint dieser Algorithmus in der elseKlausel in Ihrem Code zu funktionieren (dh wann num_removals + min_to_unlock > num_free_spaces):

  1. Suchen Sie das gewünschte Stück, das der Oberseite eines Stapels am nächsten liegt.
  2. Bewegen Sie alle Teile von oben so über das gewünschte Teil, dass sich ein Stapel (nicht der Zielstapel) mit einem leeren Feld oben befindet. Verschieben Sie bei Bedarf Teile vom Zielstapel oder einem anderen Stapel. Wenn der einzige offene Bereich die Oberseite des Zielstapels ist, bewegen Sie ein Stück dorthin, um die Oberseite eines anderen Stapels zu öffnen. Dies ist immer möglich, da es P offene Räume und höchstens P-1 Teile gibt, die sich über dem gewünschten Teil bewegen können.
  3. Bewegen Sie das gewünschte Stück an die leere Stelle auf einem Stapel.
  4. Verschieben Sie Teile vom Zielstapel, bis das Ziel geöffnet ist.
  5. Bewegen Sie das gewünschte Stück zum Ziel.

Ich habe die letzten paar Stunden damit verbracht, mich mit dieser Antwort zu befassen, und ich denke, da könnte etwas sein. Könnten Sie, wenn möglich, etwas mehr Informationen darüber geben, wie Sie die Teile bewegen würden, die über dem gewünschten Teil liegen? Wie bestimmen Sie, in welche Stapel sie verschoben werden sollen? Vielleicht ein bisschen Pseudocode / Code. Dies ist definitiv das, was ich bisher am nächsten an der Lösung gefühlt habe.
Tristen

0

Obwohl ich nicht die Zeit gefunden habe, dies mathematisch zu beweisen, habe ich mich trotzdem entschlossen, dies zu posten. ich hoffe es hilft. Der Ansatz besteht darin, einen Parameter p zu definieren, der mit guten Zügen abnimmt und genau nach Erreichen des Spiels Null erreicht. Berücksichtigt im Programm nur gute oder neutrale Züge (die p unverändert lassen) und vergisst schlechte Züge (die p erhöhen).

Also, was ist p? Definieren Sie für jede Spalte p als die Anzahl der Blöcke, die noch entfernt werden müssen, bevor alle Farben in dieser Spalte die gewünschte Farbe haben. Nehmen wir also an, wir möchten, dass die roten Blöcke in der Spalte ganz links landen (darauf komme ich später zurück), und nehmen wir an, dass sich unten ein roter Block befindet, dann ein gelber darüber und ein weiterer Block darüber das und dann ein leerer Raum. Dann ist p = 2 für diese Spalte (zwei zu entfernende Blöcke, bevor alle rot sind). Berechnen Sie p für alle Spalten. Für die Spalte, die leer bleiben soll, entspricht p der Anzahl der darin enthaltenen Blöcke (alle sollten gehen). P für den aktuellen Zustand ist die Summe aller ps für alle Spalten.

Wenn p = 0 ist, haben alle Spalten die gleiche Farbe und eine Spalte ist leer, sodass das Spiel beendet ist.

Durch die Auswahl von Bewegungen, die p verringern (oder zumindest nicht erhöhen), bewegen wir uns in die richtige Richtung. Dies ist meiner Meinung nach der entscheidende Unterschied zu Algorithmen mit kürzesten Pfaden: Dijkstra hatte keine Ahnung, ob er sich mit jedem in die richtige Richtung bewegt Scheitelpunkt, den er untersuchte.

Wie bestimmen wir also, wo jede Farbe landen soll? Grundsätzlich durch Bestimmung von p für jede Möglichkeit. Beginnen Sie also zB mit rot / gelb / grün / leer, berechnen Sie p, gehen Sie dann zu rot / gelb / leer / grün, berechnen Sie p usw. Nehmen Sie die Startposition mit dem niedrigsten p ein. Das dauert n! Berechnungen. Für n = 8 ist dies 40320, was machbar ist. Die schlechte Nachricht ist, dass Sie alle Startpositionen mit dem gleichen niedrigsten p untersuchen müssen. Die gute Nachricht ist, dass Sie den Rest vergessen können.

Hier gibt es zwei mathematische Unsicherheiten. Erstens: Ist es möglich, dass es einen kürzeren Weg gibt, der einen schlechten Zug verwendet? Scheint unwahrscheinlich, ich habe kein Gegenbeispiel gefunden, aber ich habe auch keinen Beweis gefunden. Zweitens: Ist es möglich, dass beim Starten mit einer nicht optimalen Startposition (dh nicht dem niedrigsten p) ein kürzerer Weg als bei allen optimalen Startpositionen vorhanden ist? Nochmals: kein Gegenbeispiel, aber auch kein Beweis.

Einige Implementierungsvorschläge. Das Verfolgen von p während der Ausführung für jede Spalte ist nicht schwierig, sollte aber natürlich durchgeführt werden. Ein weiterer Parameter, der für jede Spalte beibehalten werden sollte, ist die Anzahl der offenen Stellen. Wenn 0, können diese Spalten momentan keine Blöcke akzeptieren und können daher aus der Schleife herausgelassen werden. Wenn p = 0 für eine Spalte ist, ist sie nicht für einen Pop geeignet. Prüfen Sie für jeden möglichen Pop, ob es einen guten Zug gibt, dh einen, der das Gesamt-p verringert. Wenn es mehrere gibt, überprüfen Sie alle. Wenn es keine gibt, berücksichtigen Sie alle neutralen Bewegungen.

All dies sollte Ihre Rechenzeit erheblich verkürzen.


1
Ich denke, Sie haben die Frage falsch verstanden! Obwohl dies die Motivation hinter der Frage ist. Die Frage ist, die Mindestanzahl von Zügen zu finden, um ein einzelnes Stück an einen einzelnen Ort zu bewegen. Die Frage war nicht, die Mindestanzahl von Zügen zum Sortieren der Stapel zu finden, obwohl dies die Motivation hinter der Frage ist. Mit dieser Bewertung von P wären Sie jedoch falsch. Es gibt viele Fälle, in denen es "schlechte Bewegungen" gibt, die P zuerst erhöhen und später schneller verringern. Wenn dies gesagt ist, lesen Sie die Frage vielleicht noch einmal durch, da Ihre Antwort keine Relevanz hat.
Tristen

1
Ich entschuldige mich, Tristen, ich habe die Frage tatsächlich nicht sorgfältig gelesen. Ich war fasziniert von dem mathematischen Aspekt und da ich zu spät zur Party kam, war ich zu schnell, um zu antworten. Ich werde das nächste Mal vorsichtiger sein. Hoffentlich finden Sie eine Antwort.
Paul Rene
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.