The first row kills it
· algorithms
The action space is finite: one press per cell. Sequences of actions look huge. Two properties cut the plans to 2^{MN}. Then the first row makes the rest forced: 2^N.
The problem
AcWing 95, Flip Switch. Twenty-five lights in a grid. is on, is off. One step: pick a cell, toggle it, and toggle its four orthogonal neighbors — left, right, up, down, if they exist. Goal: every light on, in at most presses. If that cannot be done, the answer is .
A first board, then press the top-left cell, then the center:
1 0 1 1 1 0 1 1 1 1 0 1 1 1 10 1 1 0 1 1 1 1 0 1 1 1 0 0 11 0 1 1 1 → 1 0 1 1 1 → 1 1 0 0 11 0 0 0 0 1 0 0 0 0 1 0 1 0 01 1 0 1 1 1 1 0 1 1 1 1 0 1 1Top-left hits itself, its right neighbor, and the cell below. Center hits itself and all four neighbors. The rest of the board does not move.
Input: a positive integer (), the number of puzzles. Then blocks. Each block is five lines of five characters, each '0' or '1'. A blank line between blocks.
Output: lines. Each line is the minimum number of presses that turns that board all on, or if it takes more than .
Sample input:
30011101011100011101011100
1110111101111101111111111
0111111111111111111111111Sample output:
32-1The problem is . I still want to talk about a general board.
Plays look huge
At first I had no handle. Too many clicks.
The state is the board. Write for the set of boards, each lamp . An action is one press — not a string of presses. The action space is finite:
The transition function sends a board and a press to the board after that press. Write for the map .
A play is a sequence of actions, any length, letters reusable. That is a word over , not an element of :
The final state after that word is
Countably many words: finitely many of each length, so list length , then length , then length , and so on. Still too many to brute-force. You need the two properties below.
Then two properties.
Press twice, nothing happened
. Pressing the same cell twice is the identity on states. The board does not move.
Order does not matter
. Any two consecutive actions can be swapped and the final state is the same. Hence any permutation of a sequence of actions leaves the state unchanged.
Proof
If we only fire one of the two actions, there is nothing to prove. So take two presses, and . Pick an arbitrary lamp. Three cases.
- Neither action reaches it. Swap does nothing.
- Exactly one action reaches it. It still flips once, either order.
- Both actions reach it. It flips twice either way, so it stays as it was.
The lamp was arbitrary, so the whole board agrees: the two maps and are equal.
Those two shrink the sequences, not . Swap until the actions sit in a fixed order — left to right, top to bottom — and cancel every double. What remains is one bit per cell:
skip, press. A reduced plan is a matrix in . Apply the press at exactly when , in that order. reduced plans. was finite the whole time.
The first row kills it
One more property, and that kills it.
Once the first-row bits are chosen, every later is determined if you want all lights on.
The first row is the only row you still get to choose. Everything under it is a chase.
Why. You have already chosen . Those presses (or skips) are done. What can still touch row ? An action on row would be going back on the choice. An action on row or lower never reaches row : it flips itself, its left and right, and the cell above it, which is still only row . The only later action that can still flip a lamp on row is the cell standing directly underneath, in row .
So each leftover on row is an order, not a puzzle. If lamp is still off, you must set . Skip it and that is dead forever. If lamp is already on, you must set : pressing would turn it off, and nothing later can save it.
That writes the whole of row . Now look at row . Same story: the only people who can still fix it live in row , directly below. And so on, down the board.
Last row: nobody lives underneath. If a is still sitting there when the chase arrives, the first-row choice was a dead end. for that branch.
A small board, three by three. Suppose the first row is already pressed, and this is what you are looking at:
0 1 01 1 11 1 1Row has two debts, columns and . So . After :
1 1 00 0 10 1 1After :
1 1 10 1 00 1 0Row is clean. Row now owes columns and . So . After :
1 1 11 1 01 0 0After :
1 1 11 1 11 1 1All on. You never chose row or row . The two zeros on top wrote the whole script.
If the last row had come out with a , that would not mean “try something else downstairs.” There is no downstairs. It would mean this first row was wrong, and you try another of the first rows.
So the thing you actually enumerate is the first row. searches. Not . For that is first rows.
I used to play this as a kid. Some little game in a browser. I clicked by feel until the lights went on. Now it is like being shown how the trick is done. Simple.
The board
A row is only five bits. I stored each row as a size_t mask, five of them in an array. Bit is the leftmost lamp — the same order print_board walks.
const int kNumRows = 5;const int kNumCols = 5;const int kMaxPresses = 6;const size_t kRowAllOnMask = (1 << kNumCols) - 1;
using Board = std::array<size_t, kNumRows>;kRowAllOnMask is 0b11111. A row is all on exactly when it equals that mask.
Building a board from five strings: if the character is '1', set that bit.
Board make_board(const std::array<std::string_view, kNumRows>& rows) { Board board{}; for (size_t row = 0; row < kNumRows; ++row) { for (size_t col = 0; col < kNumCols; ++col) { if (rows[row][col] == '1') { board[row] |= 1 << col; } } } return board;}
bool is_light_on(const Board& board, size_t row, size_t col) { return (board[row] & (1 << col)) > 0;}Flip, then press
One lamp: XOR the bit, written the long way. If it is on, clear it. If it is off, set it.
void flip_light(Board& board, size_t row, size_t col) { size_t bit_mask = 1 << col; if ((board[row] & bit_mask) > 0) { board[row] &= ~bit_mask; } else { board[row] |= bit_mask; }}A press is five flips, or fewer on the border: the cell itself, then left, right, above, below, each only if that neighbor exists.
void press_switch(Board& board, size_t row, size_t col) { flip_light(board, row, col);
if (col > 0) { flip_light(board, row, col - 1); }
if (col < kNumCols - 1) { flip_light(board, row, col + 1); }
if (row > 0) { flip_light(board, row - 1, col); }
if (row < kNumRows - 1) { flip_light(board, row + 1, col); }}Press twice at the same cell and you have again. That is the involution, sitting in the code: press_switch is , its own inverse, so the first-row search can press, recurse, then press again to undo.
Chase down, enumerate the first row
Once the first row is chosen, walk rows through . If the lamp above is still off, press here. That is forced by . If we already used more than presses, give up. When the chase ends, the last row still has to be all on — nothing sits under it to save it.
The board is passed by value. The copy is the one we wreck. The original first-row board stays.
int complete_board(Board board, int press_count) { for (size_t row = 1; row < kNumRows; ++row) { for (size_t col = 0; col < kNumCols; ++col) { if (!is_light_on(board, row - 1, col)) { press_switch(board, row, col); ++press_count; if (press_count > kMaxPresses) { return -1; } } } }
if ((board[kNumRows - 1] & kRowAllOnMask) != kRowAllOnMask) { return -1; }
return press_count;}First row: at column col, try press, then try skip. After a press, call press_switch again to revoke it, then go down the skip branch. When col runs off the row, chase.
int min_presses(Board& board, size_t col, int press_count) { if (col >= kNumCols) { return complete_board(board, press_count); }
int min_press_count = -1;
press_switch(board, 0, col); int press_count_if_pressed = min_presses(board, col + 1, press_count + 1); press_switch(board, 0, col);
if (press_count_if_pressed != -1) { min_press_count = press_count_if_pressed; }
int press_count_if_skipped = min_presses(board, col + 1, press_count); if (min_press_count == -1 || (press_count_if_skipped != -1 && press_count_if_skipped < min_press_count)) { min_press_count = press_count_if_skipped; }
return min_press_count;}The three sample boards go in as string rows, same as the input. Answers , , .