C ++ und die Bibliothek von Lingeling
Zusammenfassung: Ein neuer Ansatz, keine neuen Lösungen , ein schönes Programm zum Spielen und einige interessante Ergebnisse der lokalen Nichtverbesserbarkeit der bekannten Lösungen. Oh, und einige allgemein nützliche Beobachtungen.
Mit einem SAT-
basierten Ansatz konnte ich
das ähnliche Problem für 4x4-Labyrinthe mit blockierten Zellen anstelle von dünnen Wänden und festen Start- und Ausgangspositionen an gegenüberliegenden Ecken vollständig
lösen . Also hoffte ich, die gleichen Ideen für dieses Problem verwenden zu können. Obwohl ich für das andere Problem nur 2423 Labyrinthe verwendet habe (in der Zwischenzeit wurde beobachtet, dass 2083 genug sind) und eine Lösung der Länge 29 hat, hat die SAT-Codierung Millionen von Variablen verwendet und das Lösen hat Tage gedauert.
Deshalb habe ich beschlossen, den Ansatz auf zwei wichtige Arten zu ändern:
- Bestehen Sie nicht darauf, eine Lösung von Grund auf zu suchen, sondern lassen Sie einen Teil der Lösungszeichenfolge reparieren. (Das ist durch Hinzufügen von Unit-Klauseln ohnehin einfach zu tun, aber mein Programm macht es bequem.)
- Benutze nicht alle Labyrinthe von Anfang an. Fügen Sie stattdessen schrittweise jeweils ein ungelöstes Labyrinth hinzu. Einige Irrgärten können zufällig gelöst werden, oder sie werden immer dann gelöst, wenn die bereits in Betracht gezogenen gelöst sind. Im letzteren Fall wird es niemals hinzugefügt, ohne dass wir die Auswirkungen kennen müssen.
Ich habe auch einige Optimierungen vorgenommen, um weniger Variablen und Unit-Klauseln zu verwenden.
Das Programm basiert auf @ orlp's. Eine wichtige Änderung war die Auswahl der Labyrinthe:
- Erstens sind Labyrinthe nur durch ihre Wandstruktur und die Startposition gegeben. (Sie speichern auch die erreichbaren Positionen.) Die Funktion
is_solution
prüft, ob alle erreichbaren Positionen erreicht sind.
- (Unverändert: Es werden immer noch keine Labyrinthe mit nur 4 oder weniger erreichbaren Positionen verwendet. Die meisten würden jedoch durch die folgenden Beobachtungen ohnehin weggeworfen.)
- Wenn in einem Labyrinth keine der drei oberen Zellen verwendet wird, entspricht dies einem nach oben verschobenen Labyrinth. Also können wir es fallen lassen. Ebenso für ein Labyrinth, das keine der drei linken Zellen verwendet.
- Es spielt keine Rolle, ob nicht erreichbare Teile verbunden sind, daher bestehen wir darauf, dass jede nicht erreichbare Zelle vollständig von Wänden umgeben ist.
- Ein Labyrinth mit einem einzelnen Pfad, das ein Submaze eines größeren Labyrinths mit einem einzelnen Pfad ist, wird immer dann gelöst, wenn das größere Labyrinth gelöst wird. Wir brauchen es also nicht. Jedes einzelne Pfadlabyrinth mit einer Größe von höchstens 7 ist Teil eines größeren Labyrinths (das immer noch in 3x3 passt), aber es gibt einzelne Pfadlabyrinthe der Größe 8, die es nicht sind. Lassen Sie uns der Einfachheit halber nur Einzelpfad-Labyrinthe mit einer Größe von weniger als 8 fallen. (Und ich verwende immer noch, dass nur die Extrempunkte als Startpositionen betrachtet werden müssen. Alle Positionen werden als Ausgangspositionen verwendet, was nur für den SAT-Teil von Bedeutung ist des Programms.)
Auf diese Weise erhalte ich insgesamt 10772 Labyrinthe mit Startpositionen.
Hier ist das Programm:
#include <algorithm>
#include <array>
#include <bitset>
#include <cstring>
#include <iostream>
#include <set>
#include <vector>
#include <limits>
#include <cassert>
extern "C"{
#include "lglib.h"
}
// reusing a lot of @orlp's ideas and code
enum { N = -8, W = -2, E = 2, S = 8 };
static const int encoded_pos[] = {8, 10, 12, 16, 18, 20, 24, 26, 28};
static const int wall_idx[] = {9, 11, 12, 14, 16, 17, 19, 20, 22, 24, 25, 27};
static const int move_offsets[] = { N, E, S, W };
static const uint32_t toppos = 1ull << 8 | 1ull << 10 | 1ull << 12;
static const uint32_t leftpos = 1ull << 8 | 1ull << 16 | 1ull << 24;
static const int unencoded_pos[] = {0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,3,
0,4,0,5,0,0,0,6,0,7,0,8};
int do_move(uint32_t walls, int pos, int move) {
int idx = pos + move / 2;
return walls & (1ull << idx) ? pos + move : pos;
}
struct Maze {
uint32_t walls, reach;
int start;
Maze(uint32_t walls=0, uint32_t reach=0, int start=0):
walls(walls),reach(reach),start(start) {}
bool is_dummy() const {
return (walls==0);
}
std::size_t size() const{
return std::bitset<32>(reach).count();
}
std::size_t simplicity() const{ // how many potential walls aren't there?
return std::bitset<32>(walls).count();
}
};
bool cmp(const Maze& a, const Maze& b){
auto asz = a.size();
auto bsz = b.size();
if (asz>bsz) return true;
if (asz<bsz) return false;
return a.simplicity()<b.simplicity();
}
uint32_t reachable(uint32_t walls) {
static int fill[9];
uint32_t reached = 0;
uint32_t reached_relevant = 0;
for (int start : encoded_pos){
if ((1ull << start) & reached) continue;
uint32_t reached_component = (1ull << start);
fill[0]=start;
int count=1;
for(int i=0; i<count; ++i)
for(int m : move_offsets) {
int newpos = do_move(walls, fill[i], m);
if (reached_component & (1ull << newpos)) continue;
reached_component |= 1ull << newpos;
fill[count++] = newpos;
}
if (count>1){
if (reached_relevant)
return 0; // more than one nonsingular component
if (!(reached_component & toppos) || !(reached_component & leftpos))
return 0; // equivalent to shifted version
if (std::bitset<32>(reached_component).count() <= 4)
return 0;
reached_relevant = reached_component;
}
reached |= reached_component;
}
return reached_relevant;
}
void enterMazes(uint32_t walls, uint32_t reached, std::vector<Maze>& mazes){
int max_deg = 0;
uint32_t ends = 0;
for (int pos : encoded_pos)
if (reached & (1ull << pos)) {
int deg = 0;
for (int m : move_offsets) {
if (pos != do_move(walls, pos, m))
++deg;
}
if (deg == 1)
ends |= 1ull << pos;
max_deg = std::max(deg, max_deg);
}
uint32_t starts = reached;
if (max_deg == 2){
if (std::bitset<32>(reached).count() <= 7)
return; // small paths are redundant
starts = ends; // need only start at extremal points
}
for (int pos : encoded_pos)
if ( starts & (1ull << pos))
mazes.emplace_back(walls, reached, pos);
}
std::vector<Maze> gen_valid_mazes() {
std::vector<Maze> mazes;
for (int maze_id = 0; maze_id < (1 << 12); maze_id++) {
uint32_t walls = 0;
for (int i = 0; i < 12; ++i)
if (maze_id & (1 << i))
walls |= 1ull << wall_idx[i];
uint32_t reached=reachable(walls);
if (!reached) continue;
enterMazes(walls, reached, mazes);
}
std::sort(mazes.begin(),mazes.end(),cmp);
return mazes;
};
bool is_solution(const std::vector<int>& moves, Maze& maze) {
int pos = maze.start;
uint32_t reached = 1ull << pos;
for (auto move : moves) {
pos = do_move(maze.walls, pos, move);
reached |= 1ull << pos;
if (reached == maze.reach) return true;
}
return false;
}
std::vector<int> str_to_moves(std::string str) {
std::vector<int> moves;
for (auto c : str) {
switch (c) {
case 'N': moves.push_back(N); break;
case 'E': moves.push_back(E); break;
case 'S': moves.push_back(S); break;
case 'W': moves.push_back(W); break;
}
}
return moves;
}
Maze unsolved(const std::vector<int>& moves, std::vector<Maze>& mazes) {
int unsolved_count = 0;
Maze problem{};
for (Maze m : mazes)
if (!is_solution(moves, m))
if(!(unsolved_count++))
problem=m;
if (unsolved_count)
std::cout << "unsolved: " << unsolved_count << "\n";
return problem;
}
LGL * lgl;
constexpr int TRUELIT = std::numeric_limits<int>::max();
constexpr int FALSELIT = -TRUELIT;
int new_var(){
static int next_var = 1;
assert(next_var<TRUELIT);
return next_var++;
}
bool lit_is_true(int lit){
int abslit = lit>0 ? lit : -lit;
bool res = (abslit==TRUELIT) || (lglderef(lgl,abslit)>0);
return lit>0 ? res : !res;
}
void unsat(){
std::cout << "Unsatisfiable!\n";
std::exit(1);
}
void clause(const std::set<int>& lits){
if (lits.find(TRUELIT) != lits.end())
return;
for (int lit : lits)
if (lits.find(-lit) != lits.end())
return;
int found=0;
for (int lit : lits)
if (lit != FALSELIT){
lgladd(lgl, lit);
found=1;
}
lgladd(lgl, 0);
if (!found)
unsat();
}
void at_most_one(const std::set<int>& lits){
if (lits.size()<2)
return;
for(auto it1=lits.cbegin(); it1!=lits.cend(); ++it1){
auto it2=it1;
++it2;
for( ; it2!=lits.cend(); ++it2)
clause( {- *it1, - *it2} );
}
}
/* Usually, lit_op(lits,sgn) creates a new variable which it returns,
and adds clauses that ensure that the variable is equivalent to the
disjunction (if sgn==1) or the conjunction (if sgn==-1) of the literals
in lits. However, if this disjunction or conjunction is constant True
or False or simplifies to a single literal, that is returned without
creating a new variable and without adding clauses. */
int lit_op(std::set<int> lits, int sgn){
if (lits.find(sgn*TRUELIT) != lits.end())
return sgn*TRUELIT;
lits.erase(sgn*FALSELIT);
if (!lits.size())
return sgn*FALSELIT;
if (lits.size()==1)
return *lits.begin();
int res=new_var();
for(int lit : lits)
clause({sgn*res,-sgn*lit});
for(int lit : lits)
lgladd(lgl,sgn*lit);
lgladd(lgl,-sgn*res);
lgladd(lgl,0);
return res;
}
int lit_or(std::set<int> lits){
return lit_op(lits,1);
}
int lit_and(std::set<int> lits){
return lit_op(lits,-1);
}
using A4 = std::array<int,4>;
void add_maze_conditions(Maze m, std::vector<A4> dirs, int len){
int mp[9][2];
int rp[9];
for(int p=0; p<9; ++p)
if((1ull << encoded_pos[p]) & m.reach)
rp[p] = mp[p][0] = encoded_pos[p]==m.start ? TRUELIT : FALSELIT;
int t=0;
for(int i=0; i<len; ++i){
std::set<int> posn {};
for(int p=0; p<9; ++p){
int ep = encoded_pos[p];
if((1ull << ep) & m.reach){
std::set<int> reach_pos {};
for(int d=0; d<4; ++d){
int np = do_move(m.walls, ep, move_offsets[d]);
reach_pos.insert( lit_and({mp[unencoded_pos[np]][t],
dirs[i][d ^ ((np==ep)?0:2)] }));
}
int pl = lit_or(reach_pos);
mp[p][!t] = pl;
rp[p] = lit_or({rp[p], pl});
posn.insert(pl);
}
}
at_most_one(posn);
t=!t;
}
for(int p=0; p<9; ++p)
if((1ull << encoded_pos[p]) & m.reach)
clause({rp[p]});
}
void usage(char* argv0){
std::cout << "usage: " << argv0 <<
" <string>\n where <string> consists of 'N', 'E', 'S', 'W' and '*'.\n" ;
std::exit(2);
}
const std::string nesw{"NESW"};
int main(int argc, char** argv) {
if (argc!=2)
usage(argv[0]);
std::vector<Maze> mazes = gen_valid_mazes();
std::cout << "Mazes with start positions: " << mazes.size() << "\n" ;
lgl = lglinit();
int len = std::strlen(argv[1]);
std::cout << argv[1] << "\n with length " << len << "\n";
std::vector<A4> dirs;
for(int i=0; i<len; ++i){
switch(argv[1][i]){
case 'N':
dirs.emplace_back(A4{TRUELIT,FALSELIT,FALSELIT,FALSELIT});
break;
case 'E':
dirs.emplace_back(A4{FALSELIT,TRUELIT,FALSELIT,FALSELIT});
break;
case 'S':
dirs.emplace_back(A4{FALSELIT,FALSELIT,TRUELIT,FALSELIT});
break;
case 'W':
dirs.emplace_back(A4{FALSELIT,FALSELIT,FALSELIT,TRUELIT});
break;
case '*': {
dirs.emplace_back();
std::generate_n(dirs[i].begin(),4,new_var);
std::set<int> dirs_here { dirs[i].begin(), dirs[i].end() };
at_most_one(dirs_here);
clause(dirs_here);
for(int l : dirs_here)
lglfreeze(lgl,l);
break;
}
default:
usage(argv[0]);
}
}
int maze_nr=0;
for(;;) {
std::cout << "Solving...\n";
int res=lglsat(lgl);
if(res==LGL_UNSATISFIABLE)
unsat();
assert(res==LGL_SATISFIABLE);
std::string sol(len,' ');
for(int i=0; i<len; ++i)
for(int d=0; d<4; ++d)
if (lit_is_true(dirs[i][d])){
sol[i]=nesw[d];
break;
}
std::cout << sol << "\n";
Maze m=unsolved(str_to_moves(sol),mazes);
if (m.is_dummy()){
std::cout << "That solves all!\n";
return 0;
}
std::cout << "Adding maze " << ++maze_nr << ": " <<
m.walls << "/" << m.start <<
" (" << m.size() << "/" << 12-m.simplicity() << ")\n";
add_maze_conditions(m,dirs,len);
}
}
Zuerst configure.sh
und make
der lingeling
Löser, dann kompilieren Sie das Programm mit so etwas wie
g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl
, wo ...
ist der Pfad, wo lglib.h
bzw. liblgl.a
sind, so könnten zum Beispiel beide sein
../lingeling-<version>
. Oder legen Sie sie einfach in dasselbe Verzeichnis und verzichten Sie auf die Optionen -I
und -L
.
Das Programm nimmt ein obligatorisches Befehlszeilenargument, eine Zeichenfolge , bestehend aus N
, E
, S
, W
(für feste Richtungen) oder *
. Sie können also nach einer allgemeinen Lösung der Größe 78 suchen, indem Sie eine Zeichenfolge von 78 *
s (in Anführungszeichen) eingeben, oder nach einer Lösung suchen, die mit NEWS
der Verwendung von NEWS
gefolgt von beliebig vielen *
s für zusätzliche Schritte beginnt . Nehmen Sie als ersten Test Ihre Lieblingslösung und ersetzen Sie einige Buchstaben durch *
. Dies findet schnell eine Lösung für einen überraschend hohen Wert von "some".
Das Programm erkennt das hinzugefügte Labyrinth anhand der Wandstruktur und der Startposition und gibt die Anzahl der erreichbaren Positionen und Wände an. Die Labyrinthe werden nach diesen Kriterien sortiert und die erste ungelöste wird hinzugefügt. Daher haben die meisten Labyrinthe hinzugefügt (9/4)
, aber manchmal erscheinen auch andere.
Ich nahm die bekannte Lösung der Länge 79 und versuchte, sie für jede Gruppe von 26 benachbarten Buchstaben durch 25 beliebige Buchstaben zu ersetzen. Ich habe auch versucht, 13 Buchstaben am Anfang und am Ende zu entfernen und sie durch 13 am Anfang und 12 am Ende zu ersetzen und umgekehrt. Leider ist alles unbefriedigend ausgefallen. Können wir dies als Indikator dafür nehmen, dass die Länge 79 optimal ist? Nein, ich habe in ähnlicher Weise versucht, die Lösung für Länge 80 auf Länge 79 zu verbessern, und das war auch nicht erfolgreich.
Schließlich habe ich versucht, den Anfang einer Lösung mit dem Ende der anderen zu kombinieren und auch mit einer Lösung, die durch eine der Symmetrien transformiert wurde. Jetzt gehen mir die interessanten Ideen aus und ich habe beschlossen, Ihnen zu zeigen, was ich habe, obwohl es nicht zu neuen Lösungen geführt hat.