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


    跟博弈的必败必胜一样的分析,后手存在必败则先手必胜,先手全为必胜则先手必败。
    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);
        }
    };
    
    个性签名:时间会解决一切
  • 相关阅读:
    在你设计中可能用到的20个杂志 PSD 原型
    Gradify
    CamanJS – 提供各种图片处理的 JavaScript 库
    免费素材:包含 250+ 组件的 DO UI Kit
    24个很赞的 Node.js 免费教程和在线指南
    Dynamics.js
    Page Scroll Effects
    Slides
    15款加速 Web 开发的 JavaScript 框架
    Wee – 为现代 Web 开发打造的 CSS 脚手架
  • 原文地址:https://www.cnblogs.com/lfri/p/15769082.html
Copyright © 2020-2023  润新知