问题
first time here.
I'm working on a Peg Puzzle php solver, using recursion. For small and simple boards, I get the desired results (the script solves the puzzle correctly), but for larger and full boards (i.e. all slots but one occupied) I get a php timeout. I need to get it to work with a 7x7 board, with the following layout:
x x 1 1 1 x x
x x 1 1 1 x x
1 1 1 1 1 1 1
1 1 1 0 1 1 1
1 1 1 1 1 1 1
x x 1 1 1 x x
x x 1 1 1 x x
Where 'x' is unusable, '1' is a slot with a peg and '0' is a free slot.
The board is represented by 7x7 array (an array of arrays). I traverse it one key at a time, doing the following checks:
Is this key's value '1'? If yes, is the following key's value '1' too and the following '0' ? (which means there's a peg to take, and there's a space to move the first one). If yes, then I create a copy of the board and apply these changes, and resend it to the function. If not, I check in another direction (currently checking order is: right, left, up, down).
Recursion ends when the script can't find a valid path from that position. Then, I do a check to see if there's only one peg left (which would mean that the board is solved), or if there are pegs left (which would mean that the board wasn't solved). In the latter, the whole path should be discarded.
I would copy&paste my code, but as it's still a little messy I preferred to explain it.
I tried Rodolphe Courtier's algorithm (here), with the same results.
Thanks in advance!
EDIT: Ok, so far making the DFS non-recursive didn't quite help (there are still a lot of steps involved). So now I'm thinking about checking the board for patterns that yield a unsolvable puzzle first, and if that's the case I instruct the script not to bother traversing it in the first place. As before, will post my findings.
回答1:
I've written this before in both c++ and c#. I can tell you that the 7x7 array is not best. Consider a standard depth first search algorithm and a board representation as a bitboard. I can probably post a full solution in c but for a different board if you like.
Also given that the solution requires depth first search you really can't get around the recursion. My first try did something like what you're doing and it was slow. The bitboard implementation completed in seconds not minutes.
EDIT:
This was my solution for a 15 peg board that was in the shape of a triangle. The start board had all pegs in place except for the top of the triangle, and the winning solution is defined as last peg ending up in the top position. The algorithm should work identically for you except that you need to redefine the tables for what moves are available and what moves are legal.
Basic explanation: The board is arranged like this:
p1
p2 p3
p4 p5 p6
p7 p8 p9 pa
pb pc pd pe pf
The each location is mapped to one bit on a 16-bit int. The board starts with all bits on except p1. A "move" is a triplet of three bits. For example, (p1, p2, p4) is a possible move. The move is "legal" if p1,p2 bit is set and p4 is clear, OR p2,p4 is set and p1 is clear. There's lookup tables for all moves, and the legal move definitions.
In order to do a depth first search, we need:
- the end state (one bit on: I 'cheated' by defining it as only p1 on, which is trivial)
- make and undo moves (xor the current board against the candidate move, again trivial)
- generate candidate set of moves (again, some binary xor/and operations. The only complicated part, which I can maybe elaborate on later if needed...)
The code:
#include <vector>
#include <iostream>
using namespace std;
typedef short state_t;
struct Move {
short move;
const char * desc;
};
typedef Move move_t;
struct Options {
short moves[4];
int size;
};
// name the bits
# define P1 1
# define P2 1 << 1
# define P3 1 << 2
# define P4 1 << 3
# define P5 1 << 4
# define P6 1 << 5
# define P7 1 << 6
# define P8 1 << 7
# define P9 1 << 8
# define P10 1 << 9
# define P11 1 << 10
# define P12 1 << 11
# define P13 1 << 12
# define P14 1 << 13
# define P15 1 << 14
// not valid location
# define P16 1 << 15
// move triplets
Options options[15] = {
{{P1|P2|P4, P1|P3|P6}, 2},
{{P2|P4|P7, P2|P5|P9},2},
{{P3|P5|P8, P3|P6|P10},2},
{{P1|P2|P4, P4|P7|P11, P4|P5|P6, P4|P8|P13},4},
{{P5|P8|P12, P5|P9|P14},2},
{{P1|P3|P6, P4|P5|P6, P6|P9|P13, P6|P10|P15},4},
{{P7|P4|P2, P7|P8|P9},2},
{{P8|P5|P3,P8|P9|P10},2},
{{P9|P8|P7,P9|P5|P2},2},
{{P10|P6|P3,P10|P9|P8},2},
{{P11|P7|P4,P11|P12|P13},2},
{{P12|P8|P5,P12|P13|P14},2},
{{P13|P12|P11,P13|P14|P15,P13|P8|P4,P13|P9|P6},4},
{{P14|P9|P5,P14|P13|P12},2},
{{P15|P10|P6,P15|P14|P13},2}
};
// legal moves
Options legal[15] = {
{{P1|P2, P1|P3}, 2},
{{P2|P4, P2|P5},2},
{{P3|P5, P3|P6},2},
{{P4|P2, P4|P7, P4|P5, P4|P8},4},
{{P5|P8,P5|P9},2},
{{P6|P3, P6|P5, P6|P9, P6|P10}, 4},
{{P7|P4, P7|P8},2},
{{P8|P5, P8|P9},2},
{{P9|P8,P9|P5},2},
{{P10|P6,P10|P9},2},
{{P11|P7,P11|P12},2},
{{P12|P8,P12|P13},2},
{{P13|P12,P13|P14,P13|P8,P13|P9},4},
{{P14|P9,P14|P13},2},
{{P15|P10,P15|P14},2}
};
// for printing solution
struct OptionDesc {
const char* name[4];
int size;
};
OptionDesc desc[15] = {
{{"p1 => p4", "p1 => p6"}, 2},
{{"p2 => p7", "p2 => p9"}, 2},
{{"p3 => p8", "p3 => p10"}, 2},
{{"p4 => p1", "p4 => p11", "p4 => p6", "p4 => p13"}, 4},
{{"p5 => p12", "p5 => p14"}, 2},
{{"p6 => p1", "p6 => p4", "p6 => p13", "p6 => p15"}, 4},
{{"p7 => p2", "p7 => p9"}, 2},
{{"p8 => p3", "p8 => p10"}, 2},
{{"p9 => p7", "p9 => p2"}, 2},
{{"p10 => p3", "p10 => p8"}, 2},
{{"p11 => p4", "p11 => p13"}, 2},
{{"p12 => p5", "p12 => p14"}, 2},
{{"p13 => p11", "p13 => p15", "p13 => p4", "p13 => p6"}, 4},
{{"p14 => p5", "p14 => p12"}, 2},
{{"p15 => p6", "p15 => p13"}, 2}
};
int LEGAL_COUNT = sizeof (legal) / sizeof (Options);
state_t START = P2|P3|P4|P5|P6|P7|P8|P9|P10|P11|P12|P13|P14|P15;
// make move: just xor
inline void make_move(state_t& s, move_t m)
{
s ^= m.move;
}
// undo move: just xor
inline void unmake_move (state_t& s, move_t m)
{
s ^= m.move;
}
// define end state as peg in top position
inline bool end_state (state_t s)
{
return (s ^ START) == (START|P1);
}
// generates moves from table of legal moves, and table of all possible move options
inline void generate_moves(state_t s, vector<move_t>& moves)
{
for (int i = 0; i < LEGAL_COUNT; i++) {
for (int j = 0; j < legal[i].size; j++) {
short L = legal[i].moves[j];
short M = L ^ options[i].moves[j];
if ((s & L) == L && (s & M) == 0) {
move_t m;
m.move = options[i].moves[j];
m.desc = desc[i].name[j];
moves.push_back(m);
}
}
}
}
// basic depth first search:
bool dfs (state_t& s, int& count)
{
bool found = false;
if (end_state(s)) {
count++;
return true;
}
vector<move_t> moves;
generate_moves(s, moves);
for (vector<move_t>::iterator it = moves.begin();
it != moves.end(); it++) {
make_move (s, *it);
found = dfs(s,count);
unmake_move(s, *it);
if (found && 0) {
cout << it->desc << endl;
return true;
}
}
return false;
}
void init(state_t& s)
{
s = START;
}
int main(int argc, char* argv[])
{
state_t s;
int count = 0;
init(s);
bool solved = dfs (s, count);
//cout << "solved = " << solved << endl;
cout << "solutions = " << count << endl;
char c;
cin >> c;
return 0;
}
回答2:
Based on your description, I suspect the major slow points are a) recursion b) copying the board a bunch of times. If you can put it in a loop instead of using recursion, that's likely to help. If you can make do with one or few copies of the board (say, one board per potential path, passing it by reference to the function), that's also likely to help.
Your runtime is going to increase exponentially based on the size of the board and number of pegs; what you want to do is make it increase as slowly as possible.
For instance, say you have a 20x20 board with all but 1 spot full. There's (20^2-1) 399 pegs. Each of those pegs might be iterating through each of the other 398 pegs to find a solution: that means your recursive loop iterates 20x20x20x20 times, making a new 400-peg board each time. That's a lot of loops, and that's a lot of boards!
Based on the code you linked, the only optimization I saw right away is to have the board only figure out one move at a time, try that move and see where it goes instead of calculating all moves at each stage. That's a linear optimization though, not an exponential one; it might help to a degree but it's not going to help very much. (eg, instead of doing, for instance, n^2 calculations to figure out each move, the board would do on average (n^2)/2--still a O(n^2).
Also, the getMoves function itself is very slow--it seems to me it could be rewritten to be significantly faster, especially if it's being called 160,000 times.
来源:https://stackoverflow.com/questions/6393391/timeout-on-a-php-peg-puzzle-solver