方向一:题目解析-令牌最大化分数问题 | 豆包MarsCode AI刷题

76 阅读12分钟

问题描述

小F手上有一些令牌,每个令牌的值由数组 tokens 表示,其中 tokens[i] 是第 i 个令牌的值。他的初始能量为 power,初始分数为 0。小F的目标是通过有策略地使用这些令牌来最大化他的总分数。

小F可以对每个未使用的令牌采取以下两种操作之一:

  1. 朝上使用令牌:如果当前的能量至少为 tokens[i],他可以使用令牌 i,消耗 tokens[i] 点能量并获得 1 分。
  2. 朝下使用令牌:如果当前的分数至少为 1,他可以使用令牌 i,获得 tokens[i] 点能量并失去 1 分。

你的任务是帮助小F找到最大可能的分数,并返回该分数。

例如:当 tokens = [10] 且 power = 50 时,小F可以使用令牌来获得 1 分。

测试样例

样例1:

输入:tokens = [10], power = 50
输出:1

样例2:

输入:tokens = [100,200], power = 150
输出:1

样例3:

输入:tokens = [100,200,300,400], power = 200
输出:2

样例4:

输入:tokens = [20,30,40], power = 50
输出:2

问题理解

1.问题分析

在一个特定的情境中,小 F 手中持有一定数量的令牌,而这些令牌各自所具有的值是通过一个数组来表示的,我们将这个数组命名为 tokens。具体而言,数组中的每一个元素 tokens[i] 就对应着第 i 个令牌所具备的具体数值。与此同时,小 F 一开始还拥有一个初始的能量值,我们用 power 来表示它,并且他的初始分数设定为 0 分。

小 F 怀揣着一个明确的目标,那就是要通过运用巧妙且富有策略性的方式去使用这些手中的令牌,从而让自己所能获得的总分数达到最大化的程度。

对于每一个还未曾被使用过的令牌,小 F 可以从以下两种操作方式里选择其一去执行:

第一种操作方式是 “朝上使用令牌”。在进行这种操作时,需要满足一个必要的前提条件,那就是当前小 F 所拥有的能量值至少要达到该令牌所对应的数值,也就是要不少于 tokens[i]。只有在满足这个条件的情况下,小 F 才能够使用这个第 i 个令牌。而一旦使用了这个令牌,相应地就会消耗掉 tokens[i] 这么多的能量,不过与此同时,小 F 也能够因此而获得 1 分,使得自己的总分数得以增加。

第二种操作方式则是 “朝下使用令牌”。要执行这种操作,同样也存在一个必须要满足的条件,即小 F 当前所拥有的分数至少得是 1 分才行。当满足这个分数条件后,小 F 便可以使用这个特定的令牌 i,使用之后会带来这样的结果:小 F 能够获得 tokens[i] 点的能量,从而使得自己的能量储备有所增加,但是相应地,他也需要为此付出失去 1 分的代价,自己的总分数会随之减少 1 分。

而现在摆在我们面前的任务就是,要帮助小 F 去找出在所有可能的操作策略之下,他所能获取到的最大分数是多少,并且最终将这个最大的分数返回出来。

为了让大家更清晰地理解这个规则,我们来看几个具体的例子。比如说,当 tokens 这个令牌数组里只有一个元素,也就是 tokens = [10],并且小 F 初始的能量值 power = 50 时,由于小 F 此时所拥有的能量 50 是大于这个唯一令牌的值 10 的,所以小 F 完全可以按照 “朝上使用令牌” 的规则来使用这个令牌,在消耗掉 10 点能量之后,他就能够顺利地获得 1 分。

2.任务要求

深入剖析小 F 所面临的这个任务,我们可以清晰地看到,他的核心目标就是要凭借着对那些手中令牌的合理运用,精心规划使用的策略,来让自己最终所获得的总分数达到尽可能高的水平,也就是实现分数的最大化。

而他所拥有的两种操作方式,各自有着明确的特点和作用:

第一种 “朝上使用令牌” 的操作,本质上就是一种用能量去换取分数的方式。小 F 需要以消耗一定数量的能量为代价,来使得自己的总分数能够增加,这个增加的幅度是固定的,每次使用一个符合条件的令牌就能增加 1 分,不过这一切的前提是他得有足够的能量来支撑这次操作才行。

与之相对应的第二种 “朝下使用令牌” 的操作,则是一种反向的资源转换方式,是通过消耗自己已经获得的分数,以此为条件来换取相应数量的能量。在分数足够的情况下,小 F 可以选择执行这样的操作,让自己的能量储备得以补充,当然,这么做的同时分数也会相应地减少 1 分。

数据结构选择

在思考如何去解决这个问题的时候,我们需要选择一个合适的数据结构来存储和操作这些令牌相关的数据。经过权衡与分析,我们发现使用一个排序的数组来存放这些令牌是一个较为理想的选择。

之所以做出这样的选择,是因为通过将令牌按照其数值大小进行排序后,我们在后续的操作过程中,就能够非常便捷地从这个有序的数组里挑选出那些数值最小的令牌,然后依据规则去消耗能量来获取分数;或者也能够轻松地定位到数值最大的令牌,利用 “朝下使用令牌” 的规则,通过消耗分数来获取能量。这样有序的排列方式极大地便利了我们去制定合理的策略,对令牌进行有针对性的操作,从而朝着最大化总分数的目标迈进。

算法步骤

接下来详细阐述一下解决这个问题所涉及到的具体算法步骤:

  1. 排序
    首先要做的第一步就是对代表令牌的数组 tokens 进行排序操作。这一步至关重要,它能够为后续的操作奠定良好的基础,使得我们可以依据令牌数值的大小顺序来有条不紊地进行处理。通过排序,我们能够清晰地知晓哪些令牌的数值较小,适合在能量充足时优先用来消耗能量获取分数;哪些令牌的数值较大,适合在分数足够的情况下用来获取能量,补充能量储备。

  2. 双指针
    在完成了排序的基础之上,我们引入双指针的方法来协助我们操作这个有序的令牌数组。我们会设定两个指针,其中一个指针专门指向数组中数值最小的那个令牌,这个指针主要是在我们考虑消耗能量来获取分数的时候发挥作用,因为我们通常会优先选择数值较小的令牌去消耗能量,只要当前能量允许的话。而另一个指针则是指向数组中数值最大的那个令牌,它在我们想要通过消耗分数来获取能量的时候就派上用场了,因为选择数值较大的令牌来获取能量往往更有利于后续的操作,能让我们获取到更多的能量补充。

  3. 贪心策略

    • 在整个操作过程中,我们运用贪心策略来决定每一步的具体操作。具体来说,如果当前小 F 所拥有的能量足够去使用那个由左指针(指向最小令牌的指针)所指向的最小的令牌,那么我们就果断地选择使用它。使用这个最小的令牌意味着要消耗掉与之对应的能量值,不过与此同时,小 F 的分数也会按照规则增加 1 分,这是朝着总分数最大化目标迈进的积极一步。
    • 另外一方面,如果当前小 F 的分数已经足够使用那个由右指针(指向最大令牌的指针)所指向的最大的令牌了,那么我们也会选择使用这个最大的令牌来获取能量。通过这样的操作,小 F 能够增加自己的能量储备,虽然这需要付出失去 1 分的代价,但在合适的情况下,这样做有助于后续进一步地使用其他令牌来获取更多的分数,同样也是为了实现总分数最大化这个最终目标。
  4. 终止条件
    整个操作流程不会无休无止地进行下去,它是存在一个明确的终止条件的。当我们所设定的这两个指针,也就是那个指向最小令牌的左指针和指向最大令牌的右指针相遇的时候,这就意味着所有可以被使用的令牌都已经经过了我们的考虑和操作,已经没有更多的令牌可供我们去按照规则进行使用了,此时整个操作流程就自然而然地结束了。

代码实现

下面我们来看一下具体的代码实现示例,以下是一个用 Python 语言编写的函数,用于解决这个问题:

    # 对令牌进行排序
    tokens.sort()
    
    # 初始化指针和分数
    left, right = 0, len(tokens) - 1
    score = 0
    
    while left <= right:
        # 如果能量足够使用最小的令牌
        if power >= tokens[left]:
            # 消耗能量,增加分数
            power -= tokens[left]
            score += 1
            left += 1
        # 如果分数足够使用最大的令牌
        elif score > 0 and left < right:
            # 消耗分数,增加能量
            power += tokens[right]
            score -= 1
            right -= 1
        else:
            # 无法继续操作,退出循环
            break
    
    return score

关键步骤注释

下面对代码中的几个关键步骤及其对应的注释进行详细说明,以便更好地理解代码的逻辑和功能:

  • tokens.sort()
    这行代码的作用是对 tokens 这个列表(也就是代表令牌的数组)进行排序操作。通过调用 Python 内置的 sort 方法,能够让 tokens 列表中的元素按照从小到大的顺序进行排列,这样后续在运用双指针操作以及贪心策略的时候,我们就可以方便地依据令牌数值的大小来做出合理的决策了。
  • left, right = 0, len(tokens) - 1
    这里是在初始化两个指针,分别命名为 left 和 right。其中 left 指针初始时指向 tokens 列表的第一个元素,也就是数值最小的那个令牌所在的位置(因为前面已经对列表进行了排序);而 right 指针则初始指向 tokens 列表的最后一个元素,即数值最大的那个令牌所在的位置。同时,还初始化了 score 变量为 0,它用于记录小 F 当前所获得的总分数情况。
  • while left <= right
    这是一个循环的条件判断语句,它规定了只要 left 指针所指向的位置小于等于 right 指针所指向的位置,那么这个循环就会持续进行下去。也就是说,只要两个指针还没有相遇,意味着还有令牌可供我们按照规则去操作使用,循环就不会停止,会不断地去判断并执行相应的操作步骤。
  • if power >= tokens[left]
    这是一个条件判断语句,用于判断当前小 F 所拥有的能量值是否足够去使用 left 指针所指向的那个最小的令牌。如果这个条件成立,那就意味着按照规则,小 F 可以执行 “朝上使用令牌” 的操作,通过消耗对应的能量来获取 1 分,这是朝着最大化总分数目标前进的一种有效操作方式。
  • elif score > 0 and left < right
    这个条件判断语句是在前面判断能量不够使用最小令牌的基础上进行的进一步判断。它主要是看当前小 F 的分数是否大于 0,并且 left 指针是否小于 right 指针(也就是确保还有不同的令牌可供操作,避免出现重复操作等不合理情况)。如果这个条件满足,那就说明当前小 F 可以执行 “朝下使用令牌” 的操作,通过消耗 1 分来获取 right 指针所指向的那个最大令牌对应的能量值,为后续继续操作获取更多分数创造条件。
  • else
    当上述两个条件都不满足的时候,就意味着在当前的状态下,小 F 已经没办法再按照规则去进行任何有效的操作了,无论是消耗能量获取分数,还是消耗分数获取能量都无法实现了。所以这个时候就会执行 else 分支里的语句,也就是通过 break 语句来直接退出整个循环,结束操作流程,最后返回已经获得的总分数 score