宫水三叶的刷题日记

V1

2022/06/20阅读:10主题:前端之巅同款

【宫水三叶的刷题日记】464. 我能赢吗

题目描述

这是 LeetCode 上的 464. 我能赢吗 ,难度为 中等

Tag : 「博弈论 DP」、「记忆化搜索」、「状态压缩」

在 "100 game" 这个游戏中,两名玩家轮流选择从 的任意整数,累计整数和,先使得累计整数和 达到或超过  的玩家,即为胜者。

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

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

给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家是否能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。

示例 1:

输入:maxChoosableInteger = 10, desiredTotal = 11

输出:false

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

示例 2:

输入:maxChoosableInteger = 10, desiredTotal = 0

输出:true

示例 3:

输入:maxChoosableInteger = 10, desiredTotal = 1

输出:true

提示:

二维博弈论 DP(TLE)

这是一道博弈论 DP 的题,为了方便,我们使用 来表示 ,使用 来表示

由于 数据范围为 ,且每个数只能选一次,我们可以使用一个二进制数 来表示 范围内的被选择的数的情况:二进制表示中 的位置代表数已被选择,否则代表尚未选择。

首先朴素二维状态表示相对容易想到:定义 为当前已被选择的数为 ,轮数为 时,「原始回合的先手」能否获胜( 代表能, 代表不能),其中 开始,通过 的奇偶性可知是原始回合的先手还是后手。

设计递归函数来实现「记忆化搜索」,函数 int dfs(int state, int tot, int k) 表示当前状态为 对应累计和, 代表轮数,最终答案通过判断 dfs(0, 0, 0) 是否为 来得知。

转移过程中,如果发现当前回合的决策,能够直接使得累积和超过 ,说明当前回合玩家获胜;或者如果当前决策能够导致下一回合的玩家失败的话,当前回合玩家也获胜,否则当前玩家失败。

代码:

class Solution {
    int n, t;
    int[][] f = new int[1 << 20][2];
    // 1 true / -1 false
    int dfs(int state, int tot, int k) {
        if (state == ((1 << n) - 1) && tot < t) return -1;
        if (f[state][k % 2] != 0return f[state][k % 2];
        int hope = k % 2 == 0 ? 1 : -1;
        for (int i = 0; i < n; i++) {
            if (((state >> i) & 1) == 1continue;
            if (tot + i + 1 >= t) return f[state][k % 2] = hope;
            if (dfs(state | (1 << i), tot + i + 1, k + 1) == hope) return f[state][k % 2] = hope;
        }
        return f[state][k % 2] = -hope;
    }
    public boolean canIWin(int _n, int _t) {
        n = _n; t = _t;
        if (t == 0return true;
        return dfs(000) == 1;
    }
}
  • 时间复杂度:共有 个状态,每个状态转移需要 复杂度,整体复杂度为
  • 空间复杂度:

优化状态表示

进一步发现,若能优化轮数维度,可以有效减少一半的计算量,我们调整状态定义为:定义 为当前状态为 ,「当前先手」能否获胜( 代表能, 代表不能)。

同时调整递归函数为 ,最终答案通过判断 dfs(0, 0) 是否为 来得知。

注意这里调整的重点在于:将记录「原始回合的先后手发起 和 原始回合的先后手获胜情况」调整为「当前回合发起 和 当前回合获胜情况」。

代码:

class Solution {
    int n, t;
    int[] f = new int[1 << 20];
    // 1 true / -1 false
    int dfs(int state, int tot) {
        if (f[state] != 0return f[state];
        for (int i = 0; i < n; i++) {
            if (((state >> i) & 1) == 1continue;
            if (tot + i + 1 >= t) return f[state] = 1;
            if (dfs(state | (1 << i), tot + i + 1) == -1return f[state] = 1;
        }
        return f[state] = -1;
    }
    public boolean canIWin(int _n, int _t) {
        n = _n; t = _t;
        if (n * (n + 1) / 2 < t) return false;
        if (t == 0return true;
        return dfs(00) == 1;
    }
}
  • 时间复杂度:共有 个状态,每个状态转移需要 复杂度,整体复杂度为
  • 空间复杂度:

最后

这是我们「刷穿 LeetCode」系列文章的第 No.464 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关的资料可访问排版精明的 合集新基地 🎉🎉

分类:

后端

标签:

数据结构与算法

作者介绍

宫水三叶的刷题日记
V1