C ++ ve dil bilen kütüphaneler
Özet: Yeni bir yaklaşım, yeni bir çözüm yok , oynamak için güzel bir program ve bilinen çözümlerin yerel olarak iyileştirilememesi konusundaki ilginç bazı sonuçlar. Oh, ve bazı genel olarak yararlı gözlemler.
SAT
tabanlı bir yaklaşım kullanarak
, ince duvarlar yerine bloke hücreli 4x4 labirentlerinde ve ters köşelerde sabit başlangıç ve çıkış pozisyonlarında benzer problemleri tamamen
çözebildim . Bu yüzden bu problem için aynı fikirleri kullanabilmeyi umuyordum. Bununla birlikte, diğer problem için sadece 2423 labirent kullandım (bu arada 2083'ün yeterli olduğu gözlendi) ve 29 uzunluğundaki bir çözüme sahipti, SAT kodlaması milyonlarca değişken kullandı ve günlerini çözdü.
Bu yüzden yaklaşımı iki önemli şekilde değiştirmeye karar verdim:
- Sıfırdan bir çözüm aramakta ısrar etmeyin, ancak çözüm dizesinin bir bölümünü düzeltmeye izin verin. (Ünite cümleleri ekleyerek yine de yapmak kolaydır, ancak programım bunu yapmayı rahatlatıyor.)
- Tüm labirentleri en baştan kullanmayın. Bunun yerine, art arda bir defada çözülmemiş bir labirent ekleyin. Bazı labirentler tesadüfen çözülebilir veya önceden düşünülenler çözüldüğünde her zaman çözülürler. İkinci durumda, anlamını bilmek zorunda kalmadan asla eklenmeyecek.
Ayrıca daha az değişken ve birim cümlecikleri kullanmak için bazı optimizasyonlar yaptım.
Program, @ orlp’e dayanmaktadır. Önemli bir değişiklik labirent seçimi oldu:
- Öncelikle, labirentler duvar yapılarına ve sadece başlangıç pozisyonlarına göre verilmiştir. (Ayrıca ulaşılabilir pozisyonları da saklarlar.) Fonksiyon, ulaşılabilir
is_solution
tüm pozisyonlara ulaşılıp ulaşılmadığını kontrol eder.
- (Değişmedi: hala sadece 4 veya daha az erişilebilir pozisyona sahip labirentler kullanmıyor. Fakat çoğu yine de aşağıdaki gözlemlerle atılacaktı.)
- Bir labirent en üstteki üç hücreden birini kullanmıyorsa, yukarı kaydırılan bir labirente eşdeğerdir. Böylece bırakabiliriz. Aynı şekilde, sol üç hücreden herhangi birini kullanmayan bir labirent için.
- Ulaşılamayan parçaların birbirine bağlı olup olmaması önemli değildir, bu yüzden ulaşılamayan her hücrenin tamamen duvarlarla çevrili olduğu konusunda ısrar ediyoruz.
- Daha büyük bir tek yol labirentinin alt kümesi olan tek bir yol labirenti, daha büyük bir çözümlendiğinde her zaman çözülür, bu yüzden buna ihtiyacımız olmaz. En fazla 7 büyüklüğündeki her bir tek yol labirenti, daha büyük olanın bir parçasıdır (hala 3x3'te). Basit olması için, 8'den küçük tek labirentli labirentlere bırakalım. (Ve hala sadece aşırı noktaların başlangıç pozisyonu olarak kabul edilmesi gerektiğini düşünüyorum. Programın
Bu sayede başlangıç pozisyonları ile toplam 10772 labirent elde ediyorum.
İşte program:
#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);
}
}
İlk configure.sh
ve benzeri çözücü, daha sonra bir şeyle programı derlemek
nerede, nerede yoludur solunum. mesela ikisi de olabilir
. Ya da sadece onları aynı dizine yerleştirin ve ve seçenekleri olmadan yapın .make
lingeling
g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl
...
lglib.h
liblgl.a
../lingeling-<version>
-I
-L
Program, bir zorunlu komut satırı argüman alır dize oluşan N
, E
, S
, W
(sabit tarifini) ya da *
. Böylece, 78 *
s (tırnak işaretleri içinde) bir dize vererek genel bir çözüm boyutu 78'i arayabilir veya daha sonra ek adımlar için istediğiniz kadar s'yi NEWS
kullanarak başlayarak bir çözüm arayabilirsiniz . İlk test olarak, favori çözümünüzü alın ve bazı harfleri ile değiştirin . Bu, şaşırtıcı bir şekilde yüksek bir "bazı" değeri için hızlı bir çözüm bulur.NEWS
*
*
Program, hangi duvarın labirentine eklendiğini, duvar yapısı ve başlama pozisyonu ile anlatıldığını ve aynı zamanda ulaşılabilir pozisyonların ve duvarların sayısını verir. Labirentler bu kritere göre sıralanır ve ilk çözülmeyenler eklenir. Bu nedenle çoğu labirentte labirent vardır (9/4)
, ancak bazen diğerleri de ortaya çıkar.
Bilinen uzunluktaki 79 çözümünü aldım ve komşu 26 harften oluşan her grup için, her birini 25 harften değiştirmeye çalıştım. Ayrıca, 13 harfi baştan ve sondan kaldırmaya, başlarında 13, sonunda da 12 harfle değiştirmeye çalıştım. Ne yazık ki, hepsi tatmin edilemez geldi. Peki, bunu 79 uzunluğunun en uygun olduğunu gösterebilir miyiz? Hayır, benzer şekilde uzunluk 80 çözümünü uzunluk 79'a yükseltmeye çalıştım ve bu da başarılı olamadı.
Sonunda bir çözümün başlangıcını diğerinin sonu ile ve ayrıca simetrilerden biri tarafından dönüştürülmüş bir çözümle birleştirmeyi denedim. Şimdi ilginç fikirlerim tükeniyor, bu yüzden yeni çözümlere yol açmasa da elimde olanı göstermeye karar verdim.