加载中...
464-我能赢吗(Can I Win)
发表于:2021-12-03 | 分类: 中等
字数统计: 2.4k | 阅读时长: 10分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/can-i-win

英文原文

In the "100 game" two players take turns adding, to a running total, any integer from 1 to 10. The player who first causes the running total to reach or exceed 100 wins.

What if we change the game so that players cannot re-use integers?

For example, two players might take turns drawing from a common pool of numbers from 1 to 15 without replacement until they reach a total >= 100.

Given two integers maxChoosableInteger and desiredTotal, return true if the first player to move can force a win, otherwise, return false. Assume both players play optimally.

 

Example 1:

Input: maxChoosableInteger = 10, desiredTotal = 11
Output: false
Explanation:
No matter which integer the first player choose, the first player will lose.
The first player can choose an integer from 1 up to 10.
If the first player choose 1, the second player can only choose integers from 2 up to 10.
The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal.
Same with other integers chosen by the first player, the second player will always win.

Example 2:

Input: maxChoosableInteger = 10, desiredTotal = 0
Output: true

Example 3:

Input: maxChoosableInteger = 10, desiredTotal = 1
Output: true

 

Constraints:

  • 1 <= maxChoosableInteger <= 20
  • 0 <= desiredTotal <= 300

中文题目

在 "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),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。

通过代码

高赞题解

先读懂题意:

“稳赢”指的是,假设有两个人A, B,A先抽整数然后轮到B,再轮到A…,每次都根据要凑的和选择最有利于自己赢的数(并不一定越大越好,所以说是一种博弈),如果存在A走一步之后, B无论怎么走能还是A能赢的情况,那么就说A能稳赢(而不是所有的情况下)。

例如maxChoosableInteger = 10,desiredTotal=3 那么就存在A抽3,然后B抽其他数都赢不了的情况,因为A已经赢了(可以理解为B就不用再抽了),所以A可以稳赢。

又如:maxChoosableInteger = 10,desiredTotal = 11 那么A第一轮选1(此时选得越小就越有利于自己赢),那么第二个玩家可以选择10(此时选择11-1就有利于自己赢)来赢得胜利,那么A就不能稳赢了。

下面提供两种解法(解决的思路一致,见注释)

法1:记忆化回溯


public class Solution {



    /**

     * 记忆化回溯(也称为递归+备忘录),自顶向下

     * 采用记忆化后的时间复杂度为O(2^n)(如果不进行记忆的话,时间复杂度将是O(n!)),可以理解为已经缩成了只有一个分支了

     * 然后为什么要进行记忆化:

     * 因为我们发现,例如[2,3]和[3,2]之后的玩家选择状态都是一样的,都是可以从除了2,3之外的

     * 数字进行选择,那么就可以对选择2和3后第一个玩家能不能赢进行记忆存储

     * 这里采用state[]数组存储每个数字是否都被选过,选过则记录为1,然后我们将state.toString()

     * 使得[2,3]和[3,2]它们的结果都是一样的"0011",作为key,存储在HashMap中,value是选了2和3

     * 之后第一个玩家是否稳赢

     * @param maxChoosableInteger

     * @param desiredTotal

     * @return

     */

    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {

        if (maxChoosableInteger >= desiredTotal) return true;

        if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) return false; //1,..maxChoosable数列总和都比目标和小

        int[] state = new int[maxChoosableInteger + 1];  //state[1]=1表示1被选了



        return backtraceWitMemo(state, desiredTotal, new HashMap<String, Boolean>());

    }



    private boolean backtraceWitMemo(int[] state, int desiredTotal, HashMap<String, Boolean> map) {

        String key = Arrays.toString(state); //这里比较关键,如何表示这个唯一的状态,例如[2,3]和[3,2]都是"0011",状态一样

        if (map.containsKey(key)) return map.get(key);  //如果已经记忆了这样下去的输赢结果,记忆是为了防止如[2,3],[3,2]之后的[1,4,5,..]这个选择区间被重复计算



        for (int i = 1; i < state.length; i++){

            if (state[i] == 0){ //如果这个数字i还没有被选中

                state[i] = 1;

                //如果当前选了i已经赢了或者选了i还没赢但是后面对方选择输了

                if (desiredTotal - i <= 0 || !backtraceWitMemo(state, desiredTotal - i, map)) {

                    map.put(key, true);

                    state[i] = 0; //在返回之前回溯

                    return true;

                }

                //如果不能赢也要记得回溯

                state[i] = 0;

            }

        }

        //如果都赢不了

        map.put(key, false);

        return false;

    }

}

法2:状压DP(DFS+记忆)

(这里需要一些位操作知识,我把涉及到的位运算原因都写了出来)


/**

 * @Description:

 * 由于状态不可用数组进行传递【在递归当中会受到改变,不能准确定位当前状态】,故在此处用Int的位表示状态(1表示用过,0表示未用过)

 * 这里采用DP状态压缩的方式,思想与回溯类似,只是这里的状态被压缩成了一个bitArray了

 * 状态压缩,我们可以用二进制的第i位的0或者1来表示i这个数字的选取与否,这样所有数字的选取状态就可以用一个数来很方便的表示,

 * 题目说了不超过20位,所以这里就可以用一个int来表示状态state,通过state来判断状态是否一致,进而进行记忆化的存取

 */

public class Solution {



    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {



        if (maxChoosableInteger >= desiredTotal) return true;

        if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) return false;

        /**

         *  dp表示"每个"取数(A和B共同作用的结果)状态下的输赢

         *  例如只有1,2两个数选择,那么 (1 << 2) - 1 = 4 - 1 = 3种状态表示:

         *  01,10,11分别表示:A和B已经选了1,已经选了2,已经选了1、2状态下,A的输赢情况

         *  并且可见这个表示所有状态的dp数组的每个状态元素的长度为maxChoosableInteger位的二进制数

         */

        Boolean[] dp = new Boolean[(1 << maxChoosableInteger) - 1];

        return dfs(maxChoosableInteger, desiredTotal, 0, dp);

    }



    /**

     * @param maxChoosableInteger 选择的数的范围[1,2,...maxChoosableInteger]

     * @param desiredTotal 目标和

     * @param state 当前状态的十进制表示(记录着可能不止一个数被选择的状态)

     * @param dp 记录所有状态

     * @return

     */

    private boolean dfs(int maxChoosableInteger, int desiredTotal, int state, Boolean[] dp) {

        if (dp[state] != null)

            return dp[state];

        /**

         * 例如maxChoosableInteger=2,选择的数只有1,2两个,二进制只要两位就可以表示他们的选择状态

         * 最大位为2(第2位),也就是1 << (2 - 1)的结果,所以最大的位可以表示为  1 << (maxChoosableInteger - 1)

         * 最小的位可以表示为 1 << (1 - 1),也就是1(第1位)

         * 这里i表示括号的范围

         */

        for (int i = 1; i <= maxChoosableInteger; i++){

            //当前待抉择的位,这里的tmp十进制只有一位为1,用来判断其为1的位,对于state是否也是在该位上为1

            //用以表示该位(数字)是否被使用过

            /**

             * (&运算规则,都1才为1)

             * 例如,i=3, tmp = 4, state = 3;

             *  100

             * &011

             * =0  表示该位没有被使用过,也就是第三位没有被使用过,即数字3 (i)没有被使用过

             */

            int tmp = (1 << (i - 1));

            if ((tmp & state) == 0){  //该位没有被使用过

                //如果当前选了i已经赢了或者选了i还没赢但是后面对方选择输了,tmp|state表示进行状态的更新

                /**

                 * 例如

                 *  100

                 * |011

                 * =111

                 */

                //注意这里并没有像回溯一样进行状态的(赋值化的)更新、回溯

                //其实这里是隐含了回溯的,我们通过参数传递更新后的state

                //但是我们在这个调用者这里的state还是没有进行更新的,所以

                //就相当于回溯了状态。

                if (desiredTotal - i <= 0 || !dfs(maxChoosableInteger, desiredTotal - i, tmp|state, dp)) {

                    dp[state] = true;

                    return true;

                }

            }

        }

        //如果都赢不了

        dp[state] = false;

        return false;

    }

}

有不足的地方,欢迎指正

统计信息

通过次数 提交次数 AC比率
11939 33685 35.4%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言

相似题目

题目 难度
翻转游戏 II 中等
猜数字大小 II 中等
预测赢家 中等
上一篇:
462-最少移动次数使数组元素相等 II(Minimum Moves to Equal Array Elements II)
下一篇:
466-统计重复个数(Count The Repetitions)
本文目录
本文目录