@Stiller Leser
Danke für deinen Java-Code. Weil ich mit C++ angefangen habe, kann ich ihn jetzt nicht verwenden, vielleicht später mal. 
Zuerst hatte ich mir überlegt, das auf der Basis von Mengen zu {1...9} je einzelnem Feld zu lösen, aus denen zuerst die unmöglichen Kandidaten naiv ausgeschlossen und dann nötigenfalls recht komplexe Regeln angewendet werden sollten, evtl. auch unter Berücksichtigung von Wahrscheinlichkeiten. Ein Durchprobieren sollte nachrangig stattfinden.
Um die Mengen performant zu behandeln, hatte ich mir dafür (in C++) eine Klasse erstellt, welche mit Bitmasken arbeitet, mit einem Bit je Kandidat, was also mindestens neun Bit erfordert und was noch Platz für weitere Informationen ließe. Eine zusätzliche Bit-Operation ist nämlich sehr viel schneller als die sonst nötigen zusätzlichen Lade- oder Speicheroperationen.
Dann ist mir aber der Aufwand schon beim bloßen Gedanken zu groß geworden, was mir eigentlich von vornherein klar war, aber ich ließ mich ein wenig verführen, was auch mit daran lag, dass hier noch anfänglich von einer ziemlich langen Zeit zum Lösen die Rede war, was einen komplexeren Wurf evtl. hätte rechtfertigen können.
Als ich von dem Ansatz von Whiz-zarD las, dachte ich mir, dass man damit bei nicht allzu schweren Sudokus zu genügend kurzen Zeiten gelangen müsste, weil man nämlich wegen der geringen Komplexität relativ wenige Speicherzugriffe braucht. Dann kann man im Gegenzug durchaus viele erfolglose rekursive Aufrufe in Kauf nehmen. Die "dummen" Lösungen sind auf einer PC-CPU manchmal die schnellsten. Man denkt als Mensch zu komplex (wobei das bei größeren Problemen (vielleicht auch schon bei extremen Sudokus) irgendwann nützt).
Also dachte ich mir, dass das ein realistischer Aufwand ist, um es nebenbei hinzubekommen. Ich zeige euch mal Zeiten, die sich bei mir ergeben haben. Zu berücksichtigen wäre, dass das Kompilat auf nur einem Core2Duo T6600 mit konstant 1,8 GHz (und DDR2-Speicher (PC2-5300)) läuft. Es handelt sich um ein 32-Bit-Programm.
Code:
//Zahlen von zeit.de ("SEHR-SCHWER")
const int templBoard[81]{
0, 5, 0, 0, 0, 2, 0, 0, 9,
1, 0, 2, 0, 0, 0, 0, 8, 0,
6, 0, 0, 3, 0, 0, 4, 0, 0,
0, 0, 0, 0, 2, 3, 0, 0, 0,
0, 8, 5, 0, 0, 9, 7, 0, 0,
0, 6, 3, 0, 7, 0, 0, 0, 2,
8, 0, 7, 0, 0, 0, 0, 5, 0,
0, 2, 0, 8, 0, 4, 1, 3, 0,
4, 0, 0, 0, 0, 0, 0, 0, 0
};
Dauer je Durchgang: 25,7 µs
Anzahl rekursiver Aufrufe: 527
Code:
//Zahlen von zeit.de ("SEHR-SCHWER")
const int templBoard[81]{
0, 1, 3, 8, 7, 6, 5, 0, 0,
0, 0, 0, 0, 0, 0, 8, 0, 0,
0, 0, 2, 9, 5, 4, 0, 0, 0,
0, 7, 1, 0, 0, 0, 0, 0, 0,
9, 0, 0, 0, 0, 3, 6, 2, 0,
0, 0, 0, 7, 0, 5, 0, 1, 0,
0, 0, 4, 0, 0, 0, 0, 0, 1,
0, 0, 0, 5, 0, 0, 7, 4, 0,
0, 6, 0, 0, 2, 0, 0, 8, 0
};
Dauer je Durchgang: 239 µs
Anzahl rekursiver Aufrufe: 3111
Code:
//Zahlen von 123sudoku.de ("Teuflisch (extrem)")
const int templBoard[81]{
0, 0, 0, 0, 0, 0, 0, 0, 0,
9, 0, 0, 1, 5, 4, 0, 0, 0,
7, 0, 4, 0, 9, 0, 1, 0, 0,
0, 3, 0, 4, 1, 6, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0,
8, 0, 0, 3, 0, 0, 0, 0, 0,
3, 0, 0, 0, 0, 7, 0, 0, 8,
0, 0, 7, 8, 0, 0, 9, 0, 3,
0, 5, 0, 0, 0, 9, 4, 0, 0
};
Dauer je Durchgang: 3770 µs
Anzahl rekursiver Aufrufe: 47333
Es wurde jeweils inklusive der Initialisierung des Boards sowie der Zählung der rekursiven Aufrufe gemessen. Die Werte sind aus Summen von hunderten bis hunderttausenden Durchgängen gemittelt, weil einzelne Durchgänge bei den kurzen Zeiten etwas sprunghaft sind. Eine große Diskrepanz zwischen den Zeiten von Einzeldurchgängen und mehreren gibt es im Gegensatz zu Java nicht, weil nicht zur Laufzeit optimiert wird. Vielleicht kann die CPU bei Wiederholung Cache Misses vermeiden, sodass weniger Ausreißer drin sind. Grundsätzlich ändert sich aber nichts.
Wie sehen eure Zeiten aus? Bei Interesse zeige ich gerne, was ich gemacht habe, um meine zu erreichen (es wurde nicht mit Bitoperationen getrickst). Wenn eure (unter Berücksichtigung der Hardware) viel besser sind, kann ich mir das natürlich sparen.
Ihr könnt natürlich aus dem JIT-Compiler die bestmögliche Optimierung herausholen, wenn ihr Java verwendet - vorausgesetzt, jeder Durchgang wird dabei immer noch einzeln berechnet (nicht wegoptimiert). Dass nichts weggemogelt wird, sollte sich befördern lassen, indem die rekursiven Aufrufe über alle Durchgänge hinweg gezählt und ausgegeben werden. Zur Korrektur teilt man natürlich vor der Ausgabe den Zählerstand durch die Anzahl der Durchgänge. Vermutlich ist der Compiler nicht so clever, aber sicher weiß man das nicht.