leetcode 博弈问题总结

概述

给定游戏背景和规则,问先手:
1.有没有机会赢
2.能不能稳赢

一般可以使用递归+记忆化搜索解决
定义dfs:
1)参数是当前状态,返回值是当前状态下我先手能不能赢/稳赢
2)参数是区间等,返回值是在这个区间中我能拿到的最大利益
在主函数中拿到最大利益后进行判断能不能赢

1.对于有机会赢,在递归中,我先手,如果你有可能输,那我就返回true;
2.对于稳赢,在递归中,我先手,如果你有可能赢,我就返回false;

类型一.区间博弈问题:

背景:

给定数组piles,我和你玩一个游戏:
每次只能从数组的两端拿东西,我先拿,问能不能赢?

解题套路

1.求前缀和
for (int i = 0; i < piles.size(); ++i) {
            if (i == 0) sum[i + 1] = piles[i];
            else sum[i + 1] = sum[i] + piles[i];
        }

此时sum[i+1]代表数组0-i的总和,sum[数组大小]表示数组总和
设区间下标从1开始
给定区间[L,R]
区间总和为数组下标[L-1,R-1]的总和
为(数组[0-R-1]的总和-数组[0-L-1]的总和)
即为:sum[R]-sum[L-1]

2.定义dp区间[L][R]来记忆化dfs;
std::memset(dp, -1, sizeof dp);
3.定义dfs(区间[L,R])为我在这个区间中能拿到的最大值

dfs的逻辑:
1.递归的边界:L==R
2.记忆化判断:看所求的dfs结果有没有保存过,有就直接返回
3.dfs结果计算:
我在这个区间可以拿到的最大值=
max(区间总和-你在(我拿走左端后)剩余区间可以拿的最大值,
区间总和-你在(我拿走右端后)剩余区间可以拿的最大值);

或者

我在这个区间可以拿到的最大值=
区间总和-min(你在(我拿走左端后)剩余区间可以拿的最大值,
你在(我拿走右端后)剩余区间可以拿的最大值);

4.返回结果前保存到dp区间中

877. 石子游戏

亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

 

示例:

输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
 

提示:

2 <= piles.length <= 500
piles.length 是偶数。
1 <= piles[i] <= 500
sum(piles) 是奇数
题解
class Solution {
public:
    static const int N = 500 + 5;
    int sum[N], dp[N][N];
    // 从区间[L, R]这个状态, 最多能拿多少石子
    int dfs(int L, int R) {
        // 递归的边界
        if (L == R) return sum[R] - sum[L - 1];
        // 记忆化
        if (dp[L][R] != -1) return dp[L][R];

        int ret = std::max(sum[R] - sum[L - 1] - dfs(L + 1, R), sum[R] - sum[L - 1] - dfs(L, R - 1));
        return dp[L][R] = ret;
    }

    bool stoneGame(vector<int>& piles) {
        // 前缀和
        for (int i = 0; i < piles.size(); ++i) {
            if (i == 0) sum[i + 1] = piles[i];
            else sum[i + 1] = sum[i] + piles[i];
        }
        std::memset(dp, -1, sizeof dp);
        int ret1 = dfs(1, piles.size()), ret2 = sum[piles.size()] - ret1;
        return ret1 > ret2;
    }
};
class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        int n = piles.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = 0; i < n; ++i) dp[i][i] = piles[i];
        for (int len = 1; len < n; ++len) {
            for (int i = 0, j = i + len; j < n; ++i, ++j)
                dp[i][j] = max(piles[i] - dp[i + 1][j], -dp[i][j - 1] + piles[j]);
        }
        return dp[0][n - 1] > 0;
    }
};

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        vector<int> dp = piles; int len = 1;
        while (len < piles.size())
        {
            for (int l = 0, r = len++; r < piles.size(); ++l, ++r)
                dp[l] = max(piles[l] - dp[l + 1], piles[r] - dp[l]);
        }
        return dp[0] > 0;
    }
};

486. 预测赢家

给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。

给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

 

示例 1:

输入:[1, 5, 2]
输出:False
解释:一开始,玩家1可以从1和2中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 False 。
示例 2:

输入:[1, 5, 233, 7]
输出:True
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
     最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。
题解
class Solution {
public:
    int dp[21][21];
    int sum(const vector<int> &nums, int L, int R) {
        int sum = 0;
        for(int i = L; i <= R; i++) {
            sum += nums[i-1];
        }
        return sum;
    }
    int dfs(int L, int R, const vector<int> &nums) {
        if(dp[L][R] != -1) {
            return dp[L][R];
        }
        if(L == R) {
            return dp[L][R] = nums[L-1];
        }
        return dp[L][R] = sum(nums, L, R) - min(dfs(L+1, R, nums), dfs(L, R-1, nums));
    }
    bool PredictTheWinner(vector<int>& nums) {
        memset(dp, -1, sizeof(dp));
        return dfs(1, nums.size(), nums)*2 >= sum(nums, 1, nums.size());
    }
};

类型二.给定增量和最终结果

背景

1.拿n个东西,每次可以最多拿m个,我先拿,问我能不能拿到最后一个
2.放n个东西,每次可以最多放m个,从0开始,我先放,问我能不能放最后一个

解题套路

1.无限制条件

直接return n%(m+1)==0;

2.限制条件:每次拿/放m个:m不能重复

当前状态为:{访问过的数集合,当前拿/放了多少个}
1)定义 bool dfs(当前状态) 为:从当前状态出发,我能不能赢
a.递归终止:我的“当前拿/放了多少个”满足胜利条件
b.记忆化:当前状态以前有没有计算过,计算过就直接返回
c.计算结果:遍历从1-m,
如果遇到没被访问过的,我这次就拿这个数,更新状态,轮到你了;如果你在这个状态下能赢(dfs(下一状态)),那我就赢不了,返回false;
d.在返回结果前将结果保存

292. Nim 游戏
你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。

你们是聪明人,每一步都是最优解。 编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。

示例:

输入: 4
输出: false 
解释: 如果堆中有 4 块石头,那么你永远不会赢得比赛;
     因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。
题解

回到必胜态的定义,必胜态的后继状态中只要要有一个必败态才可以。因为本题中一次可以取一到三块石子。故一个必败态 i,可以导致 i+1,i+2,i+3 为必胜态,而 i + 4 肯定是必败态。

又因为 0 是最小的必败态,这样我们可以得出一个结论,当 n%4 == 0 时,先手必败,否则先手必胜。

class Solution {
public:
    bool canWinNim(int n) {
        return n%4 != 0;
    }
};
464. 我能赢吗
在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和达到或超过 100 的玩家,即为胜者。

如果我们将游戏规则改为 “玩家不能重复使用整数” 呢?

例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。

给定一个整数 maxChoosableInteger (整数池中可选择的最大数)和另一个整数 desiredTotal(累计和),判断先出手的玩家是否能稳赢(假设两位玩家游戏时都表现最佳)?

你可以假设 maxChoosableInteger 不会大于 20, desiredTotal 不会大于 300。

示例:

输入:
maxChoosableInteger = 10
desiredTotal = 11

输出:
false

解释:
无论第一个玩家选择哪个整数,他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
题解:
class Solution {
public:
    int maxChoosableInteger, desiredTotal;
    std::unordered_map<unsigned long long, bool> mp;

    // 当前这个状态下, 能否稳赢. total_sum记录当前的和, bitset记录那些数已经被选择过了
    bool dfs(int total_sum, std::bitset<25> bs) {
        // 递归的边界
        if (total_sum >= desiredTotal) return true;
        // 记忆化
        if (mp.find(bs.to_ullong()) != mp.end()) return mp[bs.to_ullong()];    
        
        bool ret = true;
        // 假设当前状态是A
        // 这里就是上文说的, 枚举B走的所以情况的(用一个for), 只有循环里B都不能稳赢, A才能稳赢. 
        for (int i = 1; i <= maxChoosableInteger; ++i) {
            if (bs[i]) continue;
            std::bitset<25> bs_tmp = bs;
            bs_tmp[i] = 1;
            // 这里就是枚举B 
            if (dfs(total_sum + i, bs_tmp)) {
                ret = false;
                break;
            }
        }
        return mp[bs.to_ullong()] = ret;
    }

    bool canIWin(int a_, int b_) {
        maxChoosableInteger = a_;
        desiredTotal = b_;
        // 所有数加起来都小于desiredTotal, 则稳输
        if ((maxChoosableInteger + 1) * maxChoosableInteger * 0.5 < desiredTotal)
            return false;
        for (int i = 1; i <= maxChoosableInteger; ++i) {
            bitset<25> bs;
            bs[i] = 1;
            if (dfs(i, bs)) return true;
        }
        return false;
    }
};
class Solution {
public:
    int maxChoosableInteger, desiredTotal;
    std::map<std::pair<int, int>, int> mp;

    // 从(state, sum)这个状态出发能否获胜
    bool dfs(int state, int sum) {
        // 递归的边界
        if (sum >= desiredTotal) return true;
        // 记忆化
        if (mp.find({state, sum}) != mp.end()) return mp[{state, sum}];

        int ret = true;
        for (int i = 1; i <= maxChoosableInteger; ++i) {
            if ((state & (1 << i)) == 0) {
                if (dfs(state | (1 << i), sum + i)) {
                    ret = false;
                    break;
                }
            }
        }
        return mp[{state, sum}] = ret;
    }

    bool canIWin(int a, int b) {
        maxChoosableInteger = a, desiredTotal = b;
        // 如果所有数都被选了(即所有数相加)都还小于desiredTotal, 显然是false 
        if ((a + 1) * a / 2 < b) return false;

        for (int i = 1; i <= a; ++i) {
            if (dfs(1 << i, i)) return true;
        }    
        return false;
    }
};
  1. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:

我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。

每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。

然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。

示例:

n = 10, 我选择了8.

第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。

游戏结束。8 就是我选的数字。

你最终要支付 5 + 7 + 9 = 21 块钱。
给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏
class Solution {
public:
    int getMoneyAmount(int n) {
        if(n==1)
            return 0;
        //定义矩阵
        int dp[n+1][n+1];
        //初始化“\”
        for(int i=0;i<=n;i++){
            for(int j=0;j<=n;j++){
                dp[i][j]=INT_MAX;
            }
        }
        //定义基础值dp[i][i]
        for(int i=0;i<=n;i++){
            dp[i][i]=0;
        }

        //按列来,从第2列开始
        for(int j=2;j<=n;j++){
            //按行来,从下往上
            for(int i=j-1;i>=1;i--){
                //算除了两端的每一个分割点
                for(int k=i+1;k<=j-1;k++){
                    dp[i][j]=min(k+max(dp[i][k-1],dp[k+1][j]),dp[i][j]);
                }
                //算两端
                dp[i][j]=min(dp[i][j],i+dp[i+1][j]);
                dp[i][j]=min(dp[i][j],j+dp[i][j-1]);
            }
        }
        return dp[1][n];
    }
};
class Solution {
public:
    bool canWin(string s) {
        const int N = s.size();
        if (N <= 1) return false;
        if (win_.count(s) && win_[s]) return true;
        for (int i = 0; i < N - 1; ++i) {
            if (s[i] == '+' && s[i + 1] == '+') {
                string flip = s.substr(0, i) + "--" + s.substr(i + 2);
                if (!canWin(flip)) {
                    win_[s] = true;
                    return true;
                }
            }
        }
        return false;
    }
private:
    unordered_map<string, bool> win_;
};

参考:力扣题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值