秋招算法备战第34天 | 1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

1005. K 次取反后最大化的数组和 - 力扣(LeetCode)

这部分的代码对于各种边界条件调试了挺久才搞定,代码如下

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        if len(nums) == 0:
            return nums

        nums = sorted(nums)

        for i in range(k):
            if i < len(nums) and nums[i] < 0:
                nums[i] = -nums[i]
            else:
                if i >= len(nums):
                    if (k-i) % 2 == 1:
                        nums[-1] = -nums[-1]
                        break
                elif (k-i) % 2 == 1:
                    if i > 0 and nums[i-1] < nums[i]:
                        nums[i-1] = -nums[i-1]
                    else:
                        nums[i] = -nums[i]
                break
        print(nums)
        return sum(nums)

下面是GPT4给出的题解,比我的代码要简单很多,通过对K减一来判断不同状态

你需要找到一个策略来最大化一个数组的和,通过反转数组中的元素,最多进行 k 次反转。

这是一个贪心策略问题,我们可以采取以下策略来解决这个问题:

  1. 我们首先对数组进行排序。然后从左到右遍历数组,对于每个负数,如果 k>0,我们就把它变为正数,并且把 k 减 1。

  2. 如果我们遍历完所有的负数或者 k 已经变为 0,我们就停止遍历。此时有两种情况:

    a. 如果 k 仍然大于 0,并且 k 是偶数,我们就不需要做任何改动,因为反转一个数两次等于没有反转。

    b. 如果 k 是奇数,我们就需要将数组中最小的正数反转为负数,因为这样可以得到最大的和。

以下是这个问题的 Python 解决方案:

    nums.sort()
    i = 0
    while i < len(nums) and nums[i] < 0 and k > 0:
        nums[i] = -nums[i]
        i += 1
        k -= 1
    return sum(nums) - (2 * min(nums) if k % 2 == 1 else 0) 

这个算法的时间复杂度是 O(n log n),因为我们需要对数组进行排序。空间复杂度是 O(1),因为我们只需要常量的额外空间。

而参考代码按照绝对值大小降序排列,省去了很多条件判断

class Solution:
    def largestSumAfterKNegations(self, A: List[int], K: int) -> int:
        A.sort(key=lambda x: abs(x), reverse=True)  # 第一步:按照绝对值降序排序数组A

        for i in range(len(A)):  # 第二步:执行K次取反操作
            if A[i] < 0 and K > 0:
                A[i] *= -1
                K -= 1

        if K % 2 == 1:  # 第三步:如果K还有剩余次数,将绝对值最小的元素取反
            A[-1] *= -1

        result = sum(A)  # 第四步:计算数组A的元素和
        return result

134. 加油站 - 力扣(LeetCode)

这个问题是一个经典的算法问题,称为加油站问题。在这个问题中,我们需要找到一个加油站,从这个加油站出发,我们可以绕环路行驶一周。具体的算法思路如下:

  1. 创建两个变量,total_tank 和 curr_tank,用于跟踪我们的汽油总量和当前的汽油量。
  2. 创建一个变量,starting_station,用于记录我们开始的加油站。
  3. 对于每一个加油站 i,我们先计算出汽油的剩余量,也就是 gas[i] - cost[i]。然后更新 total_tank 和 curr_tank。如果 curr_tank 变为负数,说明我们不能从当前的出发点开始,因此我们需要更新 starting_station 为 i+1,并将 curr_tank 重置为 0。
  4. 在遍历完所有的加油站之后,如果 total_tank 仍然大于等于0,那么说明存在一个加油站可以满足条件,返回 starting_station;否则,返回 -1。

这个算法的时间复杂度是 O(n),因为我们只遍历了一次所有的加油站。

下面是使用 Python 实现的代码:

def canCompleteCircuit(gas, cost):
    # 初始化总汽油量和当前汽油量为0
    total_tank, curr_tank = 0, 0
    # 初始化起始加油站为0
    starting_station = 0
    # 遍历每一个加油站
    for i in range(len(gas)):
        # 更新总汽油量和当前汽油量
        total_tank += gas[i] - cost[i]
        curr_tank += gas[i] - cost[i]
        # 如果当前汽油量小于0,意味着从当前起始加油站出发无法到达第i个加油站
        if curr_tank < 0:
            # 更新起始加油站为下一个加油站,重置当前汽油量为0
            starting_station = i + 1
            curr_tank = 0
    # 如果总汽油量大于等于0,返回起始加油站,否则返回-1,表示无法从任何加油站出发绕环路行驶一周
    return starting_station if total_tank >= 0 else -1

在这段代码中,我们首先初始化 total_tank、curr_tank 和 starting_station。然后,我们遍历每一个加油站,并计算 total_tank 和 curr_tank。如果 curr_tank 小于 0,我们就更新 starting_station 并重置 curr_tank。最后,如果 total_tank 大于等于 0,我们就返回 starting_station,否则返回 -1。

下面是对“更新起始加油站为下一个加油站”的说明

这个算法中的这一步其实利用了一种“贪心”的策略。

假设从加油站 A 出发,你在加油站 B 首次发现汽油不够用了(即 A 到 B 的汽油不够了)。根据算法的策略,你将会考虑从加油站 B+1开始作为新的起点,并检查这一新的起点能否让你完成一整圈。

你可能会有疑问:既然 A 到 B 的汽油不够,为什么不考虑 A+1或者其他更早的加油站作为新的起点呢?
这是因为根据问题设定,所有的加油站总能提供的汽油量是大于或等于总的消耗量的,也就是说肯定存在一种可能的路径。现在,你从A 出发到 B 已经失败了,这表示从 A 到 B 这段路上,你收到的汽油总量是小于消耗量的。因此,如果你选择 A 和 B之间的任何一个加油站作为新的起点,从新的起点出发到达 B 时,你手上的汽油量更少,还是不够的。

所以,这个算法选择从 B+1 作为新的起点并不会漏掉任何可能的解。

135. 分发糖果 - 力扣(LeetCode)

这是一个典型的贪心算法问题。我们首先给每个孩子分发一颗糖果,然后我们按照以下两个规则来分发更多的糖果:

  1. 从左到右遍历孩子,如果一个孩子的评分比他左边的孩子高,那么这个孩子应该得到比他左边的孩子更多的糖果。

  2. 从右到左遍历孩子,如果一个孩子的评分比他右边的孩子高,且他目前的糖果数少于他右边的孩子,那么他应该得到和他右边孩子相同数量的糖果。

我们需要注意的是,最后一个孩子不需要和右边的孩子比较,因为他没有右边的孩子。

以下是Python代码实现:

def candy(ratings):
    n = len(ratings)
    candies = [1] * n

    for i in range(1, n):
        if ratings[i] > ratings[i-1]:
            candies[i] = candies[i-1] + 1

    for i in range(n-2, -1, -1):
        if ratings[i] > ratings[i+1]:
            candies[i] = max(candies[i], candies[i+1] + 1)

    return sum(candies)

以上代码首先给每个孩子分发一颗糖果,然后从左到右和从右到左分别遍历孩子,根据上述两个规则给孩子分发更多的糖果,最后返回总的糖果数量。

这里很巧妙的一点是,通过两次遍历,先保证了每个孩子满足左边的情况,然后再解决右边的情况

总结

两次遍历有些意思,将复杂问题拆解为简单问题

你可能感兴趣的:(算法)