The definition of a nim game is generally as follows: There are several limited stones in the pile, two players take turns to take the stones, each time you can choose a pile of stones and take away any stones, the last person who took the stone won the prize.
To solve this general problem, there are two basic concepts:
Winning position: A policy exists from this status to win.
Losing position: there is no strategy to win from this status.
Note that the current status changes to the next status after an action is executed. The basic principles for solving the problem are as follows:
1. Each end state is losing position. This is because the current player cannot perform legal actions.
2. There is a status in which the subsequent status is losing position, which is winning position.
3. All subsequent statuses are in the winning position state as losing position.
Intuitively, as long as the subsequent status is losing position, the current player can execute this action and leave the losing position to the opponent to win.
Based on this principle, Bouton's solution provides a further specific algorithm: the State is losing position when and only when the number of stones per heap is different or the accumulated value is 0.
Abstract The problem into a directed acyclic graph, and the same conclusion can be drawn from the SD-Grundy theorem.
For more details, let's look at two examples of Nim problems and their variants:
SRM 330: longlongnim
The feature is that the number of sub-groups that can be obtained in each step can only be one of the sets. The above three basic principles can be used to solve this problem. Consider the current number of coins as the status, calculate whether each status is winning position or losing position, and then accumulate the number.
#include <iostream>#include <vector>#include <string>#include <algorithm>using namespace std;// trivial implementationclass LongLongNim {private:int isWiningPos(int idx, vector <int> moves){if (0 == idx) {return 0;}for (vector<int>::iterator it = moves.begin(); it != moves.end(); it++) {if (idx - *it < 0)continue;if (!isWiningPos(idx - *it, moves))return 1;}return 0;}public:int numberOfWins(int maxN, vector <int> moves) {int cnt = 0;for (int i = 1; i <= maxN; ++i) {if (!isWiningPos(i, moves))cnt++;}return cnt;}};
However, it is obviously inefficient to calculate each State from the beginning. Let's reuse the existing results and take advantage of the condition that the action given in the question is at most 22.
const int MAX_MOVE_STEP = 22;const int MAX_STATE = 1 << MAX_MOVE_STEP;class LongLongNim {public:int numberOfWins(int maxN, vector <int> moves) {int cnt = 0;unsigned int state = MAX_STATE - 1;unsigned int moves_bit_vec = 0;for (vector<int>::iterator it = moves.begin(); it != moves.end(); ++it) {moves_bit_vec |= (1 << (*it - 1));}for (int i = 1; i < maxN; ++i) {if ((state & moves_bit_vec) == moves_bit_vec) { //losing pos for the first playerstate <<= 1;state = state;cnt++;} else {state <<= 1;state |= 1;}}return cnt;}};
In this way, the calculated intermediate state results are reused, but they are not optimized. Because the appearance of winning position and losing position changes cyclically with the status sequence. Because the question is given a range of up to 22 moves, each State is only determined by the first 22 States. Therefore, when the pattern of 22 consecutive States appears the same twice, we can regard it as a cycle, and determine that there will be the same cycle, and the States in the middle of the cycle will not be counted.
const int MAX_MOVE_STEP = 22;const int MAX_STATE = 1 << MAX_MOVE_STEP;int res_idx[MAX_STATE] = {0};// pattern for most recent continous 22 states int res_cnt[MAX_STATE] = {0};class LongLongNim {public:int numberOfWins(int maxN, vector <int> moves) {int cnt = 0;unsigned int state = MAX_STATE - 1; // init configure, 1 for winning position, 0 for losing postion. Initially, winning position for the second player, namely, losing position for the first player.unsigned int moves_bit_vec = 0;for (vector<int>::iterator it = moves.begin(); it != moves.end(); ++it) {moves_bit_vec |= (1 << (*it - 1));}memset(res_idx, 0, sizeof(int) * MAX_STATE);memset(res_cnt, 0, sizeof(int) * MAX_STATE);for (int i = 1; i <= maxN; ++i) {if (res_idx[state]) { // one cycle found, there is repetitive patternint cycle_len = i - res_idx[state];int res_in_cycle = cnt - res_cnt[state];int cycle_num = (maxN - i) / cycle_len;i += cycle_num * cycle_len; // move forward by cycle_num of cycle_lencnt += cycle_num * res_in_cycle; // result augmented by cycle_num * res_in_cycle} else {// record the result for i res_idx[state] = i; res_cnt[state] = cnt;}if ((state & moves_bit_vec) == moves_bit_vec) {// all the successors after moves are winning position, so the i is losing postion for the first player, thus the winning position for the second player.state = (state << 1) & (MAX_STATE - 1);// pop the oldest one, push the new one with the value 0cnt++;} else {state = ((state << 1) & (MAX_STATE - 1)) + 1; // push the new one with the value 1}}return cnt;}};
SRM 309: stonegamestrategist
It is characteristic that a rule is added, and the number of each heap is incrementing and cannot be violated from start to end. This condition makes Bouton's solution unable to be directly applied, which requires converting the original problem as follows. The specific method is to subtract the number of each heap in the original problem, and then take the element at the odd position. For example, (3, 4, 5) is changed to (3, 1, 1), and the odd number is (3, 1 ). In the original problem, the even point of the odd number corresponds to the reduction of elements in the new problem. In the original problem, the even point of the stone moves to the odd position, which corresponds to the increase of elements in the new problem. The new problem is equivalent to the original problem. When the odd position is 0, all elements in the original problem must be exclusive or accumulative.
#include <iostream> #include <vector> #include <algorithm> #include <string> #include <string.h> using namespace std; class StoneGameStrategist {private:int isLosingPos(vector<int> a) {int i;int sum = 0;vector<int> b;// construct the new Nim game.b.push_back(a[0]);for (i = 1; i < a.size(); ++i) b.push_back(a[i] - a[i - 1]);for (i = b.size() - 1; i >= 0; i -= 2) sum ^= b[i];return sum == 0;}public:string play(vector <int> piles) {int i, j;int found = 0;int res_pos, res_num;string ret;char buf[256];for (j = 1; j <= 1000; ++j) {// range given by the problem statement.for (i = 0; i < piles.size(); ++i) {vector<int> t = piles;t[i] -= j;if (t[i] < 0 || (i != 0 && t[i] < t[i - 1])) // lead to an illegal state.continue;if (isLosingPos(t)) { // could lead to losing position, so the current position is winning one.found = 1;res_pos = i;res_num = j;break;}}if (found)break;}if (!found)ret = "YOU LOSE";else {sprintf(buf, "TAKE %d STONES FROM PILE %d", res_num, res_pos);ret = buf;}return ret;}};