找出游戏的获胜者

标签: 递归 队列 数组 数学 模拟

难度: Medium

共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序1n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。

游戏遵循如下规则:

  1. 从第 1 名小伙伴所在位置 开始
  2. 沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
  3. 你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
  4. 如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
  5. 否则,圈子中最后一名小伙伴赢得游戏。

给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。

示例 1:

输入:n = 5, k = 2
输出:3
解释:游戏运行步骤如下:
1) 从小伙伴 1 开始。
2) 顺时针数 2 名小伙伴,也就是小伙伴 1 和 2 。
3) 小伙伴 2 离开圈子。下一次从小伙伴 3 开始。
4) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 4 。
5) 小伙伴 4 离开圈子。下一次从小伙伴 5 开始。
6) 顺时针数 2 名小伙伴,也就是小伙伴 5 和 1 。
7) 小伙伴 1 离开圈子。下一次从小伙伴 3 开始。
8) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 5 。
9) 小伙伴 5 离开圈子。只剩下小伙伴 3 。所以小伙伴 3 是游戏的获胜者。

示例 2:

输入:n = 6, k = 5
输出:1
解释:小伙伴离开圈子的顺序:5、4、6、2、3 。小伙伴 1 是游戏的获胜者。

提示:

  • 1 <= k <= n <= 500

进阶:你能否使用线性时间复杂度和常数空间复杂度解决此问题?

Submission

运行时间: 28 ms

内存: 0.0 MB

class Solution:
    def findTheWinner(self, n: int, k: int) -> int:
        circle = [i for i in range(1, n+1)]
        last_idx = 0
        while len(circle) > 1: 
            last_idx = (last_idx + k - 1) % len(circle)
            del circle[last_idx]
            last_idx = last_idx % len(circle)
        return circle[0]

Explain

该题解采用模拟的方式解决约瑟夫问题。首先,创建一个列表,其中包含从1到n的所有小伙伴的编号。然后,进入一个循环,每次循环中根据给定的k值找到并删除应当离开游戏的小伙伴。循环直到列表中只剩下一个元素,即游戏的获胜者。在每次删除元素后,更新下一次计数的起始索引。通过不断调整索引以模拟环形结构,确保计数可以在圆圈中正确进行。

时间复杂度: O(n^2)

空间复杂度: O(n)

class Solution:
    def findTheWinner(self, n: int, k: int) -> int:
        # 初始化从1到n的编号列表
        circle = [i for i in range(1, n+1)]
        # 设置初始索引为0
        last_idx = 0
        # 循环直到列表中只剩一个元素
        while len(circle) > 1:
            # 计算需要删除的小伙伴的索引位置
            last_idx = (last_idx + k - 1) % len(circle)
            # 删除指定索引的小伙伴
            del circle[last_idx]
            # 更新索引,以便下一轮计数
            last_idx = last_idx % len(circle)
        # 返回列表中唯一剩下的元素,即获胜者
        return circle[0]

Explore

这个计算是为了确保索引值在列表的有效范围内,避免索引越界。由于列表的长度在每次删除一个元素后会减少1,使用取余操作`% len(circle)`可以确保即使索引值last_idx等于当前列表长度(这种情况会发生在last_idx指向列表的最后一个元素并被删除时),索引也会被正确地重置到列表的开始位置,从而模拟环形结构。这样可以保证游戏的计数在圆圈中持续进行而不会出错。

模拟环形结构通过数组和索引调整是一种直观且易于理解的方法,但它涉及到频繁的数组元素删除操作,这在时间复杂度上是不够高效的(每次删除操作平均时间复杂度为O(n))。有一种更高效的方法是使用链表(尤其是循环链表),因为在链表中删除节点的操作时间复杂度可以降低至O(1)。此外,数学方法尤其是约瑟夫环问题的递推公式也可以无需模拟过程直接计算出结果,这种方法在理论上是最快的,时间复杂度为O(n),但需要理解和实现相应的数学推导。

题解中的模拟方法可以处理k值大于n的情况,因为它在每次计算删除索引时使用了模运算`last_idx = (last_idx + k - 1) % len(circle)`。这种使用模运算的方法可以有效地处理k值大于列表长度的情况,将计数自动限制在当前列表长度的范围内。因此,不论k的值有多大,算法都能正确地计算出每轮应当删除的元素的索引,并继续模拟直到只剩一个元素。