C ++ e la libreria dal persistere
Riepilogo: un nuovo approccio, nessuna nuova soluzione , un bel programma con cui giocare e alcuni risultati interessanti della non improvvisabilità locale delle soluzioni conosciute. Oh, e alcune osservazioni generalmente utili.
Utilizzando un
approccio basato su SAT , sono stato in grado di risolvere completamente
il problema simile per i labirinti 4x4 con celle bloccate anziché pareti sottili e posizioni di partenza e uscita fisse agli angoli opposti. Quindi speravo di poter usare le stesse idee per questo problema. Tuttavia, anche se per l'altro problema ho usato solo 2423 labirinti (nel frattempo è stato osservato che 2083 sono sufficienti) e ha una soluzione di lunghezza 29, la codifica SAT ha utilizzato milioni di variabili e la risoluzione ha richiesto giorni.
Quindi ho deciso di cambiare l'approccio in due modi importanti:
- Non insistere sulla ricerca di una soluzione da zero, ma consentire di correggere una parte della stringa della soluzione. (È facile da fare comunque aggiungendo clausole unitarie, ma il mio programma lo rende comodo da fare.)
- Non usare tutti i labirinti dall'inizio. Invece, aggiungi in modo incrementale un labirinto irrisolto alla volta. Alcuni labirinti possono essere risolti per caso, oppure sono sempre risolti quando quelli già considerati sono risolti. In quest'ultimo caso, non verrà mai aggiunto, senza che noi abbiamo bisogno di conoscere le implicazioni.
Ho anche fatto alcune ottimizzazioni per usare meno variabili e clausole unitarie.
Il programma si basa su @ orlp's. Un cambiamento importante è stata la selezione di labirinti:
- Prima di tutto, i labirinti sono dati dalla loro struttura del muro e solo dalla posizione iniziale. (Memorizzano anche le posizioni raggiungibili.) La funzione
is_solution
controlla se tutte le posizioni raggiungibili sono raggiunte.
- (Invariato: continua a non usare labirinti con solo 4 o meno posizioni raggiungibili. Ma la maggior parte di essi verrebbe comunque buttata via dalle seguenti osservazioni.)
- Se un labirinto non utilizza nessuna delle tre celle superiori, è equivalente a un labirinto spostato verso l'alto. Quindi possiamo lasciarlo cadere. Allo stesso modo per un labirinto che non utilizza nessuna delle tre celle a sinistra.
- Non importa se le parti non raggiungibili sono collegate, quindi insistiamo sul fatto che ogni cella non raggiungibile è completamente circondata da muri.
- Un labirinto a percorso singolo che è un sottomarino di un labirinto a percorso singolo più grande viene sempre risolto quando viene risolto quello più grande, quindi non ne abbiamo bisogno. Ogni labirinto a singolo percorso di dimensioni al massimo 7 fa parte di un labirinto più grande (ancora adatto a 3x3), ma ci sono labirinti a percorso singolo di dimensioni 8 che non lo sono. Per semplicità, lasciamo cadere labirinti a percorso singolo di dimensioni inferiori a 8. (E sto ancora usando che solo i punti estremi devono essere considerati come posizioni di partenza. Tutte le posizioni sono usate come posizioni di uscita, che conta solo per la parte SAT del programma.)
In questo modo, ottengo un totale di 10772 labirinti con posizioni iniziali.
Ecco il programma:
#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);
}
}
Prima configure.sh
e make
il lingeling
risolutore, quindi compilare il programma con qualcosa di simile
g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl
, dov'è ...
il percorso in cui lglib.h
resp. liblgl.a
lo sono, quindi entrambi potrebbero essere ad esempio
../lingeling-<version>
. O semplicemente inseriscili nella stessa directory e fai a meno delle opzioni -I
e -L
.
Il programma prende un argomento obbligatorio linea di comando, una stringa costituita N
, E
, S
, W
(per direzioni fisse) o *
. Quindi potresti cercare una soluzione generale della dimensione 78 fornendo una stringa di 78 *
s (tra virgolette) o cercare una soluzione che inizia con l' NEWS
uso NEWS
seguito da tutti gli *
s che desideri per ulteriori passaggi. Come primo test, prendi la tua soluzione preferita e sostituisci alcune delle lettere con *
. Questo trova una soluzione veloce per un valore sorprendentemente alto di "alcuni".
Il programma dirà quale labirinto aggiunge, descritto dalla struttura della parete e dalla posizione iniziale e fornirà anche il numero di posizioni e pareti raggiungibili. I labirinti sono ordinati in base a questi criteri e viene aggiunto il primo irrisolto. Pertanto la maggior parte dei labirinti aggiunti ha (9/4)
, ma a volte ne appaiono anche altri.
Ho preso la soluzione nota della lunghezza 79 e, per ogni gruppo di 26 lettere adiacenti, ho provato a sostituirle con 25 lettere qualsiasi. Ho anche provato a rimuovere 13 lettere dall'inizio e dalla fine e sostituirle con 13 all'inizio e 12 alla fine e viceversa. Sfortunatamente, tutto è risultato insoddisfacente. Quindi, possiamo prendere questo come indicatore che la lunghezza 79 è ottimale? No, ho anche cercato di migliorare la soluzione lunghezza 80 alla lunghezza 79, e anche questo non ha avuto successo.
Alla fine, ho provato a combinare l'inizio di una soluzione con la fine dell'altra, e anche con una soluzione trasformata da una delle simmetrie. Ora sto esaurendo le idee interessanti, quindi ho deciso di mostrarti quello che ho, anche se non ha portato a nuove soluzioni.