C ++ và thư viện từ lingeling
Tóm tắt: Một cách tiếp cận mới, không có giải pháp mới , một chương trình hay để chơi và một số kết quả thú vị về tính không ngẫu hứng cục bộ của các giải pháp đã biết. Oh, và một số quan sát thường hữu ích.
Sử dụng
phương pháp tiếp cận dựa trên SAT , tôi hoàn toàn có thể
giải quyết
vấn đề tương tự đối với các mê cung 4 x 4 với các ô bị chặn thay vì các bức tường mỏng và các vị trí bắt đầu và thoát cố định ở các góc đối diện. Vì vậy, tôi hy vọng có thể sử dụng những ý tưởng tương tự cho vấn đề này. Tuy nhiên, mặc dù đối với vấn đề khác, tôi chỉ sử dụng 2423 mê cung (trong khi đó, theo quan sát thì năm 2083 là đủ) và nó có một giải pháp dài 29, mã hóa SAT đã sử dụng hàng triệu biến số và giải quyết được mất nhiều ngày.
Vì vậy, tôi quyết định thay đổi cách tiếp cận theo hai cách quan trọng:
- Đừng khăng khăng tìm kiếm giải pháp từ đầu, nhưng cho phép sửa một phần của chuỗi giải pháp. (Điều đó dễ thực hiện bằng cách thêm các mệnh đề đơn vị, nhưng chương trình của tôi làm cho nó thoải mái để làm.)
- Đừng sử dụng tất cả các mê cung ngay từ đầu. Thay vào đó, tăng dần một mê cung chưa giải quyết tại một thời điểm. Một số mê cung có thể được giải quyết một cách tình cờ, hoặc chúng luôn được giải quyết khi những cái đã được xem xét được giải quyết. Trong trường hợp sau, nó sẽ không bao giờ được thêm vào, mà chúng ta không cần phải biết hàm ý.
Tôi cũng đã thực hiện một số tối ưu hóa để sử dụng ít biến và mệnh đề đơn vị.
Chương trình này dựa trên @ orlp's. Một thay đổi quan trọng là lựa chọn mê cung:
- Trước hết, mê cung được cho bởi cấu trúc tường và vị trí bắt đầu. (Họ cũng lưu trữ các vị trí có thể tiếp cận.) Chức năng
is_solution
kiểm tra nếu đạt được tất cả các vị trí có thể tiếp cận.
- (Không thay đổi: vẫn không sử dụng mê cung chỉ có 4 vị trí có thể tiếp cận hoặc ít hơn. Nhưng hầu hết trong số chúng sẽ bị vứt đi dù sao đi nữa bởi các quan sát sau đây.)
- Nếu một mê cung không sử dụng bất kỳ một trong ba ô trên cùng, thì nó tương đương với một mê cung được dịch chuyển lên. Vì vậy, chúng tôi có thể thả nó. Tương tự như vậy đối với một mê cung không sử dụng bất kỳ một trong ba ô bên trái.
- Sẽ không có vấn đề gì nếu các phần không thể truy cập được kết nối, vì vậy chúng tôi khẳng định rằng mỗi ô không thể truy cập được bao quanh hoàn toàn bởi các bức tường.
- Một mê cung đường dẫn duy nhất là một mê cung của một mê cung đường dẫn lớn hơn luôn luôn được giải quyết khi một mê cung đường lớn hơn được giải quyết, vì vậy chúng ta không cần nó. Mỗi mê cung đường dẫn có kích thước tối đa 7 là một phần của một mê cung lớn hơn (vẫn phù hợp với 3x3), nhưng có những mê cung đường dẫn đơn kích thước 8 không có. Để đơn giản, chúng ta chỉ cần thả các mê cung đường dẫn có kích thước nhỏ hơn 8. (Và tôi vẫn đang sử dụng chỉ các điểm cực trị cần được coi là vị trí bắt đầu. Tất cả các vị trí được sử dụng làm vị trí thoát, chỉ quan trọng đối với phần SAT của chương trình.)
Bằng cách này, tôi nhận được tổng cộng 10772 mê cung với vị trí bắt đầu.
Đây là chương trình:
#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);
}
}
Đầu tiên configure.sh
và make
người lingeling
giải quyết, sau đó biên dịch chương trình với một cái gì đó như
g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl
, đâu ...
là con đường nơi tôn trọng lglib.h
. liblgl.a
là, vì vậy cả hai có thể là ví dụ
../lingeling-<version>
. Hoặc chỉ cần đặt chúng trong cùng một thư mục và làm mà không có -I
và -L
các tùy chọn.
Chương trình này mất một đối số dòng lệnh bắt buộc, một chuỗi bao gồm N
, E
, S
, W
(đối với hướng cố định) hoặc *
. Vì vậy, bạn có thể tìm kiếm một giải pháp chung có kích thước 78 bằng cách đưa ra một chuỗi 78 *
giây (trong ngoặc kép) hoặc tìm kiếm một giải pháp bắt đầu NEWS
bằng cách sử dụng NEWS
theo sau là nhiều *
s như bạn muốn cho các bước bổ sung. Như một thử nghiệm đầu tiên, lấy giải pháp yêu thích của bạn và thay thế một số chữ cái bằng *
. Điều này tìm thấy một giải pháp nhanh chóng cho giá trị cao của "một số" đáng ngạc nhiên.
Chương trình sẽ cho biết nó thêm mê cung nào, được mô tả bởi cấu trúc tường và vị trí bắt đầu, đồng thời cũng đưa ra số lượng vị trí và tường có thể tiếp cận. Các mê cung được sắp xếp theo các tiêu chí này, và tiêu chí chưa được giải quyết đầu tiên được thêm vào. Do đó, hầu hết các mê cung được thêm vào (9/4)
, nhưng đôi khi những người khác cũng xuất hiện.
Tôi đã lấy giải pháp đã biết có độ dài 79, và với mỗi nhóm 26 chữ cái liền kề, đã cố gắng thay thế chúng bằng 25 chữ cái bất kỳ. Tôi cũng đã cố gắng loại bỏ 13 chữ cái từ đầu và cuối, và thay thế chúng bằng 13 chữ cái ở đầu và 12 chữ cái ở cuối và ngược lại. Thật không may, tất cả đã đi ra không thỏa mãn. Vì vậy, chúng ta có thể coi đây là chỉ số cho thấy chiều dài 79 là tối ưu? Không, tôi cũng đã cố gắng cải thiện giải pháp chiều dài 80 thành chiều dài 79 và điều đó cũng không thành công.
Cuối cùng, tôi đã thử kết hợp sự bắt đầu của một giải pháp với kết thúc của giải pháp kia và cũng với một giải pháp được biến đổi bởi một trong các đối xứng. Bây giờ tôi đang cạn kiệt những ý tưởng thú vị, vì vậy tôi quyết định cho bạn thấy những gì tôi có, mặc dù nó không dẫn đến những giải pháp mới.