博弈树-极大极小搜索算法

跟博弈的必败必胜一样的分析,后手存在必败则先手必胜,先手全为必胜则先手必败。
DFS时对后手的返回值做上述两种判断就行。

LC 913. 猫和老鼠

方法一:必胜态分析+DFS

思路:每次走一步,鼠走的时候,如果之后存在鼠必胜(即猫必败),则当前鼠必胜(相当于沿着必胜的方式一直走);如果之后都是猫必胜,则当前鼠必败;如果不是这两种情况,说明是平局。
DFS递归的出口:(鼠,猫,步数),关键:走2n步没结果说明是平局

class Solution {
public:
    int dp[55][55][105];
    // (鼠,猫,步数)
    int dfs(vector<vector<int>>& graph,int i, int j, int k) {
        // cout << i << " " << j << " " << k << endl;
        int& ret = dp[i][j][k];
        if(ret != -1)  return ret;
        if(i == j)  return ret = 2;
        if(i == 0)  return ret = 1;
        if(k > 2*graph.size())  return ret = 0;
        bool all_one = true, all_two = true;
        if(k%2 == 0) {          // 鼠走
            for(int u : graph[i]) {
                int tmp = dfs(graph, u, j, k+1);
                if(tmp == 1)  return ret=1;
                if(tmp == 0)  all_two = false;
            }
            if(all_two)  return ret = 2;
            return ret = 0;
        } else {     // 猫走
            for(int v : graph[j]) {
                if(v == 0)  continue;
                int tmp = dfs(graph, i, v, k+1);
                if(tmp == 2)  return ret=2;
                if(tmp == 0)  all_one = false;
            }
            if(all_one)  return ret = 1;
            return ret = 0;
        }
    }

    int catMouseGame(vector<vector<int>>& graph) {
        int n = graph.size();
        memset(dp, -1, sizeof(dp));
        return dfs(graph, 1, 2, 0);
    }
};

方法二:极大极小博弈+DFS

最单纯的极大极小算法

局面估价函数:我们给每个局面(state)规定一个估价函数值 f,评价它对于己方的有利程度。胜利的局面的估价函数值为 +oo,而失败的局面的估价函数值为–oo。

  • Max 局面:假设这个局面轮到己方走,有多种决策可以选择,其中每种决策都导致一种子局面(sub-state)。由于决策权在我们手中,当然是选择估价函数值 f 最大的子局面,因此该局面的估价函数值等于子局面 f 值的最大值,把这样的局面称为 max 局面。
  • Min 局面:假设这个局面轮到对方走,它也有多种决策可以选择,其中每种决策都导致一种子局面(sub-state)。但由于决策权在对方手中,在最坏的情况下,对方当然是选择估价函数值 f 最小的子局面,因此该局面的估价函数值等于子局面 f 值的最小值,把这样的局面称为 max 局面。
  • 终结局面:胜负已分(假设没有和局)

大概是这样的博弈树,一层取最小,一层取最大。

我们假设老鼠赢为 11 分,猫赢为 33 分,和局为 22 分,题目就可以转化为最大最小博弈。对于老鼠来说,得分要尽可能小,而对于猫来说,得分要尽可能高。【参考题解喜帖-极大极小搜索

class Solution {
public:
    int dp[55][55][105];
    // (鼠,猫,步数)
    // 鼠1 猫3 平局2
    int dfs(vector<vector<int>>& graph,int i, int j, int k) {
        // cout << i << " " << j << " " << k << endl;
        int& ret = dp[i][j][k];
        if(ret != -1)  return ret;
        if(i == j)  return ret = 3;
        if(i == 0)  return ret = 1;
        if(k > 2*graph.size())  return ret = 2;
        if(k%2 == 0) {          // 鼠走
            ret = INT_MAX;
            for(int u : graph[i]) {
                ret = min(ret, dfs(graph, u, j, k+1));
                if(ret == 1)  return ret;  // 必胜,可以不管后面的了,优化
            }
            return ret;
        } else {     // 猫走
            ret = -INT_MAX;
            for(int v : graph[j]) {
                if(v == 0)  continue;
                ret = max(ret, dfs(graph, i, v, k+1));
                if(ret == 3)  return 3;  // 优化
            }
            return ret;
        }
    }

    int catMouseGame(vector<vector<int>>& graph) {
        int n = graph.size();
        memset(dp, -1, sizeof(dp));
        int res = dfs(graph, 1, 2, 0);
        if(res == 1)  return 1;
        else if(res == 2)  return 0;
        return 2;
    }
};

1406. 石子游戏 III

方法一:极大极小+DFS

方法:同样也可以看成一个极大极小博弈问题,将后手的转成负数,先手越大越好,后手越小越好。【参考 题解Netcan-极小化极大算法
dfs的含义是对turn=0返回最大值,对turn=1返回最小值

class Solution {
public:

    int dp[50005][2];
    const int INF = 0x3f3f3f3f;
    // 拿到了第i堆,轮到了turn
    int dfs(vector<int>& stoneValue, int i, int turn) {
        int n = stoneValue.size();
        if(i >= n)  return 0;
        int& ret = dp[i][turn];
        if(ret != INF)  return ret;

        int cur = 0;
        if(turn == 0) {  // turn=0, Alice拿
            ret = -INF;
            for(int k = 0;k < 3 && i+k< n;k++) {
                cur += stoneValue[i+k];
                ret = max(ret, cur + dfs(stoneValue, i+k+1, !turn));
            }
        } else {
            ret = INF;
            for(int k = 0;k < 3 && i+k< n;k++) {
                cur -= stoneValue[i+k];
                ret = min(ret, cur + dfs(stoneValue, i+k+1, !turn));
            }
        }
        return ret;
    }

    string stoneGameIII(vector<int>& stoneValue) {
        memset(dp, INF, sizeof(dp)); // -0x3f3f3f3f不能这样填充
        int res = dfs(stoneValue, 0, 0);
        cout << "res: " << res << endl;
        if(res > 0)  return "Alice";
        if(res < 0)  return "Bob";
        return "Tie";
    }
};

方法二:极大极小简化版

由于turn每次都是反转过来,上面的代码进一步简化(记忆化不能丢,不让会超时)
当前值减去后手的-每种情况最大值,这里DFS的含义就是(i, turn)的最大值

class Solution {
public:

    int dp[50005][2];
    const int INF = 0x3f3f3f3f;
    // 拿到了第i堆,轮到了turn
    int dfs(vector<int>& stoneValue, int i, int turn) {
        int n = stoneValue.size();
        if(i >= n)  return 0;
        int& ret = dp[i][turn];
        if(ret != INF)  return ret;

        int cur = 0;
        ret = -INF;
        for(int k = 0;k < 3 && i+k< n;k++) {
            cur += stoneValue[i+k];
            ret = max(ret, cur - dfs(stoneValue, i+k+1, !turn));
        }
        return ret;
    }

    string stoneGameIII(vector<int>& stoneValue) {
        memset(dp, INF, sizeof(dp)); // -0x3f3f3f3f不能这样填充
        int res = dfs(stoneValue, 0, 0);
        cout << "res: " << res << endl;
        if(res > 0)  return "Alice";
        if(res < 0)  return "Bob";
        return "Tie";
    }
};

方法三:线性递推,改成DP

很容易发现,上面的状态可以倒推得到,而且是线性递推(不像猫捉老鼠是图结构),所以可以改成dp。

class Solution {
public:

    string stoneGameIII(vector<int>& stoneValue) {
        int n = stoneValue.size();
        vector<vector<int>>dp(n+1, vector<int>(2, INT_MIN));
        dp[n][0] = dp[n][1] = 0; 

        for(int i = n-1;i>=0;i--) {
            for(int turn = 0;turn < 2;turn++) {
                int sum = 0;
                for(int k = 0;k < 3 && i+k < n;k++) {
                    sum += stoneValue[i+k];
                    dp[i][turn] = max(dp[i][turn], sum - dp[i+k+1][!turn]);
                }
            }
        }
        int res = dp[0][0];
        // cout << "res: " << res << endl;
        if(res > 0)  return "Alice";
        if(res < 0)  return "Bob";
        return "Tie";
    }
};

LC 1510. 石子游戏 IV

方法:博弈最简单的例题了,线性的,没有平局。这里采用必胜必败分析,也可以用极大极小(必胜必败算是其特例),也可以递推。

class Solution {
public:
    int dp[100005];
    int dfs(int n) {
        // cout << "n: " << n << endl;
        if(n == 0)  return 0;
        int& ret = dp[n];
        if(ret != -1)  return ret;
        
        int gen = sqrt(n);
        for(int i = 1;i <= gen;i++) {
            if(dfs(n-i*i) == 0)  return ret=1; // 后手存在必败,则先手必胜
        }
        return ret=0;  // 由于没有平局,一定为必败
    }
    bool winnerSquareGame(int n) {
        memset(dp, -1, sizeof(dp));
        return dfs(n);
    }
};
posted @ 2022-01-05 22:11  Rogn  阅读(1186)  评论(0编辑  收藏  举报