Clonen funktioniert nicht

Hallo,

ich möchte ein Problem aus dem Spiel Schach lösen und muss dafür eine eigene Klasse Schachbrett clonen.
Das versuche ich so:

	protected Schachbrett clone() {
		try {
			return (Schachbrett) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			return null;
		}
	}```

Habe natürlich auch `implements Cloneable` gesetzt.

Wenn ich dann das hier mache:
```Schachbrett s = new Schachbrett();
		Schachbrett s1 = s.clone();```

Erhalte ich aber trotzdem das gleiche Objekte nur mit Referenz. Was mache ich falsch?

Du solltest die Attribute ‘manuell’ kopieren in zB einer Methode oder Konstruktor.

Was mit “das gleiche Objekte nur mit Referenz” gemeint ist, müßtest du ggf. genauer sagen. Allgemein ist “clone” ziemlich hakelig. Josh Bloch empfielt in “Effective Java”, es nur zurückhaltend einzusetzen. Einige Caveats findet man in AngelikaLanger.com - Implementing the clone() Method - Part 1 - Angelika Langer Training/Consulting

Hast du schon Alternativen (wie etwa einen “Copy-Konstruktor” oder eine eigene “copy()”-Methode) in Betracht gezogen? Das reicht “meistens” aus…

soviel Code schon da fehlt doch nur noch die Klassendefinition
(inklusive des selbst davon schon angesprochenden implements Cloneable)

sowie die main-Klasse/ Methode mit nur noch einer wesentlichen neuen Code-Zeile,
wobei die auch schon angesprochen: Vergleich der Objekte

	public static void main(String[] args) {
		Schachbrett s = new Schachbrett();
		Schachbrett s1 = s.clone();
		System.out.println(s == s1);
	}
}

class Schachbrett implements Cloneable {
	@Override
	protected Schachbrett clone() {
		try {
			return (Schachbrett) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			return null;
		}
	}
}

und vollkommen selbstverständlich sind diese Objekte natürlich nicht dieselben, sondern unterschiedlich,

etwas unklar im ersten Posting, ob soweit auch schon klar und es nur an anderen Faktoren im Programm liegt, warum bei dir nicht,
aber da kann man blind ja nicht unbedingt viel raten,
von einer typischen Ursache für so ein Verhalten ist mir zumindest nichts bekannt, da es ja auch gar nicht als Möglichkeit anzunehmen ist

vielleicht doch nochmal überprüfen, ob es nicht schon funktioniert, oder vollständiges Test-Programm mit nachvollziehbaren Fehler posten,

falls das Problem nur darin besteht, dass die inneren Variablen denselben Inhalt haben,
hilft vielleicht alternativ das Konzept Deep Copy
Tiefe Objektkopien

************Hallihallöle,

bei meinem Projekt hat das bisher nicht funktioniert. Ganz seltsam.
Also zu meinem eigentlichen Problem, vielleicht kann der Fehler ja auch woanders liegen:
Ich möchte das Springerproblem angehen. Hat nichts mit Axel Springer zu tun, der ja auch immer für Probleme sorgt(e). Man hat ein Schachbrett mit xy Feldern und stellt irgendwo einen Springer auf. Innerhalb der nächsten xy-1 Züge soll er einmal das gesamte Feld ablaufen und dabei jedes Feld genau einmal berühren.

Ich mache das nun, indem ich auf alle x*y Felder eine Figur stelle und dann immer in alle Richtungen in welche sie gehen könnte ein neues Objekt, also sozusagen einen neuen Zweig aufmache.
Das heißt also, wenn ich irgendwo in der Mitte stehe und gerade ein Schachbrett mit Zugkombinationen habe, dann kann ich von dort aus in 8 Richtungen gehen und muss dieses Schachbrett-Objekt verachtfachen. Nicht nur die Referenz darauf. Und offensichtlich ging das bisher irgendwie nicht.

Also mal so nebenbei die Anmerkung: In der Theorie sind das bei einem 8x8 Feld, wenn man davon ausgeht, dass das Feld keinen Rand hätte 64x8^64 Kombinationsmöglichkeiten, also eine riesige Menge, die kein PC dieser Welt verarbeiten kann. Dem bin ich mir bewusst. Ich würde das dann auch auf einem kleineren Feld probieren. In der Theorie macht er aber nur insgesamt 2224 Durchgänge, was ich bisher auf diese Clone-Sache bezog.

Also ich hab mal mein Repository hochgeladen, hier könnt ihr es ansehen:

Ich habe drei Klassen. Springerproblem, Einzelfeld und Schachbrett.
In Springerproblem steuere ich das Problem selbst, Schachbrett generiert Schachbrett und speichert die Züge ab und Einzelfeld ist ein Feld auf einem Schachbrett.

Wichtig für das Problem ist denke ich mal dann die Methode Springerproblem.naechsteReihe()

Verurteilt mich nicht für meine komplizierte Denkweise. Geht bestimmt alles auch einfacher und kürzer. :smiley:
Kann jemand ein Problem in meinem Denken erkennen?

Schönen Sonntag!
Lukas

====
Wichtige Ergänzung:
Ich lasse mir in der Konsole die „Runden“ ausgeben. Also jedes Schachbrett muss ja x*y Durchläufe also Spielzüge haben.
Die Ausgabe „Runde: 1“ kommt genau 64 mal vor in meiner Konsole, das heißt, er hat für jedes bereits am Anfang existierende Objekt genau einen Durchlauf. Es ist also meiner Meinung nach sicher, dass es am falschen Clonen liegt.

Naja, zur eigentlichen Frage nach dem „clone“: Du kopierst ja die Felder nicht. Die Arrays im geklonten Schachbrett sind danach dieSELBEN (!) wie im original. Die Arrays müssen nach dem super.clone() noch händisch kopiert werden.

Ansonsten, allgemein: Wenn Code so repetitiv aussieht, wie der in „naechsteReihe“, sollte man sich überlegen, ob man das nicht verbessern kann :wink:

NOCH allgemeiner: Eigentlich muss man die Felder nicht klonen, außer, wenn man aus irgendeinem Grund immer alle Kombinationen im Speicher halten will (was wegen der schieren Menge ohnehin nicht geht). Das Springerproblem (und ähnliche Probleme) löst man üblicherweise mit Backtracking. Und dazu braucht man nur EIN Spielbrett (und tatsächlich nur wenige Zeilen Code). Aber auch das wird spätestens bei einem 8x8-Feld zu langsam, wegen der expontentiellen Laufzeit. Um das bei 8x8 und mehr Feldern noch „schnell“ zu lösen, braucht man dann die Warnsdorffregel : Springerproblem – Wikipedia

Für Klonen gilt das gleiche wie für Regex: Wer damit ein Problem lösen will, hat danach zwei.

Klonen umgeht den normalen Objekterzeugungsmechanismus, deshalb sollte man wirklich einen guten Grund haben, es zu nutzen, und wissen, was man tut. Und der einziger Grund, der mir einfällt, wäre Performance - aber auch nur unter ganz spezifischen Voraussetzungen, die hier nicht gegeben sind.

Alternativen wurden schon genannt, üblicherweise ein Copy-Konstruktor. Und es ist auch zu prüfen, ob man wirklich kopieren muss, Stichwort “unveränderliche Objekte”.

Nach clone() wahrscheinlich nur referentielle Gleichheit der Attribute, was bei zwei gleichen oder ähnlichen Brettern für das Springerproblem (das gibt es auch schon ewig…) whrs. nicht gewollt ist imo.

Dh, veränderst du einen Springer (1) auf Brett A, verändert sich dieser auch auf Brett B - nicht gewollt.

clone() ist eigentlich ein Überbleibsel und sollte, nach den ErfindernInnen, nicht verwendet werden, wie angemerkt.

Grüße

Hallöle ihr alle,
also das mit dem Clonen hab ich nun gelassen. Ich danke euch für eure Beratung dazu. War auch irgendwie eine ungünstige Idee, die mich da getrieben hat.

Nun möchte ich aber noch einmal auf das Ursprungsproblem des Projekts zurückkommen.
Ich bin dabei das Springerproblem zu lösen.

Ich habe zwei ganz interessante Seiten gefunden.
Einerseits diese Seite hier, die mehr als ausführlich alle möglichen brillanten Sachen festhält und welche auch diese Behauptung aufgestellt hat:

Nebenbei sprang so noch ein Verfahren dabei heraus, das erlaubt, z.B. ein 100.000x100.000 oder ein 100.000.000x123.456.789-Brett innerhalb einer Sekunde zu berechnen - vorausgesetzt, sie haben einen langsamen Rechner.

Das Springerproblem

Das Programm selbst konnte ich leider nicht austesten, weil es sich um eine Windows-Exe handelte. Naja, die Theorie reichte ja auch erstmal. Viele interessante Sachen dabei, die man umsetzen kann.

Dann noch eine zweite Sache: Jemand hat eine interessante Lösung in Java gepostet, auch mit graphischer Oberfläche:

Und noch eine weitere in Java:

(die gesamte Seite mit vielen weiteren Vorschlagen ist diese: Springerproblem | [HaBo] |ich verstehe nur leider die anderen Programmiersprachen nicht so gut)

Ohne sie jetzt ins Detail studiert zu haben, sie arbeiten beide bis 6x6 Feldern sehr akkurat und schnell, was aber auch daran liegt, dass bis 6x6 generell alles schnell lösbar ist. Schon bei 8x8 saß ich vor dem PC und habe es abbrechen müssen, weil kein Ergebnis kam. Bei beiden.

Das erste arbeitet etwas stark amateuerhaft, auch was die Objektorientierung angeht.

Sie scheinen aber beide die Warnsdorffregel nicht zu verwenden, es scheint eher, dass sie das Backtracking-Verfahren anwenden, wo man dann bei falschem Zug immer zurücksetzt. Sehr ungünstig eigentlich, hab ich ja dann am Ergebnis gesehen.

Ich für meinen Teil möchte nun, auf Grundlage des Designs von dem Projekt da oben ein Programm entwickeln und auch einen gescheiten Algorithmus finden, der dem mir vorenthaltenen Exe-Teil da oben ähnelt.

Bisher hab ich das hier hochgeladen: GitHub - Dabendorf/Java-Springerproblem: Dies ist ein Lösungsansatz zum Springerproblem.

Schöne Grüße
Lukas

OK, ich hätte wohl doch nicht um 1:48 damit anfangen sollen, jetzt bin ich doch etwas müde, aber… hab’ mal schnell was hingehackt, wo man durch ein/auskommentieren der magischen Zeile
applyWarnsdorffRule(targets);
sehen kann, wie wichtig diese Regel ist. Für “ausgefeiltere” Lösungen (speziell Divide-And-Conquer) wird man sich aber Strukturen ausdenken müssen, die über einen rohen int-Array hinausgehen.

package bytewelt.knights;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class KnightsTourMain
{
    public static void main(String[] args)
    {
        KnightsTourSolver k = new KnightsTourSolver(8, 8);
        k.solve();
    }
}

class KnightsTourSolver
{
    private final int sizeX;
    private final int sizeY;
    private final int board[];
    private final Point deltas[] =
    {
        new Point(-1,  2),
        new Point( 1,  2),
        new Point(-1, -2),
        new Point( 1, -2),
        new Point(-2,  1),
        new Point( 2,  1),
        new Point(-2, -1),
        new Point( 2, -1),
    };
    
    public KnightsTourSolver(int sizeX, int sizeY)
    {
        this.sizeX = sizeX;
        this.sizeY = sizeY;
        this.board = new int[sizeX*sizeY];
    }
    
    private int get(int x, int y)
    {
        return board[x+y*sizeX];
    }
    
    private void set(int x, int y, int n)
    {
        board[x+y*sizeX] = n;
    }
    
    private boolean isValid(int x, int y)
    {
        return x >= 0 && x < sizeX && y >= 0 && y < sizeY; 
    }
    
    private List<Point> computeTargets(int x, int y)
    {
        List<Point> targets = new ArrayList<Point>();
        for (Point delta : deltas)
        {
            int tx = x + delta.x;
            int ty = y + delta.y;
            if (isValid(tx, ty) && get(tx, ty) == 0)
            {
                targets.add(new Point(tx, ty));
            }
        }
        return targets;
    }
    
    boolean solve()
    {
        for (int x=0; x<sizeX; x++)
        {
            for (int y=0; y<sizeY; y++)
            {
                boolean solved = solve(x, y, 1);
                if (solved)
                {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean solve(int currentX, int currentY, int step)
    {
        if (step == sizeX * sizeY)
        {
            set(currentX, currentY, step);
            System.out.println("After step "+step);
            print();
            return true;
        }
        set(currentX, currentY, step);
        
        System.out.println("After step "+step);
        print();
        
        List<Point> targets = computeTargets(currentX, currentY);
        
        applyWarnsdorffRule(targets);
        
        for (Point target : targets)
        {
            boolean solved = solve(target.x, target.y, step+1);
            if (solved)
            {
                return true;
            }
        }
        set(currentX, currentY, 0);
        return false;
    }
    
    private void applyWarnsdorffRule(List<Point> targets)
    {
        Map<Point, Integer> targetCounters = new HashMap<Point, Integer>();
        for (Point target : targets)
        {
            List<Point> nextTargets = computeTargets(target.x, target.y);
            targetCounters.put(target, nextTargets.size());
        }
        Collections.sort(targets, new Comparator<Point>()
        {
            @Override
            public int compare(Point p0, Point p1)
            {
                Integer c0 = targetCounters.get(p0);
                Integer c1 = targetCounters.get(p1);
                return Integer.compare(c0, c1);
            }
        });
    }
    
    
    private void print()
    {
        StringBuilder sb = new StringBuilder();
        for (int y=0; y<sizeY; y++)
        {
            for (int x=0; x<sizeX; x++)
            {
                int v = get(x,y);
                if (v == 0)
                {
                    sb.append(" .. ");
                }
                else
                {
                    sb.append(String.format(" %2d ", v));
                }
            }
            sb.append("
");
        }
        System.out.println(sb.toString());
    }
    
    
}

Moin Marco, leider dauert das länger, als man denkt. Ich muss auch soetwas ähnliches machen. Kannst du Zeile 75 bis 89 knapp erklären? Wenn nicht solve() true liefert, wird dann wieder zurückgesetzt? Wäre das dynamische Programmierung? Es ist ja kein rekursiver Aufruf. Müssen für alle Möglichkeiten solve() aufgerufen werden? Danke für deine Erklärung.

In der solve-Methode ab Zeile 75 probiert er lediglich alle Startfelder durch. Nur der Vollständigkeit halber (er wird immer bei solve(0,0,1) eine Lösung finden, aber das “weiß man ja nicht”. Man könnte auch direkt die solve(x,y,1)-Methode aufrufen, wenn man sagen wollte “starte bei Feld (x,y)”.)

Diese andere solve-Methode danach ist dann die rekursive, und macht schlicht und einfach backtracking (nur eben mit der optionalen Warnsdorff-Regel).

Dankeschön. Jetzt hab ich’s auch gerallt. applyWarnsdorffRule() <=> Umsortierung der potentiellen Figuren (in optimale order), kann bei Bedarf auskommentiert werden. Aber wie würdest du es komplett iterativ formulieren? Iterativ ist doch oft gefragt, bei dieser Übung.

Eine Rekursion in eine Iteration umformulieren ist weitgehend mechanisch möglich. Lapidar: Einfach alle Methodenargumente auf einen eigenen Stack legen. (Natürlich kann’s im Einzelfall dann etwas fummelig sein, aber an sich ist das Muster immer das gleiche).

Hallöle,

das um 1:48 Uhr zu programmieren hat sich auf jeden Fall gelohnt. Es funktioniert nicht nur, Du hast mir auch ein verständliches Beispiel geliefert, was ich verstehen kann. Eigentlich doch recht einfach gemacht, gefällt mir. Bei kleineren Brettern funktioniert es auf jeden Fall. Bei größeren Feldern müsste ich noch mir noch überlegen, wie ich die Idee mit der Einteilung in kleinere (oft 5x5 große) Felder einbaue. Mal sehen, was ich da so finde.

Das Ursprungsproblem ist auf jeden Fall gelöst! Vielen Dank!

Schönes Wochenende
Lukas

PS: Stichwort Geschlossene Pfade. Ich hab erst gedacht, das würde auch geschlossene Pfade lösen. Ich hab drei Beispiele probiert und immer waren das Anfangs- und das Endfeld einen Sprung entfernt. Habe aber dann im Quellcode keinerlei Hinweise dazu gefunden und dann gesehen, aha, ich hab nur richtig zufällige Beispiele gehabt. :smiley: Es interessiert mich an dieser Stelle, wie es kommt, dass das mit dem Algorithmus doch in, geschätzt 50% der Fällen geschlossene Pfade erzeugt. Aber das ist dann wohl eher eine mathematische Frage, bei eigentlich mehreren Millionen Lösungsvarianten auf einem 8x8 Feld.

Um 4 Uhr ist man am produktivsten. :wink:

Einteilung in kleinere… sollten doch nur die Grenzen überprüft werden.

Btw. Alle Springer auf 8- oder 1-Linie zu stellen, wäre nicht zulässig, oder - damit sie sich nicht gegenseitig schlagen können?

Naja. Da jede Lösung über alle Felder führt, ist es völlig egal, wo du anfängst.

Das ist leider nicht ganz richtig. Es gibt sogenannte Offene und Geschlossene Pfade. Bei den geschlossen Pfaden, die natürlich auch schwieriger zu Programmieren sind, ist das erste Feld einen Springerzug vom Letzten Feld entfernt.
Ausgehend davon ist Deine Aussage natürlich völlig richtig.

So weit ich das überblickt habe ist Marco13s Lösung jedoch eine mit offenen Pfaden.

Ah, wenn es keine geschlossenen sind, dann ist es natürlich durchaus entscheidend, von wo begonnen wird. Oder könnte entscheidend sein. Zumindest sind Lösungen mit anderen Startfeldern dann definitiv andere Lösungen.

Hmja, geschlossene Pfade könnten ganz interessant sein. Die einzubauen ist nicht sooo schwer, und er findet sie auch sehr schnell.

Etwas anderes wäre wohl wichtig, wenn man das ganze für größere Boards mit Divide And Conquer lösen will: Das große Board muss ja in kleinere aufgeteilt werden, und ich denke, da könnte es hilfreich sein, wenn man Start- und Endpunkt eines Pfades angeben kann. Ein schneller Versuch, das einzubauen, zeigt: Das wird wohl schwieriger. (Man müßte sich auch mal einlesen, ob das denn immer (!?) mit beliebigen Start/Endpunkten (!?) möglich ist…)