环形闯关游戏

标签: 位运算 并查集 数组 堆(优先队列)

难度: Hard

「力扣挑战赛」中有一个由 `N` 个关卡组成的**环形**闯关游戏,关卡编号为 `0`~`N-1`,编号 `0` 的关卡和编号 `N-1` 的关卡相邻。每个关卡均有积分要求,`challenge[i]` 表示挑战编号 `i` 的关卡最少需要拥有的积分。 ![图片.png](https://pic.leetcode-cn.com/1630392170-ucncVS-%E5%9B%BE%E7%89%87.png){:width="240px"} 小扣想要挑战关卡,闯关具体规则如下: - 初始小扣可以指定其中一个关卡为「开启」状态,其余关卡将处于「未开启」状态。 - 小扣可以挑战处于「开启」状态且**满足最少积分要求**的关卡,若小扣挑战该关卡前积分为 `score`,挑战结束后,积分将增长为 `score|challenge[i]`(即位运算中的 `"OR"` 运算) - 在挑战某个关卡后,该关卡两侧相邻的关卡将会开启(若之前未开启) 请帮助小扣进行计算,初始最少需要多少积分,可以挑战 **环形闯关游戏** 的所有关卡。 **示例1:** > 输入:`challenge = [5,4,6,2,7]` > > 输出:`4` > > 解释: 初始选择编号 3 的关卡开启,积分为 4 >挑战编号 3 的关卡,积分变为 $4 | 2 = 6$,开启 2、4 处的关卡 >挑战编号 2 的关卡,积分变为 $6 | 6 = 6$,开启 1 处的关卡 >挑战编号 1 的关卡,积分变为 $6 | 4 = 6$,开启 0 处的关卡 >挑战编号 0 的关卡,积分变为 $6 | 5 = 7$ >挑战编号 4 的关卡,顺利完成全部的关卡 **示例2:** > 输入:`challenge = [12,7,11,3,9]` > > 输出:`8` > > 解释: 初始选择编号 3 的关卡开启,积分为 8 >挑战编号 3 的关卡,积分变为 $8 | 3 = 11$,开启 2、4 处的关卡 >挑战编号 2 的关卡,积分变为 $11 | 11 = 11$,开启 1 处的关卡 >挑战编号 4 的关卡,积分变为 $11 | 9 = 11$,开启 0 处的关卡 >挑战编号 1 的关卡,积分变为 $11 | 7 = 15$ >挑战编号 0 的关卡,顺利完成全部的关卡 **示例3:** > 输入:`challenge = [1,1,1]` > > 输出:`1` **提示:** - `1 <= challenge.length <= 5*10^4` - `1 <= challenge[i] <= 10^14`

Submission

运行时间: 441 ms

内存: 32.2 MB

from sortedcontainers import SortedList
from typing import List
from collections import defaultdict, deque
from itertools import accumulate
import heapq
import sys

'''
1 设n为结果 max_n/2 <= n <= max
2 m为去头 结果为 max_n/2 | m
3 最大 - 其它的| 必在 m 里
4 <
4 dp[0][n]=s d
'''
class Solution:
    def log(self, *s):
        pass
    
    def ringGame(self, challenge: List[int]) -> int:
        n=len(challenge)
        max_value=challenge[0]
        max_index=0
        for i in range(1,len(challenge)):
            if challenge[i]>max_value:
                max_value=challenge[i]
                max_index=i
        challenge = challenge[max_index:]+challenge+challenge[:max_index+1]
        n3=len(challenge)
        def check(m) :
            i = 1
            while i < n3:
                # 尝试以关卡i为起点
                if challenge[i] > m :
                    i += 1
                    continue

                s, l, r = m|score_left[i]|score_right[i], idx_left[i], idx_right[i]
                while True :
                    if r-l > n : 
                        return True # 已挑战完所以关卡
                    # 向两边挑战关卡,并更新积分
                    if challenge[l] <= s :
                        s |= score_left[l]
                        l = idx_left[l]
                    if challenge[r] <= s :
                        s |= score_right[r]
                        r = idx_right[r]
                    else : break
                i = r # 扩展经过的点都可以不必再尝试作为起点;

            return False


        # 预计算出以某个关卡为起点(也以它的挑战分为初始分),单纯向左或向右能扩展多远和扩展后的积分
        score_left, idx_left = challenge[:], [0]*n3
        for i in range(n3) :
            j = i-1
            while j >= 0 and challenge[j] <= score_left[i] :
                score_left[i] |= score_left[j]
                j = idx_left[j]
            idx_left[i] = j

        score_right, idx_right = challenge[:], [0]*n3
        for i in range(n3-1, -1, -1) :
            j = i+1
            while j < n3 and challenge[j] <= score_left[i] :
                score_right[i] |= score_right[j]
                j = idx_right[j]
            idx_right[i] = j
        # self.log(challenge)
        # self.log(score_right)
        # self.log(idx_right)
        # 从高到低确定答案的二进制位
        bit = 1 << (max_value.bit_length()-1)
        res = bit
        while bit:
            bit >>= 1
            if not check(res | (bit-1)) : 
                res |= bit
        return res


    def baoli(self,challenge):
        def util(ret,i):
            visited=[False]*len(challenge)
            stacks=[]
            heapq.heappush(stacks,[challenge[i],i])
            while len(stacks):
                next_v,next_i=heapq.heappop(stacks)
                if next_v>ret:
                    return False
                visited[next_i]=True
                ret = ret|next_v
                next_i_right=(next_i+1)%len(challenge)
                next_i_left=(next_i-1+len(challenge))%len(challenge)
                if not visited[next_i_right]:
                    heapq.heappush(stacks,[challenge[next_i_right],next_i_right])
                if not visited[next_i_left]:
                    heapq.heappush(stacks,[challenge[next_i_left],next_i_left])
            return True
        def check(ret):
            for i in range(len(challenge)):
                if challenge[i]<=ret and util(ret,i):
                    return True
            return False
        while ret<max_score:
            mid=(ret+max_score)//2
            self.log(ret,mid,max_score)
            if check(mid):
                max_score=mid
            else:
                ret=mid+1
        return max_score
    

    def run(self, *args):
        return self.ringGame(*args)


class SolutionDebug(Solution):
    logs = ""

    def log(self, *s):
        self.logs += " ".join([str(v) for v in s])+"
"

    def run(self, *args, **kw) -> int:
        self.logs = ""
        return super().run(*args, **kw)


if __name__ == '__main__':
    s = SolutionDebug()
    null=None
    true=True
    false=False
    for case in [
        [[1,180,447],266],
        [[5,4,6,2,7],4],
        [[12,7,11,3,9],8],
        [[7,7],7],
        [[1,17,1,19,1],18],
        [[1,1,1],1]
    ]:
        s.logs = ""
        e = s.run(*case[:-1])
        if e != case[-1]:
            print(e, case)
            print(s.logs)
            break


    

Explain

此题解的核心思想是利用二分搜索和双指针技术来寻找最小的初始积分,以挑战所有关卡。首先,找到挑战值最大的关卡,并以此关卡为基点重构环形数组为线性数组,以简化问题。然后,使用二分搜索从可能的最低积分开始,使用双指针检测该积分是否足以开始并完成游戏。在二分搜索的每一步,会尝试将挑战分和数组两侧的积分合并,检查能否覆盖整个数组。这种方法有效地将环形结构转化为线性结构,从而简化了逻辑判断。

时间复杂度: O(n log(max_value))

空间复杂度: O(n)

from sortedcontainers import SortedList
from typing import List
from collections import defaultdict, deque
from itertools import accumulate
import heapq
import sys

class Solution:
    def log(self, *s):
        pass # 日志函数,用于调试
    
    def ringGame(self, challenge: List[int]) -> int:
        n = len(challenge)
        max_value = max(challenge)
        max_index = challenge.index(max_value)
        # 重构数组使其线性化,简化问题
        challenge = challenge[max_index:] + challenge[:max_index]
        n3 = len(challenge)
        
        def check(m):
            # 检查起始积分m是否足够挑战所有关卡
            for i in range(1, n3):
                if challenge[i] > m:
                    continue
                s, l, r = m | challenge[i], i - 1, i + 1
                while True:
                    if r - l > n:
                        return True
                    if l >= 0 and challenge[l] <= s:
                        s |= challenge[l]
                        l -= 1
                    if r < n3 and challenge[r] <= s:
                        s |= challenge[r]
                        r += 1
                    else:
                        break
                return False
        # 二进制搜索最低有效积分
        res = 1 << max_value.bit_length() - 1
        while res:
            if not check(res):
                res <<= 1
            else:
                res >>= 1
        return res
    
    def run(self, *args):
        return self.ringGame(*args)

if __name__ == '__main__':
    s = Solution()
    result = s.run([5,4,6,2,7])
    print(result) # 应该输出4

Explore

将环形数组转换为线性数组是为了简化问题的处理。在环形结构中,数组的末尾与开头相连,这使得处理边界条件和遍历时的逻辑变得复杂。通过将环形数组以最大值所在的位置为断点重构为线性数组,可以将问题从一个循环处理问题转变为一个更容易管理的线性处理问题。这种转换使得数组的遍历和检查变得直接明了,减少了复杂的边界判断,从而提高了代码的可读性和易于管理。

在题解中,二分搜索的初始下界设为0,上界设为最大关卡挑战值的二进制长度左移1位的结果。具体来说,上界的计算方法是将最大挑战值的二进制表示长度减一后,结果为1的位数左移一位。这样做是基于对问题的理解:一个合理的起始上界应该足够大,能够覆盖最大的单一挑战,并且还应有余地以确保可以通过其他较小的挑战。这种设置上界的方法是为了确保在二分搜索开始时,有一个足够宽的搜索范围来逐步缩小至最优解。

双指针策略在本题中是通过设置一个左指针和一个右指针来实现的,这两个指针分别从当前考察的关卡的左右两侧开始移动。在每次二分搜索尝试的过程中,会从挑战数组中的某一点开始,用当前的积分尝试向左右两边扩展。如果当前积分足够覆盖左侧或右侧的下一个关卡,相应的指针就会移动一位,并将该关卡的挑战值合并入当前积分。这个过程会一直持续,直到指针超出数组边界或无法继续向某方向扩展(因为挑战值大于当前积分)。如果在某次尝试中,左右指针能够覆盖整个数组,那么说明当前积分足够通过所有关卡。