LeetCode周赛321,思维题不要慌,切忌眼高手低

大家好,我是梁唐。

今天是周一,我们照惯例来看看昨天的LeetCode周赛。

昨天的LeetCode周赛由佳期投资赞助,佳期投资是国内很有名的一家私募基金公司。据说工作强度不大,福利待遇非常不错。感兴趣的小伙伴可以了解一下。

本场周赛的难度不大,很多高手抱怨又是手速场。对于这样的场次相比于AC,更重要的是不能有罚时,提交之前要仔细斟酌,看看有无遗漏的条件和情况,避免因为无畏的粗心而罚时。

找出中枢整数

给你一个正整数 n ,找出满足下述条件的 中枢整数 x

  • 1x 之间的所有元素之和等于 xn 之间所有元素之和。

返回中枢整数 x 。如果不存在中枢整数,则返回 -1 。题目保证对于给定的输入,至多存在一个中枢整数。

题解

数学题,如果知道等差数列求和公式可以迅速算出来。

首先1到x之间的和是x(x+1)2\frac {x(x+1)} 2,x到n之间的和为(x+n)(nx+1)2\frac {(x+n)*(n-x+1)} 2。这两个式子联立我们就得到了一个关于x的方程,化简之后可以得到x=n(n+1)2x = \sqrt{\frac {n(n+1)} 2}

因为需要保证x为整数,解出x之后我们可以取整再带回式子验算一下,如果结果不对返回-1,否则返回x。

class Solution {
public:
    int pivotInteger(int n) {
        int x = sqrt(n * (n+1) / 2);

        if (2 * x * x == n * n + n) return x;
        else return -1;
    }
};
复制代码

追加字符以获得子序列

给你两个仅由小写英文字母组成的字符串 st

现在需要通过向 s 末尾追加字符的方式使 t 变成 s 的一个 子序列 ,返回需要追加的最少字符数。

子序列是一个可以由其他字符串删除部分(或不删除)字符但不改变剩下字符顺序得到的字符串。

题解

水题,注意本题当中说的是子序列而非子串,即可以不连续,那么我们只需要从头开始贪心地匹配t 串中的字符,最后剩余不能匹配的字符即是需要添加在s 串末尾的。

答案为t串的长度减去匹配的字符数量。

class Solution {
public:
    int appendCharacters(string s, string t) {
        int m = 0;
        for (auto c : s) {
            if (m < t.length() && c == t[m]) {
                m++;
            }
        }
        return t.length() - m;
    }
};
复制代码

从链表中移除节点

给你一个链表的头节点 head

对于列表中的每个节点 node ,如果其右侧存在一个具有 严格更大 值的节点,则移除 node

返回修改后链表的头节点 head

题解

分析,由于链表的长度最大为10510^5,所以不能暴力求解。

本题的难点有两个, 第一个是链表前面的元素都有可能被删除,我们无法确定究竟哪一个会是头结点。第二个难点是需要频繁删除元素修改链表。

我们一个一个来分析,首先对于头结点会被删除的问题,我们可以插入一个不会被删除的虚拟头结点。比如在本题当中,当某个节点小于右侧节点的时候就会被删除,那么我们可以插入一个近似于无穷大的值作为链表的头结点。根据题意可以保证它一定不会被删除。

对于第二个问题,我们观察分析之后发现本题存在传递性。对于当前节点u 来说,假设我们通过某种方法可以解决链表u->next的问题,那么我们只需要判断u 节点是否需要被删除即可。如果需要,那么返回u->next,如果不用返回u。利用这个传递性,我们可以很轻松地使用递归实现。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:

    ListNode* dfs(ListNode* u) {
        if (u->next == nullptr) return u;
        // 递归
        u->next = dfs(u->next);
        auto nxt = u->next;
        // 判断是否要删除u
        if (u->val < nxt->val) return nxt;
        return u;
    }

    ListNode* removeNodes(ListNode* head) {
        // 插入虚拟头结点
        ListNode *nd = new ListNode(1e9, head);
        return dfs(nd)->next;
    }
};
复制代码

统计中位数为 K 的子数组

给你一个长度为 n 的数组 nums ,该数组由从 1n不同 整数组成。另给你一个正整数 k

统计并返回 num 中的 中位数 等于 k 的非空子数组的数目。

注意:

  • 数组的中位数是按 递增 顺序排列后位于 中间 的那个元素,如果数组长度为偶数,则中位数是位于中间靠 的那个元素。
    • 例如,[2,3,1,4] 的中位数是 2[8,4,3,5,1] 的中位数是 4
  • 子数组是数组中的一个连续部分。

题解

本题拿到手觉得非常棘手,首先数组内的元素是无序的,从其中取出来的子数组也是无序的。其次又需要保证某个特定的值是子数组的中位数,看起来就更加困难了。

这个时候往往容易慌张,觉得没有头绪,做不出来了想要放弃。这个时候心态最重要,一定要稳住心态,先冷静下来再仔细读一下题目。再读一下题目就可以发现,题目当中明确说了,这n个数各不相同,并且范围是1到n。这句话信息量很大,首先说明了数组当中一定会有k,其次说明了k只会有一个。

要使得k是子数组中的中位数,首先要保证k在子数组当中。这样一来候选的子数组的数量大大减少。子数组的左侧端点必然在k的左侧,右侧端点必然在k的右侧。接着我们再分析可以发现,要保证k是中位数,只需要保证小于k和大于k的元素数量达到均衡即可。我们可以把这个子数组按照k分成左右两个部分考虑,如果我们分别知道左右两个部分当中小于k和大于k的元素的数量,那么我们就可以知道当前的子数组是否符合要求。

并且更进一步,我们还可以把左右两部分的情况进行聚合。比如我们知道某个子数组k左侧一共有3个小于k的元素,1个大于k的元素。要使得k是中位数的话,那么需要右侧大于k的元素比小于k的元素多两个或者一个。我们用数组lef记录小于k元素更多的情况,rig记录大于k元素更多的情况。在这个例子当中我们将lef[1]++, lef[2]++

最后我们把所有能够凑成k刚好是中位数的情况相乘求和即可,如lef[1] * rig[1], lef[1] * rig[0]等。本质上我们是用数组存储了小于k元素数量-大于k元素数量每一个值对应的情况数。由于这个差值可能是负数,所以我们将大于等于0的部分存储到了lef数组当中,小于0的部分存储到了rig数组当中,注意对于i=0的情况只需要计算一次。

我们一次遍历就可以求出k左右两侧所有的情况,最后再遍历一次所有匹配的情况计算总和,整体的复杂度为O(n)O(n)

class Solution {
public:
    int countSubarrays(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> lef(n+2, 0), rig(n+2, 0);

        // 找到k的位置
        int p = 0;
        for (int i = 0; i < n; i++) if (nums[i] == k) {
            p = i; 
            break;
        }

        int ret = 0;

        // 遍历k左侧,lef[x]表示小于k元素比大于k元素多x的情况
        int lt = 0, gt = 0;
        for (int i = p; i > -1; i--) {
            if (nums[i] < k) lt++;
            if (nums[i] > k) gt++;
            if (lt > gt) lef[lt-gt]++;
            else if (lt < gt) rig[gt-lt]++;
            else lef[0]++, rig[0]++;
        }
        
        lt = 0, gt = 0;
        // 遍历k右侧,使用llef, rrig
        vector<int> llef(n+2, 0), rrig(n+2, 0);
        for (int i = p; i < n; i++) {
            if (nums[i] < k) lt++;
            if (nums[i] > k) gt++;
            if (lt > gt) llef[lt-gt]++;
            else if (lt < gt) rrig[gt-lt]++;
            else llef[0]++, rrig[0]++;
        }

        // 求和求答案
        for (int i = 0; i < n; i++) {
            // k左右刚好均等
            ret += lef[i] * rrig[i];
            // k右侧元素多一个
            ret += lef[i] * rrig[i+1];
            // k左右刚好均等,这里要注意i=0的情况已经包含了,要从i=1开始
            if (i > 0) ret += rig[i] * llef[i];
            // k右侧元素多一个
            if (i > 0) ret += rig[i] * llef[i-1];
        }
        return ret;
    }
};
复制代码

严格说起来这道题的思路并不算太难想到,只不过情况比较复杂,很容易思绪混乱,需要理清楚头绪写出代码来并不容易。我个人也是非常不擅长这类问题,做起来也都感觉很棘手。

因此刚开始觉得困难也是正常的,一点小技巧是,面对这样的问题,切记心浮气躁和眼高手低。一定要勤动手,多写一点公式和推算,不要托大只用脑子想,很容易越理越乱,没了头绪。