LeetCode周赛316,数学和思维场

大家好,我是梁唐。

还是照惯例我们来看看昨天的LeetCode周赛,这一场是第316场。由华人运通高合汽车赞助,夺得第一的小伙伴可以获得车模一个。

这一次的比赛题意中藏了不少信息,因此赛后有些小伙伴调侃为阅读理解专场。

总体来说这次比赛的题目质量还是不错的,不过相对来说数学和思维题较多,因此也有人吐槽,说自己不爱数学……

闲言少叙,下面我们来看题。

判断两个事件是否存在冲突

给你两个字符串数组 event1event2 ,表示发生在同一天的两个闭区间时间段事件,其中:

  • event1 = [startTime1, endTime1]
  • event2 = [startTime2, endTime2]

事件的时间为有效的 24 小时制且按 HH:MM 格式给出。

当两个事件存在某个非空的交集时(即,某些时刻是两个事件都包含的),则认为出现 冲突

如果两个事件之间存在冲突,返回 true ;否则,返回 false

题解

我们需要判断两个时间区间是否有重叠。如果我们把两个区间写成[s1, e1], [s2, e2]的话,如果两个区间有重叠意味着:s2e1e2s_2 \leq e_1 \leq e_2s1s2e1s_1 \leq s_2 \leq e_1

比较麻烦的是这题当中的时间都是以字符串给定的,需要我们手动分割一下。为了偷懒使用了Python,用C++也差不太多。

class Solution:
    def haveConflict(self, event1: List[str], event2: List[str]) -> bool:
        
        def greater(time1, time2):
            h1, m1 = list(map(int, time1.split(':')))
            h2, m2 = list(map(int, time2.split(':')))
            return h1 > h2 or (h1 == h2 and m1 >= m2)
        
        return greater(event1[1], event2[0]) and greater(event2[0], event1[0]) or greater(event1[0], event2[0]) and greater(event2[1], event1[0])
复制代码

最大公因数等于 K 的子数组数目

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 nums 的子数组中元素的最大公因数等于 k 的子数组数目。

子数组 是数组中一个连续的非空序列。

数组的最大公因数 是能整除数组中所有元素的最大整数。

题解

对于长度为n的区间包含的子数组的个数应该是O(n2)O(n^2),如果再要遍历每一个元素的话,时间复杂度会达到O(n3)O(n^3),对于当前题目的复杂度来说是无法接受的。

所以我们要进行优化,如果对最大公约数的定义熟悉的话,可以想到对于若干个数的最大公约数来说。随着元素的增加,最大公约数有变小的趋势。并且如果gcd(a0,a1,,an1)=kgcd(a_0, a_1, \cdots, a_{n-1})=k,那么gcd(a0,a1,,an)=gcd(k,an)gcd(a_0, a_1, \cdots, a_n)=gcd(k, a_n)

综合上面两点可以想到,我们可以想到,我们并不需要枚举所有的区间,再遍历求取每个区间的最大公约数。我们可以枚举区间的左侧端点,再从左往右依次添加元素,当最大公约数不再是kk的倍数时退出(最大公约数有变小的趋势),枚举下一个左侧端点。

class Solution {
public:
    int subarrayGCD(vector<int>& nums, int k) {
        int ret = 0;
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            // 当前最大公约数
            int cur = nums[i];
            for (int j = i; j < n; j++) {
                cur = gcd(cur, nums[j]);
                // 等于k时答案+1
                if (cur == k) ret++;
                // 不能整除k时退出
                if (cur % k) break;
            }
        }
        return ret;
    }
};
复制代码

使数组相等的最小开销

给你两个下标从 0 开始的数组 nums 和 cost ,分别包含 n 个 整数。

你可以执行下面操作 任意 次:

  • 将 nums 中 任意 元素增加或者减小 1 。

对第 i 个元素执行一次操作的开销是 cost[i] 。

请你返回使 nums 中所有元素 相等最少 总开销。

题解

我们假设最后所有数都是xx,我们把nums叫做aacost叫做bb。我们代入之后写出最后总开销的方程:

ans=i=1nxaibians = \sum_{i=1}^n | x - a_i| * b_i

这个式子当中存在绝对值看起来非常讨厌,但没有关系,我们可以分情况讨论把绝对值符号消除。假设xmin{a1,a2,,an}x\leq min\{a_1, a_2, \cdots, a_n \},此时对于所有的式子去掉绝对值后xx的系数都是1-1,所有式子合并之后xx的系数是n-n。当xmax{a1,a2,,an}x \geq max\{a_1, a_2, \cdots, a_n \}时,此时每一项xx的系数都是11,所有式子合并之后xx的系数为nn

xx在最大值和最小值中间时,一部分式子的系数为正,一部分为负。合并之后的系数可能是正也可能是负,很容易想到最后式子的系数和xx值的大小是正相关的。在某个临界点之前为负,到了某个临界点之后为正。既然如此,那么整个函数就是先减后增的。

对于先减后增的函数求最小值,我们可以使用三分法。如果对三分法不了解的同学可以点击下方链接跳转一下。没记错的话,这是我在公众号上更的第二篇算法文。

二分法的兄弟,三分法

class Solution {
public:
    long long minCost(vector<int>& nums, vector<int>& cost) {
        long long ret = 0;
        
        int n = nums.size();
        int maxi = 0, mini = 0x3f3f3f3f;
        
        for (int i = 0; i < n; i++) {
            maxi = max(maxi, nums[i]);
            mini = min(mini, nums[i]);
        }
        
        int l = mini-1, r = maxi+1;
        
        auto get_cost = [&](int m) -> long long {
            long long ret = 0;
            for (int i = 0; i < n; i++) {
                ret += cost[i] * (long long) abs(m - nums[i]);
            }
            return ret;
        };
        
        while (l+2 < r) {
            int len = (r - l) / 3;
            int m1 = l + len;
            int m2 = m1 + len;
            
            long long c1 = get_cost(m1), c2 = get_cost(m2);
            
            if (c1 <= c2) {
                r = m2;
                ret = c1;
            }else {
                l = m1;
                ret = c2;
            }
            
        }
        return ret;
    }
};
复制代码

所以本质上这题就是一个方程求最值的问题,希望大家不要被我的想法限制住,除了三分法之外还有很多可行的方法。

使数组相似的最少操作次数

给你两个正整数数组 nums 和 target ,两个数组长度相等。

在一次操作中,你可以选择两个 不同 的下标 i 和 j ,其中 0 <= i, j < nums.length ,并且:

  • 令 nums[i] = nums[i] + 2 且
  • 令 nums[j] = nums[j] - 2 。

如果两个数组中每个元素出现的频率相等,我们称两个数组是 相似 的。

请你返回将 nums 变得与 target 相似的最少操作次数。测试数据保证 nums 一定能变得与 target 相似。

题解

这题看起来很难,但实际上有一个trick:一个数每次操作它的奇偶性保持不变,即奇数仍然是奇数,不能变成偶数。进而可以想到我们可以把numstarget中的数字都按照奇偶性分成两组。

每一次操作会将一个数+2,而将另外一个数-2,也就是所有数的总和保持不变。题目保证最后一定有解,那么numstarget的和相同。我们将它们排序对齐之后,必然一部分数小于target一部分大于target。直观上我们很容易想到,按照贪心的思路,依次操作对齐。

每一次操作,一个数增加2,一个数减少2,带来的它们之间的差值变化是4。所以最后答案就是两个数组差值绝对值的和再除以4。

评论区中有大神进行了简单的证明:

假设nums数组中有两个值ax, aytarget数组当中有两个值bx, by。可以证明无论什么情况,直接对应都是最优的。

class Solution {
public:
    long long makeSimilar(vector<int>& nums, vector<int>& target) {
        vector<int> odd_n, even_n, odd_t, even_t;
        
        for (auto x: nums) {
            if (x % 2) odd_n.push_back(x);
            else even_n.push_back(x);
        }
        
        for (auto x: target) {
            if (x % 2) odd_t.push_back(x);
            else even_t.push_back(x);
        }
        
        #define SORT(x) sort(x.begin(), x.end())
        
        SORT(odd_n); SORT(even_n); SORT(odd_t); SORT(even_t);
        
        int n = odd_n.size();
        
        long long diff = 0;
        for (int i = 0; i < n; i++) diff += abs(odd_n[i] - odd_t[i]);
        
        n = even_n.size();
        for (int i = 0; i < n; i++) diff += abs(even_n[i] - even_t[i]);
        
        return diff / 4;
    }
};
复制代码