在传球游戏中最大化函数值

标签: 位运算 数组 动态规划

难度: Hard

给你一个长度为 n 下标从 0 开始的整数数组 receiver 和一个整数 k 。

总共有 n 名玩家,玩家 编号 互不相同,且为 [0, n - 1] 中的整数。这些玩家玩一个传球游戏,receiver[i] 表示编号为 i 的玩家会传球给编号为 receiver[i] 的玩家。玩家可以传球给自己,也就是说 receiver[i] 可能等于 i 。

你需要从 n 名玩家中选择一名玩家作为游戏开始时唯一手中有球的玩家,球会被传 恰好 k 次。

如果选择编号为 x 的玩家作为开始玩家,定义函数 f(x) 表示从编号为 x 的玩家开始,k 次传球内所有接触过球玩家的编号之  ,如果有玩家多次触球,则 累加多次 。换句话说, f(x) = x + receiver[x] + receiver[receiver[x]] + ... + receiver(k)[x] 。

你的任务时选择开始玩家 x ,目的是 最大化 f(x) 。

请你返回函数的 最大值 。

注意:receiver 可能含有重复元素。

示例 1:

传递次数 传球者编号 接球者编号 x + 所有接球者编号
      2
1 2 1 3
2 1 0 3
3 0 2 5
4 2 1 6
输入:receiver = [2,0,1], k = 4
输出:6
解释:上表展示了从编号为 x = 2 开始的游戏过程。
从表中可知,f(2) 等于 6 。
6 是能得到最大的函数值。
所以输出为 6 。

示例 2:

传递次数 传球者编号 接球者编号 x + 所有接球者编号
      4
1 4 3 7
2 3 2 9
3 2 1 10
输入:receiver = [1,1,1,2,3], k = 3
输出:10
解释:上表展示了从编号为 x = 4 开始的游戏过程。
从表中可知,f(4) 等于 10 。
10 是能得到最大的函数值。
所以输出为 10 。

提示:

  • 1 <= receiver.length == n <= 105
  • 0 <= receiver[i] <= n - 1
  • 1 <= k <= 1010

Submission

运行时间: 333 ms

内存: 58.4 MB

class Solution:
    def getMaxFunctionValue(self, receiver: List[int], k: int) -> int:
        n = len(receiver)
        prevs = [[] for _ in range(n)]
        res = [-1]*n
        offMap = {}
        for i, ri in enumerate(receiver):
            prevs[ri].append(i)
        it1 = (i for i, (lst, v) in enumerate(zip(prevs, res)) if not lst and v < 0)
        it2 = (i for i, v in enumerate(res) if v < 0)
        class FillNode:
            def __init__(self, i, accu=0, _next=None):
                self.i = i
                self.accu = accu
                self.next = _next
                self.pursue, self.pursueAccu = -1, 0
                self.q = deque()
        def iterNode(head):
            node = head.next
            while node:
                yield node
                node = node.next
        def fill(prev, circleLen, circleSum, off, accus):
            # print('fill', prev, circleLen, off, accus)
            head = FillNode(-1)
            head.next = FillNode(prev, prev)
            head.next.prev = head
            circles, r = divmod(k+1, circleLen)
            circles *= circleSum
            for rnd in count(1):
                if not head.next: break
                if rnd < k+1:
                    r -= 1
                    if r < 0:
                        circles -= circleSum
                        r += circleLen
                    delta = circles + accus[off+r] - accus[off]
                    for node in iterNode(head):
                        res[node.i] = node.accu + delta
                        #print('delta', circles, r, off, delta, node.i, node.accu)
                elif rnd == k + 1:
                    for node in iterNode(head):
                        node.pursue = node.pursueAccu = prev
                        res[node.i] = node.accu
                        #print('purs', rnd, node.i, node.pursue)
                else:
                    for node in iterNode(head):
                        res[node.i] = node.accu - node.pursueAccu
                        #print(node.q, prevs[node.pursue], node.i)
                        node.pursue = node.q.popleft() if len(prevs[node.pursue]) > 1 else prevs[node.pursue][0]
                        node.pursueAccu += node.pursue
                for node in iterNode(head):
                    ps = prevs[node.i]
                    if len(ps) == 1:
                        node.i = ps[0]
                        node.accu += node.i
                    else:
                        node.prev.next = node.next
                        if node.next:
                            node.next.prev = node.prev
                        for p in ps:
                            # print('spilt', rnd, node.i, p)
                            new = FillNode(p, node.accu + p, head.next)
                            if head.next:
                                head.next.prev = new
                            head.next = new
                            new.prev = head
                            new.q = deque(node.q)
                            new.q.append(p)
                            new.pursue = node.pursue
                            new.pursueAccu = node.pursueAccu
                
        for i in chain(it1, it2):
            # print('i', i)
            offMap.clear()
            accus = [0]
            while offMap.get(i) is None:
                offMap[i] = len(offMap)
                accus.append(accus[-1]+i)
                last, i = i, receiver[i]
            circleLen = len(offMap) - offMap[i]
            circles, r = divmod(k + 1, circleLen)
            startOff = offMap[i]
            circleSum = accus[-1] - accus[startOff]
            circleTotal = circleSum * circles
            #print('circle', circleSum, circles, circleTotal)
            for off in range(startOff, len(offMap)):
                accus.append(accus[-1] + i)
                res[i] = circleTotal + accus[off+r] - accus[off]
                # print('resi', i, res[i])
                for prev in prevs[i]:
                    if prev != last:
                        fill(prev, circleLen, circleSum, off, accus)
                last, i = i, receiver[i]
        #print(res) 
        return max(res)
        # x y 
        # off = 0... (x+y-1)
        # off + k-1

Explain

该题解采用复杂的方法处理球传递游戏问题。首先,创建一个数据结构记录每个玩家作为接球者的所有前驱,用于处理球的传递链。通过定义 FillNode 类来追踪传球过程中的累积分数以及其他元数据,然后实现一个 iterNode 函数来遍历传球链。主要的处理逻辑在 fill 函数中,该函数利用循环找到传球序列的开始和结束,并计算序列长度以及累积和,从而能够模拟整个传球过程并更新每个玩家的最终得分。最后,通过遍历所有可能的传球起始玩家来找到可以获得最大分数的起始玩家。

时间复杂度: O(n * k)

空间复杂度: O(n)

class Solution:
    def getMaxFunctionValue(self, receiver: List[int], k: int) -> int:
        n = len(receiver)
        # 初始化每个玩家的前驱列表和初始分数数组
        prevs = [[] for _ in range(n)]
        res = [-1]*n
        # 字典用于存储传球链的偏移量映射
        offMap = {}
        for i, ri in enumerate(receiver):
            prevs[ri].append(i)
        it1 = (i for i, (lst, v) in enumerate(zip(prevs, res)) if not lst and v < 0)
        it2 = (i for i, v in enumerate(res) if v < 0)
        class FillNode:
            def __init__(self, i, accu=0, _next=None):
                self.i = i
                self.accu = accu
                self.next = _next
                self.pursue, self.pursueAccu = -1, 0
                self.q = deque()
        def iterNode(head):
            node = head.next
            while node:
                yield node
                node = node.next
        def fill(prev, circleLen, circleSum, off, accus):
            head = FillNode(-1)
            head.next = FillNode(prev, prev)
            head.next.prev = head
            circles, r = divmod(k+1, circleLen)
            circles *= circleSum
            for rnd in count(1):
                if not head.next: break
                if rnd < k+1:
                    r -= 1
                    if r < 0:
                        circles -= circleSum
                        r += circleLen
                    delta = circles + accus[off+r] - accus[off]
                    for node in iterNode(head):
                        res[node.i] = node.accu + delta
                elif rnd == k + 1:
                    for node in iterNode(head):
                        node.pursue = node.pursueAccu = prev
                        res[node.i] = node.accu
                else:
                    for node in iterNode(head):
                        res[node.i] = node.accu - node.pursueAccu
                        node.pursue = node.q.popleft() if len(prevs[node.pursue]) > 1 else prevs[node.pursue][0]
                        node.pursueAccu += node.pursue
                for node in iterNode(head):
                    ps = prevs[node.i]
                    if len(ps) == 1:
                        node.i = ps[0]
                        node.accu += node.i
                    else:
                        node.prev.next = node.next
                        if node.next:
                            node.next.prev = node.prev
                        for p in ps:
                            new = FillNode(p, node.accu + p, head.next)
                            if head.next:
                                head.next.prev = new
                            head.next = new
                            new.prev = head
                            new.q = deque(node.q)
                            new.q.append(p)
                            new.pursue = node.pursue
                            new.pursueAccu = node.pursueAccu
        
        for i in chain(it1, it2):
            offMap.clear()
            accus = [0]
            while offMap.get(i) is None:
                offMap[i] = len(offMap)
                accus.append(accus[-1]+i)
                last, i = i, receiver[i]
            circleLen = len(offMap) - offMap[i]
            circles, r = divmod(k + 1, circleLen)
            startOff = offMap[i]
            circleSum = accus[-1] - accus[startOff]
            circleTotal = circleSum * circles
            for off in range(startOff, len(offMap)):
                accus.append(accus[-1] + i)
                res[i] = circleTotal + accus[off+r] - accus[off]
                for prev in prevs[i]:
                    if prev != last:
                        fill(prev, circleLen, circleSum, off, accus)
                last, i = i, receiver[i]
        return max(res)

Explore

使用复杂的数据结构记录每个玩家作为接球者的所有前驱主要是为了高效地处理问题中的循环依赖和重复计算。这种方法可以快速确定传球链中的环形结构和环外部分,从而避免在模拟传球过程中重复遍历同一路径或节点,尤其是在长传球链和环形链中更为有效。此外,这种数据结构还有助于快速更新和查询每个玩家的累积得分,尤其是在有多个前驱的情况下,可以更灵活地处理分支和合并的情况。

在 iterNode 函数中,`yield node` 的作用是创建一个生成器,它逐一返回传球链中的节点。这种方式允许函数的调用者在每次迭代时获得当前节点并对其执行操作,而不需要一次性获取全部节点。这样的延迟计算(按需计算)可以提高内存利用率,同时使得处理每个节点的逻辑更加灵活和可控。通过生成器,可以在遍历过程中更容易地插入或删除节点,或在遍历时动态调整遍历策略,这对于处理复杂的传球链尤为重要。

在 fill 函数中,`circleLen` 代表在传球链中检测到的环的长度,即环中包含的节点数。`circleSum` 则代表这个环中所有节点的索引值的累积和。环的检测是通过记录节点在遍历过程中的出现位置来完成的,一旦发现某个节点重新被访问,即确认存在环。此时,`circleLen` 计算为当前节点位置与该节点首次出现位置之间的差值。`circleSum` 是从环开始的位置到环结束的位置节点值的累积和。这两个参数对于计算在环中进行多圈后每个节点的累积得分非常关键,它们使得可以快速计算多次遍历环时的得分增加量。

算法通过映射每个节点第一次被访问的位置来识别环,并使用环的长度和累积和来计算环上的节点得分。一旦检测到环,算法计算传球次数与环长度的商和余数,利用这些信息来确定遍历环的完整圈数和额外部分。通过这种方式,算法可以精确地计算在规定的传球次数内每个节点的得分,而无需实际模拟每一次传球,从而避免了无限循环的问题。对于环上的节点,算法计算多圈后的累积得分加上在最后一部分环内的得分,以此确保每个节点得分的正确性。