C ++ y la biblioteca de lingeling
Resumen: un nuevo enfoque, sin nuevas soluciones , un buen programa para jugar y algunos resultados interesantes de la no mejorabilidad local de las soluciones conocidas. Ah, y algunas observaciones generalmente útiles.
Utilizando un
enfoque basado en SAT , pude resolver completamente
el problema similar para laberintos 4x4 con celdas bloqueadas en lugar de paredes delgadas y posiciones fijas de inicio y salida en esquinas opuestas. Así que esperaba poder usar las mismas ideas para este problema. Sin embargo, aunque para el otro problema solo usé 2423 laberintos (mientras tanto se ha observado que 2083 son suficientes) y tiene una solución de longitud 29, la codificación SAT usó millones de variables y su resolución tomó días.
Así que decidí cambiar el enfoque de dos maneras importantes:
- No insista en buscar una solución desde cero, pero permita arreglar una parte de la cadena de la solución. (Eso es fácil de hacer de todos modos agregando cláusulas de unidad, pero mi programa lo hace cómodo).
- No uses todos los laberintos desde el principio. En cambio, agregue incrementalmente un laberinto sin resolver a la vez. Algunos laberintos pueden resolverse por casualidad, o siempre se resuelven cuando se resuelven los que ya se consideran. En el último caso, nunca se agregará, sin que necesitemos saber la implicación.
También hice algunas optimizaciones para usar menos variables y cláusulas unitarias.
El programa se basa en @ orlp's. Un cambio importante fue la selección de laberintos:
- En primer lugar, los laberintos están dados únicamente por su estructura de pared y la posición de inicio. (También almacenan las posiciones alcanzables.) La función
is_solution
verifica si se alcanzan todas las posiciones alcanzables.
- (Sin cambios: todavía no se utilizan laberintos con solo 4 o menos posiciones alcanzables. Pero la mayoría de ellos serían desechados de todos modos por las siguientes observaciones).
- Si un laberinto no usa ninguna de las tres celdas superiores, es equivalente a un laberinto desplazado hacia arriba. Entonces podemos dejarlo caer. Del mismo modo para un laberinto que no utiliza ninguna de las tres celdas de la izquierda.
- No importa si hay partes inalcanzables conectadas, por lo que insistimos en que cada celda inalcanzable esté completamente rodeada por paredes.
- Un laberinto de un solo camino que es un submaze de un laberinto de un solo camino más grande siempre se resuelve cuando se resuelve el laberinto más grande, por lo que no lo necesitamos. Cada laberinto de ruta individual de tamaño máximo 7 es parte de uno más grande (que todavía se ajusta en 3x3), pero hay laberintos de ruta única de tamaño 8 que no lo son. Para simplificar, dejemos caer laberintos de un solo camino de tamaño inferior a 8. (Y sigo usando que solo los puntos extremos deben considerarse como posiciones de inicio. Todas las posiciones se usan como posiciones de salida, lo que solo importa para la parte SAT Del programa.)
De esta manera, obtengo un total de 10772 laberintos con posiciones de inicio.
Aquí está el programa:
#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);
}
}
Primero configure.sh
y make
el lingeling
solucionador, luego compila el programa con algo como
g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl
, dónde ...
está el camino donde lglib.h
resp. liblgl.a
son, por lo que ambos podrían ser, por ejemplo
../lingeling-<version>
. O simplemente colóquelos en el mismo directorio y no use las opciones -I
y -L
.
El programa toma un argumento de línea de comandos obligatoria, una cadena que consiste en N
, E
, S
, W
(para direcciones fijas) o *
. Por lo tanto, puede buscar una solución general de tamaño 78 dando una cadena de 78 *
s (entre comillas), o buscar una solución comenzando con el NEWS
uso NEWS
seguido de tantos *
s como desee para pasos adicionales. Como primera prueba, tome su solución favorita y reemplace algunas de las letras *
. Esto encuentra una solución rápida para un valor sorprendentemente alto de "algunos".
El programa le dirá qué laberinto agrega, descrito por la estructura de la pared y la posición de inicio, y también le dará la cantidad de posiciones y paredes alcanzables. Los laberintos se ordenan según estos criterios, y se agrega el primero sin resolver. Por lo tanto, la mayoría de los laberintos añadidos tienen (9/4)
, pero a veces otros también aparecen.
Tomé la solución conocida de longitud 79, y para cada grupo de 26 letras adyacentes, traté de reemplazarlas con 25 letras. También intenté eliminar 13 letras del principio y del final, y reemplazarlas por 13 al principio y 12 al final, y viceversa. Desafortunadamente, todo salió insatisfactorio. Entonces, ¿podemos tomar esto como un indicador de que la longitud 79 es óptima? No, igualmente intenté mejorar la solución de longitud 80 a longitud 79, y eso tampoco fue exitoso.
Finalmente, intenté combinar el comienzo de una solución con el final de la otra, y también con una solución transformada por una de las simetrías. Ahora me estoy quedando sin ideas interesantes, así que decidí mostrarte lo que tengo, a pesar de que no condujo a nuevas soluciones.