Предыстория
Чтобы летом держать мозг в тонусе я скачал себе сборник головоломок. По началу задания были довольно простыми и не особо требовательными к проявлению логики, но по ходу игры чувствовалось нарастающее усложнение.
В какой-то момент я застрял на головоломке под названием «Китайские шашки». Редкие потуги решить её своими силами не приносили особых плодов на протяжение долгого времени и в итоге я отложил свои муки с решением до лучших времен.
Закончилась зимняя сессия, а до начала учебы еще пара недель — чем не «лучшие времена»? Я заглянул в интернет, дабы проверить есть ли у данной головоломки вообще хоть какое-нибудь решение, и первые же результаты поискового запроса убедили меня в том, что оно действительно существует.
Я не стал подглядывать в прохождение, мне хотелось дойти до него своими силами — или самому решить, или написать программу, которая найдет мне это решение. Однако напрямую применить силу мозга мне так и не удалось, я явно упускал из виду что-то принципиально важное для нахождения решения.
— «Ну всё, пусть эта головоломка поговорит с моим многоядерным другом!» — пронеслось у меня в голове, и я сел за написание брутфорса.
Постановка задачи
Поле — 33 ячейки, 32 из которых заняты фишками. Цель — съесть максимально возможное количество фишек(должна остаться лишь одна). Есть можно только по вертикали и горизонтали таким образом:

Пример хода
Первая попытка решения
Первый брутфорс был написан без учета теории за пол-часа, его идея заключалась в рекурсивном спуске по дереву возможных ходов, с рандомным выбором каждого последующего хода.

Псевдо блок-схема первой версии
И всё это чудо вертелось в цикле. В первые 15 секунд он даже смог найти ход решения приводящий к 3 остающимся фишкам на поле, а в последующую минуту и к 2! Но радость была не долгой, ибо рандом, пусть даже псевдо, трудно предсказуем — за ночь работы он не смог более приблизиться к решению. Становилось очевидна потребность рассмотрения вопроса с теоретической точки зрения.
Немного теории
Если действующее игровое поле вытянуть в строку, то получится 33 разрядное двоичное число, где 1 соответствует ячейке с фишкой, а 0 пустой ячейке. Соответственно всего состояний у поля может быть не больше 2^33 — 2, так как по правилам поле не может стать полностью пустым(всегда остается хотя бы одна фишка), или полностью заполненным фишками(их дается 32 изначально и больше уже стать не может).
Это можно интерпретировать как то, что ходов у нас всего не более 8 589 934 590, что вполне перебираемо на домашнем компьютере. То есть нужно просто написать честный полный перебор.
Вторая попытка решения
Избавившись от рандомной составляющей алгоритм брутфорса принял следующий вид:

Псевдо блок-схема второй версии
Этот брутфорс написанный на C++ на всех известных мне оптимизациях в релиз-версии работал довольно резво, перебирая чуть ли не 200 000 ходов в секунду, а значит для полного перебора понадобилось бы (2^33 — 2) / (2 * 10^5) ≈ 12 часов. Но так как четыре варианта первого хода очевидно ведут к лишним повторениям, то мы делаем первый ход за программу, таким образом на 3/4 уменьшая количество ходов которые требуется перебрать(а значит и время работы). Программа занимала в оперативной памяти 2.6 МБ и довольно быстро перебрала 2 * 10^7 ходов и все бы ничего, но ответа до сих пор не было.
Самое веселье же было в том, что на этих двух миллиардах программа показывала что перебирает подветвь дерева всех возможных ходов которая образуется после 12 хода! Что-то тут не так, либо ошибка в брутфорсе, либо существуют такие различные ходы, которые приводят поле к одному состоянию, причем этих пересечений должно быть достаточно много.
Мои опасения подтвердились простейшей проверкой — разные ходы действительно могли приводить поле в одно и то же состояние.
Третья попытка решения
Значит нужно сохранять рассчитанные поля и при обнаружение повтора сразу сообщать, что их считать не нужно.

Псевдо блок-схема третьей версии с запоминанием рассчитанных полей
Только вот это дело оказалось весьма накладным, если хранить просчитанные поля в оперативной памяти, то получается в идеальном случае что нам потребуется 33 бита на одно поле, а их даже с учетом сделанного первого хода (2^33 — 2) / 4, то есть памяти понадобится на хранение всех просчитанных полей ((2^33 — 2) / 4) * 33) / (8 * 2^20) ≈ 8.5 ГБ, а у меня всего 4, из которых я могу дать программе лишь 2. Что же делать? Особенно если учесть, что 33 бита это теоретический минимум требуемый на игровое поле, а на практике их ведь постоянно нужно сравнивать, да и не как-то там, а побитово! Можно было поискать системные функции для побитового сравнения двух участков памяти, или попробовать сделать это ассемблерными вставками. Но все равно нужно ведь будет хранить еще и указатели на эти поля, то есть в 33 бита игрового поля + 4 байта указатель(на моей машине) на них + выравнивание структур в памяти — ну никак не уложиться даже с оперативкой в 16 ГБ. Можно было бы попробовать сделать хранение на диске, но тогда скорость перебора упала бы на непозволительный уровень…
В итоге было решено опробовать злосчастный std::vector bool для хранения рассчитанных полей и молиться о том, чтобы ход решения был найден до того, как иссякнет память.
Новый брутфорс перебирал около 7000 ходов в секунду, но этого оказалось вполне достаточно — по счастливому стечению обстоятельств я выловил аж 4 решения до того как выскочила плашка «Memory out». Память иссякла когда он перебрал пять миллионов ходов и находился при этом на подветви дерева всех возможных ходов которая образуется после 5 хода от начала игры.
Заключение
Вроде головоломка и решена своими силами, но недосказанность осталась. Решение вполне могло и не находится в перебранном мной диапазоне, тогда бы пришлось думать дольше, и возможно уже совсем в ином направление.
PS Перед публикацией нашел на хабре пост Применяем на практике знания, полученные на курсе MIT 6.00x (edx.org) часть которого повествовала о решение этой же задачи, но справедливости ради замечу что мы с автором того поста пошли разными путями.
PPS Возможно наличие ошибок и неточностей, буду признателен за поправки и советы.
#include <iostream> #include <fstream> #include <list> #include <string> #include <set> #include <vector> namespace China_Checkers { // Size of the game field unsigned x, y; // Class of game field for storage class Field { static const unsigned size; std::vector<bool> *field; bool is_copy; public: Field(bool** f) { field = new std::vector<bool>(size); // Transforming game field as matrix to vector for (unsigned i = 0; i < x; ++i) { for (unsigned j = 0; j < y; ++j) { if ((i > 1 && i < 5) && (j < 2 || j > 4)) field->push_back(f[i][j]); else if (j > 1 && j < 5) field->push_back(f[i][j]); } } } Field(Field& f) { f.is_copy = true; field = f.field; } Field &operator=(Field& f) { f.is_copy = true; field = f.field; } friend bool operator==(const Field& m1, const Field& m2) { if (m1.field == m2.field) return true; if ((*m1.field) != (*m2.field)) return false; return true; } friend bool operator<(const Field& m1, const Field& m2) { if ((*m1.field) < (*m2.field)) return true; return false; } ~Field() { if (is_copy) return; delete field; } }; const unsigned Field::size = 33; class China_Checkers_Hack { typedef bool** field ; // Type of a game field typedef std::pair<unsigned, unsigned> position; // Pair of x, y typedef std::pair<position, position> move ; // Type for a move (start position, end position) typedef std::pair<field, std::list<move*>*> step ; // Type for description one step of the bruteforce int win_condition; // Condition scoring std::list<field> path_to_win ; // Path to win std::set<Field> fld_buf ; // We checked these fields // To output a field in a stream friend std::ostream &operator<<(std::ostream &out, const field fld) { if (fld == nullptr) { std::cout << "Attempt to output nullptr field" << std::endl; system("pause"); return out; } for (unsigned j = 0; j < x ; ++j) { for (unsigned i = 0; i < y; ++i) { if (((i < 2) || (i > 4)) && ((j < 2) || (j > 4))) out << '*' << ' '; else out << fld[i][j] << ' '; } out << '\n' << std::flush; } return out; } // To display a some message void message(const std::string msg = "") { std::cout << "Message: " << msg << std::endl; system("pause"); } // Returns a pointer to a memory was allocated for a field, or nullptr in error case field alloc_mem_for_field() { field fld = nullptr; try { fld = new bool*[x]; for (unsigned i = 0; i < x; ++i) { fld[i] = new bool[y]; } } catch(std::bad_alloc) { fld = nullptr; message("Memory out in alloc_mem_for_field()"); } return fld; } // Copy a field and returns a reference on it, or nullptr in error case field copy_field(field fld_dest, const field fld_source) { if (fld_dest == nullptr) { if ((fld_dest = alloc_mem_for_field()) != nullptr) { for (unsigned i = 0; i < x; ++i) { for (unsigned j = 0; j < y; ++j) { fld_dest[i][j] = fld_source[i][j]; } } } } else { for (unsigned i = 0; i < x; ++i) { for (unsigned j = 0; j < y; ++j) { fld_dest[i][j] = fld_source[i][j]; } } } return fld_dest; } // First initialize of a field void init_field(field &fld) { for (unsigned i = 0; i < x; ++i) { for (unsigned j = 0; j < y; ++j) { if ( (((i == 0) || (i == 1)) || ((i == (x - 1)) || (i == (x - 2)))) && (((j == 0) || (j == 1)) || ((j == (y - 1)) || (j == (y - 2)))) ) fld[i][j] = (bool)2; // Not used else fld[i][j] = 1; } } // Make the first move to reduce space of moves on 3/4 fld[(x / 2)][(y / 2)] = 1; fld[(x / 2)][(y / 2 - 1)] = 0; fld[(x / 2)][(y / 2) - 2] = 0; } // Returns num of chips on field int IsWin(const field &fld) { int k = 0; for (unsigned i = 0; i < x; ++i) { for (unsigned j = 0; j < y; ++j) { if ( (i < 2 || i > 4) && (j < 2 || j > 4) ) continue; if (fld[i][j] == 1) ++k; } } return k; } // Free alloc memory void free_mem_from_field(field &fld) { if (fld == nullptr) { message("Attempt to delete nullptr in free_mem_from_field()"); return; } for (unsigned i = 0; i < y; ++i) { delete[] fld[i]; } delete[] fld; fld = nullptr; } // Returns true if a cell is empty bool IsCell(const field &fld, const int lx, const int ly) { if ( (lx > 0 && ly > 0) && (lx < (int)x && ly < (int)y) && (!((lx < 2 || lx > 4) && (ly < 2 || ly > 4))) ) { if (fld[(unsigned)lx][(unsigned)ly] == 0) return true; } return false; } // Returns true if a cell has a chip bool IsChip(const field &fld, const int lx, const int ly) { if ( (lx > 0 && ly > 0) && (lx < (int)x && ly < (int)y) && (!((lx < 2 || lx > 4) && (ly < 2 || ly > 4))) ) { if (fld[(unsigned)lx][(unsigned)ly] == 1) return true; } return false; } // Returns a pointer on a list of all moves for the current field // or nullptr if moves doesn't exist std::list<move*> *get_all_moves(const field &fld) { std::list<move*> *buf = nullptr; try { buf = new std::list<move*>; move* m = nullptr; for (int i = 0; (unsigned)i < x; ++i) { for (int j = 0; (unsigned)j < y; ++j) { if ((fld[i][j] == 1) && (!(((i < 2) || (i > 4)) && ((j < 2) || (j > 4))))) { if (IsCell(fld, i + 2, j)) { if (IsChip(fld, i + 1, j)) { m = new move(); m->first.first = i; m->first.second = j; m->second.first = i + 2; m->second.second = j; buf->push_back(m); } } if (IsCell(fld, i - 2, j)) { if (IsChip(fld, i - 1, j)) { m = new move(); m->first.first = i; m->first.second = j; m->second.first = i - 2; m->second.second = j; buf->push_back(m); } } if (IsCell(fld, i, j + 2)) { if (IsChip(fld, i, j + 1)) { m = new move(); m->first.first = i; m->first.second = j; m->second.first = i; m->second.second = j + 2; buf->push_back(m); } } if (IsCell(fld, i, j - 2)) { if (IsChip(fld, i, j - 1)) { m = new move(); m->first.first = i; m->first.second = j; m->second.first = i; m->second.second = j - 2; buf->push_back(m); } } } } } if (buf->size() == 0) { delete buf; buf = nullptr; } } catch (std::bad_alloc) { delete buf; buf = nullptr; message("Memory out in get_all_moves()"); } return buf; }//*/ // To make a move field make_move(field fld, const move &mv) { if (fld == nullptr) { message("Attempt to move on empty field in make_move()"); return fld; } if (mv.first.first != mv.second.first) { if (mv.first.first < mv.second.first) { fld[mv.first.first + 1][mv.second.second] = 0; } else fld[mv.first.first - 1][mv.second.second] = 0; } else if (mv.first.second != mv.second.second) { if (mv.first.second < mv.second.second) { fld[mv.first.first][mv.first.second + 1] = 0; } else fld[mv.first.first][mv.first.second - 1] = 0; } else { message("Move is incorrect, error in get_all_moves()"); return fld; } fld[mv.first.first][mv.first.second] = 0; fld[mv.second.first][mv.second.second] = 1; return fld; } // Recursive passage all the moves void bruteforce(field fld) { static bool display = false; static int lvl = 33; static time_t counter = 0; // Num of checked field ++counter; // Num 7000 is empirical // Once we get the conclusion of the 7000, it allows the processor // to be optimally loaded, and we see a progress if ((counter % 7000) == 0) { display = true; } if ( counter > 33 ) { int ibuf = path_to_win.size(); if (lvl > ibuf) lvl = ibuf; } std::list<move*> *b = get_all_moves(fld); step buf; if (b != nullptr) { buf = step(fld, b); path_to_win.push_back(fld); } else { int res = IsWin(fld); path_to_win.push_back(fld); if (display == true) { system("cls"); std::cout << "Min level: " << lvl << "\n<" << counter << '>' << "\nCurrent result:\n" << fld << std::endl; display = false; } if (res == win_condition) { std::cout << "Num of checked fields: " << fld_buf.size() << "\nSize of the path_to_win: " << path_to_win.size() << std::endl; std::ofstream winpath_to_win("path_to_win.txt"); if (winpath_to_win) { for (std::list<field>::const_iterator it = path_to_win.begin(); it != path_to_win.end(); ++it) { winpath_to_win << (*it) << std::endl; } winpath_to_win.close(); } for (std::list<field>::const_iterator it = path_to_win.begin(); it != path_to_win.end(); ++it) { std::cout << (*it) << std::endl; } system("pause"); } path_to_win.pop_back(); return; } field fwithmove = nullptr; for ( std::list<move*>::iterator it = buf.second->begin(); it != buf.second->end(); fwithmove = nullptr, buf.second->pop_front(), it = buf.second->begin() ) { fwithmove = copy_field(fwithmove, buf.first); // Make move in copy of the buf.first field, // buf.first field not changed and use to copy in next iteration make_move(fwithmove, *(*it)); Field *f = new Field(fwithmove); if (fld_buf.count(*f)) { free_mem_from_field(fwithmove); delete (*it); continue; } else fld_buf.insert(*f); delete f; bruteforce(fwithmove); free_mem_from_field(fwithmove); // Free alloc memory for the fwithmove field, where we did move delete (*it); // Free memory from under "move" in list<move*> which we did } path_to_win.pop_back(); delete buf.second; } public: China_Checkers_Hack(const int _x = 7, const int _y = 7, const int wincondition = 1) : win_condition(wincondition) { x = _x; y = _y; field main_field; if ((main_field = alloc_mem_for_field()) != nullptr) { init_field(main_field); bruteforce(main_field); free_mem_from_field(main_field); } else message("Memory out, your computer gonna update RAM!"); } ~China_Checkers_Hack() { for (std::list<field>::iterator it = path_to_win.begin(); it != path_to_win.end(); ++it) { free_mem_from_field(*it); } } }; } int main(int argc, char* argv[]) { China_Checkers::China_Checkers_Hack bruteforce; system("pause"); return 0; }
* * 1 0 1 * *
1 1 1 0 1 1 1
1 1 1 1 1 1 1
1 1 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 0 1 * *
1 0 0 1 1 1 1
1 1 1 1 1 1 1
1 1 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 0 1 * *
1 1 0 1 1 1 1
1 0 1 1 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 0 1 * *
0 0 1 1 1 1 1
1 0 1 1 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 1 1 * *
* * 1 0 1 * *
0 1 0 0 1 1 1
1 0 1 1 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 1 1 0 1 1 1
1 0 1 1 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
1 0 1 1 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
1 1 0 0 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
0 0 1 0 1 1 1
1 0 1 1 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
0 0 1 0 1 1 1
1 1 0 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
0 0 1 0 1 1 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
0 0 1 1 0 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 1 1 * *
* * 0 1 1 * *
0 0 0 0 1 1 1
0 0 1 0 0 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 1 * *
0 0 0 1 1 1 1
0 0 1 0 0 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 0 * *
0 0 0 1 0 1 1
0 0 1 0 1 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 0 * *
0 0 0 1 1 0 0
0 0 1 0 1 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 0 * *
0 0 0 0 0 1 0
0 0 1 0 1 0 1
0 0 1 0 1 1 1
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 0 * *
0 0 0 0 0 1 1
0 0 1 0 1 0 0
0 0 1 0 1 1 0
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 0 * *
0 0 0 0 1 0 0
0 0 1 0 1 0 0
0 0 1 0 1 1 0
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 1 * *
* * 0 0 1 * *
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 0 1 0 1 1 0
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 1 0 0
0 0 1 0 0 0 0
0 0 1 0 1 1 0
* * 1 1 1 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 1 0 0
0 0 1 0 1 0 0
0 0 1 0 0 1 0
* * 1 1 0 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 0 1 0 1 1 0
* * 1 1 0 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 0 1 1 0 0 0
* * 1 1 0 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 1 0 0 0 0 0
* * 1 1 0 * *
* * 1 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 1 1 0 0 0 0
* * 0 1 0 * *
* * 0 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 1 0 0 0 0 0
* * 1 1 0 * *
* * 0 1 1 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 1 0 0 0 0 0
* * 1 1 0 * *
* * 1 0 0 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 1 1 0 0 0 0
* * 0 1 0 * *
* * 0 0 0 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 1 0 0 0
* * 0 1 0 * *
* * 0 0 0 * *
* * 0 0 0 * *
* * 0 0 0 * *
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
* * 0 0 0 * *
* * 0 1 0 * *
ссылка на оригинал статьи http://habrahabr.ru/post/168469/
Добавить комментарий