首页 > 代码库 > 《数据结构与算法分析:C语言描述》复习——第十章“算法设计技巧”——拿石头游戏

《数据结构与算法分析:C语言描述》复习——第十章“算法设计技巧”——拿石头游戏

2014.07.08 00:08

简介:

  本章里没有讲到这个内容,是我在看书的时候回忆起了自己被问过的一道面试题。当时觉得特别难,现在回想起来才知道是自己无知。

  如果有50颗石子,两人轮流拿。每次可以从其中拿走1,2,4或者8颗。谁拿走了最后一颗,谁就输了(输或者赢根本无所谓)。

  请问,先手或者后手有什么必胜策略吗?

描述:

  如果你是参加过ACM训练的人,肯定觉得小儿科,于是你可以不用看了。

  但对于我这样的外行,在听到这道题目的时候,脑子里一片混乱,甚至打算在草稿纸上硬推出一个结果。

  作为一个软件工程师,如果你在面试的时候以求出正确答案为傲,那么你就错了。因为人家期望的是你要么用理论解释答案,要么用代码解决问题。

  至于具体答案是几?Don’t care。

  

  巴什博弈几乎是博弈问题里最简单的一种了:给定n颗石头,俩人轮流拿走石头,如果每次拿走1~m颗石头的话,谁必胜或者必败呢?

  这些问题有共同点,有不同点。而当你遇到陌生问题的时候,要做的肯定不是用无数脑细胞和人肉计算来得到一个答案,而是用你已有的理论和工具来分析问题。

  我们学过些什么呢?博弈论我肯定不懂了,我就懂一点动态规划的基本知识。

  于是有了下面几条思路:

    1. 不论是{1,2,4,8}还是1~m,我至少有选择的余地。我可以决定下一步怎么走。

    2. 我为什么会输?因为我不论选择哪一步,都是输。我为自己的利益着想,自然是不得已才会输。

    3. 我为什么会赢?因为我至少能找到一步,让我赢。我为自己的利益着想,能赢我一定会赢。

    4. 那么n=50的时候,我到底能不能赢?

  于是动态规划就来了,dp[n]表示n个石头的情况下的游戏结果。

  dp[n]和谁有关?必然是dp[n-1]、dp[n-2]、dp[n-4]、dp[n-8],因为可选的操作只有{1,2,4,8}。

  我们用-1表示对手赢,+1表示我赢,0表示未知(也就是公平竞争,没有人必胜或者必败)。

  如果dp[n-1]、dp[n-2]、dp[n-4]、dp[n-8]全都是+1,表示前一步都是我赢。那么不论我拿走几颗石头到达n,我都是输。(我不想输,所以我是迫不得已的)

  如果dp[n-1]、dp[n-2]、dp[n-4]、dp[n-8]至少有一个是-1,表示前一步有可能是对手赢。那么我会选择对手赢的那条路,因为那样我能赢。(只要能赢,我就会赢)

  而对于结果为0的情况,不作讨论。

  这种由“与”和“或”构成的递推关系式,就是动态规划的关键了。

  尽管这种动态规划的思路写成代码后效率并不高,但思想却很重要。后来我查阅了资料才发现这种动态规划的思想就是“博弈树”。

  

  此外,这个问题虽然是我临时想起来的,但作为一种博弈问题,自然也会和本章的Min-Max策略发生关联。接下来的一篇博文会进行介绍。

  下面的代码给出了这种拿石头问题的程序解法,与其在草稿纸上硬算,不如把代码写给面试官看。既然是码农,自然应该用代码说话了。

实现:

 1 // This is said to be an interview question about game. 2 // This solution is by dynamic programming. 3 // Description: 4 //    If there‘re 50 stones at the beginning, you and your component take turns to take away 1, 2, 4 or 8 stones. 5 //    Whoever takes away the last stone loses the game. 6 //    Does this game has a winning strategy for any side? 7 #include <algorithm> 8 #include <iostream> 9 #include <vector>10 using namespace std;11 12 void game(const int n, const vector<int> &move, vector<int> &dp)13 {14     int i;15     // dp[i] means the game result when the total number of stones is i.16     // 1 for offensive win, -1 for defensive win, 0 for uncertain(fair play).17     dp.resize(n + 1, 0);18     19     int n_move = (int)move.size();20     int n_win;21     dp[move[0]] = -1;22     23     int j;24     for (i = move[0] + 1; i <= n; ++i) {25         n_win = 0;26         for (j = 0; j < n_move; ++j) {27             if (i - move[j] <= 0) {28                 ++n_win;29                 continue;30             }31             if (dp[i - move[j]] == -1) {32                 // one of the parent node in the game tree is lose, 33                 // then you win.34                 dp[i] = 1;35                 break;36             } else if (dp[i - move[j]] == 1) {37                 ++n_win;38             }39         }40         if (j == n_move && n_win == n_move) {41             // all parent nodes in the game tree are wins, then you lose42             dp[i] = -1;43         }44     }45 }46 47 int main()48 {49     vector<int> dp;50     vector<int> move;51     int n;52     int i;53     int tmp;54     55     while (cin >> n && n > 0) {56         while (cin >> tmp && tmp > 0) {57             move.push_back(tmp);58         }59         game(n, move, dp);60         sort(move.begin(), move.end());61         for (i = 1; i <= n; ++i) {62             cout << i << :;63             switch(dp[i]) {64             case -1:65                 cout << "Defensive win." << endl;66                 break;67             case 0:68                 cout << "Fair play." << endl;69                 break;70             case 1:71                 cout << "Offensive win." << endl;72                 break;73             }74         }75         76         dp.clear();77         move.clear();78     }79     80     return 0;81 }