日渐头秃的代码日记 -- 第299场周赛第3题心路历程

129 阅读2分钟

拼接数组的最大分数

原题目如下:

给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,长度都是 n 。

你可以选择两个整数 left 和 right ,其中 0 <= left <= right < n ,接着 交换 两个子数组 nums1[left...right] 和 nums2[left...right] 。

  • 例如,设 nums1 = [1,2,3,4,5] 和 nums2 = [11,12,13,14,15] ,整数选择 left = 1 和 right = 2,那么 nums1 会变为 [1,12,13,4,5] 而 nums2 会变为 [11,2,3,14,15] 。

你可以选择执行上述操作 一次 或不执行任何操作。

数组的 分数 取 sum(nums1) 和 sum(nums2) 中的最大值,其中 sum(arr) 是数组 arr 中所有元素之和。

返回 可能的最大分数 。

子数组 是数组中连续的一个元素序列。arr[left...right] 表示子数组包含 nums 中下标 left 和 right 之间的元素 (含 下标 left 和 right 对应元素

 

示例 1:

输入: nums1 = [60,60,60], nums2 = [10,90,10]
输出: 210
解释: 选择 left = 1right = 1 ,得到 nums1 = [60,90,60] 和 nums2 = [10,60,10] 。
分数为 max(sum(nums1), sum(nums2)) = max(210, 80) = 210

示例 2:

输入: nums1 = [20,40,20,70,30], nums2 = [50,20,50,40,20]
输出: 220
解释: 选择 left = 3right = 4 ,得到 nums1 = [20,40,20,40,20] 和 nums2 = [50,20,50,70,30] 。
分数为 max(sum(nums1), sum(nums2)) = max(140, 220) = 220

示例 3:

输入: nums1 = [7,11,13], nums2 = [1,1,1]
输出: 31
解释: 选择不交换任何子数组。
分数为 max(sum(nums1), sum(nums2)) = max(31, 3) = 31

 

提示:

  • n == nums1.length == nums2.length
  • 1 <= n <= 10^5
  • 1 <= nums1[i], nums2[i] <= 10^4

解析

从题目的描述中可以看出,我们需要遍历不同的起点和终点,并将两个数组对应位置的子数组进行交换,然后再对每一个数组求和,计算其结果,从而得到得分。

题目思路已经有了,主要是如何实现。

如果直接按照上述思路进行模拟,很自然的,时间复杂度是O(N^2),必然无法达到时间要求。

肯定要进行一些优化,考虑到两个数组交换,全都交换和全不交换是一样的等价的,交换前一半和交换后一半是一样的等价的,所以在交换上,是可以进行优化裁剪的。

但是如何从左边到右边逐个遍历起点和终点,依然是耗时最久的环节。

换一个思路,两个数组都是在求和,而他们的总和是不变的,因为只是互换了数字,两个数组的总和是固定的,所以交换来交换去,无非是一些增增减减。可以先算算看两个数组的对位差是多少,来回交换就是在起始的sum和的基础上,用对位差进行微调。

比如以题目的例子为例nums1 = [60,60,60], nums2 = [10,90,10],nums1的起始和是180,nums2的起始和是110.计算一下对位的差是:[50, -30, 50],要得到结果最大,就是看对位差连续n个数字之和最大或者最小。在竞赛的时候没有仔细想到底是最大还是最小,不过不重要,都给算了,取一个最大即可。

这个例子不够明显,我们看第二个例子nums1 = [20,40,20,70,30], nums2 = [50,20,50,40,20],这两个数组的对位差是 [-30, 20, -30, 30, 10],而对于对位差,可以看出来,最后两个30+10才是和最大的,因此就互换这两个位置的原数组即可。

现在问题来到了,如果对一个对位差数组delta,找到连续n个数字之和最大。这个问题是一个典型的动态规划问题,而且是一维的,比较简单。可以较为容易的写出状态转移方程,如下:

dp[i+1] = max(dp[i]+delta[i+1], delta[i+1])

到第i+1个位置为止,sum求和最大的连续子序列之和是dp[i+1],它要么是delta[i+1]本身,要么是前序求和的结果加上delta[i+1],此时并不需要知道从哪一位开始,只需要知道到i+1位的和就行了。

得到dp数组之后,直接max一下就能知道最大和是多少了,直接加回到起始数组的和上。

同理,之前提到过,不知道应该是 nums1-nums2还是 nums2-nums1,因此做两遍,时间复杂度不增加,不会影响通过情况

这样就会在原有数组和的基础上在增加两个和,一个四个数字,选一个最大的即可。如果选出来的最大数组是原有数组和,说明这时候的交换方法是一个都不换,或者全部都换,他俩是等价的。

class Solution:
    def maximumsSplicedArray(self, nums1: List[int], nums2: List[int]) -> int:
        sum1 = sum(nums1)
        sum2 = sum(nums2)
        sum3 = sum1
        sum4 = sum2
        delta = [nums1[i] - nums2[i] for i in range(len(nums1))]
        print(delta)
        length = len(delta)
        dp = [0] * length
        dp[0] = delta[0]
        for i in range(1, length):
            dp[i] = max(delta[i], dp[i-1] + delta[i])
        max_dp = max(dp)
        if max_dp > 0:
            sum4 = sum2 + max_dp
        
        delta2 = [nums2[i] - nums1[i] for i in range(len(nums1))]
        print(delta2)
        length = len(delta2)
        dp2 = [0] * length
        dp2[0] = delta2[0]
        for i in range(1, length):
            dp2[i] = max(delta2[i], dp2[i-1] + delta2[i])
        max_dp2 = max(dp2)
        if max_dp2 > 0:
            sum3 = sum1 + max_dp2
        print(sum1, sum2, sum3, sum4)
        return max(sum1, sum2, sum3, sum4)

后记

比较开心,在周赛的时候能够顺利地一次性通过第三题,以往都是两道题就结束了比赛。想比于过去的周赛,第298场和299场的题目质量较高,难度适中,不是过分拗难度的那种,但是又不放水,适合绝大多数朋友参与。不像有的场次,一个DFS走到死,三道题都是DFS。也有的时候,三道DP动态规划,二维三维,故意增加难度。不太贴近于实际工作场景。像本题,很有可能是从实际的工作场景中抽象出来的,也可以将这种解题思路在日常工作中得到使用,是个很好的赛题。

同时也让自己反思一下,在日常的工作中,是否是for循环套着for循环,一套就是三四层,QPS扛不住了就申请加机器,而不是从代码层面入手,考虑一下算法是否可以优化。

参加周赛不只是为了做题,而是为了能够举一反三,真正做到对自己的能力有所提升。