LeetCode 88 双周赛

30 阅读6分钟

2423. 删除字符使频率相同

给你一个下标从 0 开始的字符串 word ,字符串只包含小写英文字母。你需要选择 一个 下标并 删除 下标处的字符,使得 word 中剩余每个字母出现 频率 相同。

如果删除一个字母后,word 中剩余所有字母的出现频率都相同,那么返回 true ,否则返回 false

注意:

  • 字母 x频率 是这个字母在字符串中出现的次数。
  • 必须 恰好删除一个字母,不能一个字母都不删除。

提示:

  • 2 <= word.length <= 100
  • word 只包含小写英文字母。

示例

输入:word = "abcc"
输出:true
解释:选择下标 3 并删除该字母,word 变成 "abc" 且每个字母出现频率都为 1

思路

这次的第一题,坑很多。我WA了4次。最初的想法是:统计26个小写字符的出现频率。将出现频率相同的字符归为同一类,那么统计结束后,字符种类必须为2,且较大的频率high_freq必须比较小的频率low_freq恰好多1。

// C++
class Solution {
public:
    bool equalFrequency(string s) {
        int freq[26] = {0};
        for (int i = 0; i < s.size(); i++) {
            freq[s[i] - 'a']++;
        }
        unordered_set<int> v;   
        for (auto &f : freq) {
            if (!f) continue;
            v.insert(f);
        }
        if (v.size() != 2) return false;
        int first = 0;
        int gap = 0;
        for (auto &f : v) {
            if (first == 0) first = f;
            else gap = abs(f - first);
        }
        return gap == 1;
    }
};

这个思路很明显是有问题的。我们实际上是统计了不同频率的数目。比如字符a出现了2次,字符b出现了3次,字符c出现了3次。那么不同的频率有2,3。一共有2种不同的频率。较高的频率high_freq = 3,较低的频率low_freq = 2,并且满足high_freq = low_freq + 1。但此时应该返回true吗?很显然不能。因为频率为3的字符共有bc两种,而我们只能删除一个字符。

所以,我们应当在上述条件的基础上,至少增加一个判断条件,即出现频率为high_freq的字符只能有1种。那么当满足如下条件时,一定返回true

  • 一共有2种不同的频率,2种频率之间相差1,并且较高的频率对应的字符只有1种。

另外,当2种不同频率之一是1,而且频率为1的字符种类数只有1,也应当返回true

接下来我们考虑,一定要2种不同的频率才行吗?小于2种或者大于2种行不行?

  • 小于2种频率

    只可能是1或者0。由于字符串长度至少为2,那么小于2的频率数目只可能是1。

    当只有1种频率时,是否有可能返回true呢?答案是肯定的。当只有1种频率时,只有当频率为1时(比如abcd);或字符种类只有1种时(比如aaaa);删除一个字符才是不影响的。(可自行模拟,这里的细节导致我多WA了好几次)

  • 大于2种频率

    那么最小有3种频率,至少有3种不同的字符(每种字符分别对应一种频率)。而只能删除1个字符。则最多使得3种频率变成2种频率。所以大于2种频率的,都不行。

综上,在满足如下几个条件之一时,应当返回true

  • 一共有2种不同的频率,2种频率之间相差1,并且较高的频率对应的字符只有1种
  • 一共有2种不同的频率,其中一种频率为1,且频率为1的字符只有1种
  • 一共有1种频率,且这种频率为1,或字符种类只有1种

C++:

// C++
class Solution {
public:
    bool equalFrequency(string s) {
        int freq[26] = {0};
        for (int i = 0; i < s.size(); i++) freq[s[i] - 'a']++;
        unordered_map<int, int> freq_map; // 出现次数 -> 字符种类
        for (int i = 0; i < 26; i++) {
            if (!freq[i]) continue;
            freq_map[freq[i]]++; // 出现频率为 freq[i] 的字符种类数加一
        }
        if (freq_map.size() == 1) return freq_map.begin()->first == 1 || freq_map.begin()->second == 1; 
        // 如果只有一种频率, 则只有当频率为1时满足条件; 或者只有当字符种类数为1
        
        if (freq_map.size() != 2) return false; // 否则, 必须恰好出现2种频率
        // 且频率较高者, 比频率较低者, 多1, 且频率较高者, 只出现了一种字符
        
        int low_freq = 0, high_freq = 0, high_freq_cnt = 0, low_freq_cnt = 0;
        for (auto &[k, v] : freq_map) {
            if (!high_freq) {
                high_freq = k;
                high_freq_cnt = v;
            } else {
                low_freq = k;
                low_freq_cnt = v;
                if (low_freq > high_freq) {
                    swap(low_freq, high_freq);
                    swap(low_freq_cnt, high_freq_cnt);
                }
            }
        }
        
        return high_freq == low_freq + 1 && high_freq_cnt == 1 || low_freq == 1 && low_freq_cnt == 1;
    }
};

Java:

class Solution {
    public boolean equalFrequency(String word) {
        int[] freq = new int[26];
        for (int i = 0; i < word.length(); i++) {
            int u = word.charAt(i) - 'a';
            freq[u]++;
        }
        // 每种频率下, 有多少种字符
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int i = 0; i < 26; i++) {
            if (freq[i] == 0) continue; // 这种字符频率为0, 跳过
            freqMap.put(freq[i], freqMap.getOrDefault(freq[i], 0) + 1);
        }
        // 超过2种频率, 则一定不可能
        if (freqMap.size() > 2) return false;
        if (freqMap.size() == 1) {
            for (int k : freqMap.keySet()) return k == 1 || freqMap.get(k) == 1;
        }
        // 2种频率
        int lowFreq = 0, highFreq = 0, lowFreqCnt = 0, highFreqCnt = 0;
        for (int k : freqMap.keySet()) {
            int v = freqMap.get(k);
            if (lowFreq == 0) {
                lowFreq = k;
                lowFreqCnt = v;
            } else {
                highFreq = k;
                highFreqCnt = v;
            }
        }

        if (lowFreq > highFreq) {
            int t = lowFreq;
            lowFreq = highFreq;
            highFreq = t;

            t = lowFreqCnt;
            lowFreqCnt = highFreqCnt;
            highFreqCnt = t;
        }

        return highFreq == lowFreq + 1 && highFreqCnt == 1 || lowFreq == 1 && lowFreqCnt == 1;
    }
}

2424. 最长上传前缀

给你一个 n 个视频的上传序列,每个视频编号为 1n 之间的 不同 数字,你需要依次将这些视频上传到服务器。请你实现一个数据结构,在上传的过程中计算 最长上传前缀

如果 闭区间 1i 之间的视频全部都已经被上传到服务器,那么我们称 i 是上传前缀。最长上传前缀指的是符合定义的 i 中的 最大值

请你实现 LUPrefix 类:

  • LUPrefix(int n) 初始化一个 n 个视频的流对象。
  • void upload(int video) 上传 video 到服务器。
  • int longest() 返回上述定义的 最长上传前缀 的长度。

提示:

  • 1 <= n <= 10^5
  • 1 <= video <= 10^5
  • video 中所有值 互不相同
  • uploadlongest 总调用 次数至多不超过 2 * 10^5 次。
  • 至少会调用 longest 一次。

示例

输入:
["LUPrefix", "upload", "longest", "upload", "longest", "upload", "longest"]
[[4], [3], [], [1], [], [2], []]
输出:
[null, null, 0, null, 1, null, 3]

解释:
LUPrefix server = new LUPrefix(4);   // 初始化 4个视频的上传流
server.upload(3);                    // 上传视频 3 。
server.longest();                    // 由于视频 1 还没有被上传,最长上传前缀是 0 。
server.upload(1);                    // 上传视频 1 。
server.longest();                    // 前缀 [1] 是最长上传前缀,所以我们返回 1 。
server.upload(2);                    // 上传视频 2 。
server.longest();                    // 前缀 [1,2,3] 是最长上传前缀,所以我们返回 3 。

思路

1i之间视频全部上传成功,i才是一个前缀。

这道题有点类似区间合并。我们需要实时维护,从1开始,往右最远能到达的位置i[1, i] 中间不能断。

对于合并,很自然的想到并查集,对于1i之间视频是否全部上传,我们可以判断1i的连通性。而对于同一个连通块(连通区间),我们维护一下这个区间的最右侧的端点即可。

C++:

// C++
class LUPrefix {
public:
    
    int r = 0; // 当前的最长前缀
    
    vector<int> p; // 并查集 parent 数组
    
    vector<int> len; // 某个连通区间的最右侧端点
    
    vector<bool> st; // 某个点是否已经出现
    
    int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
            len[x] = len[p[x]];
        }
        return p[x];
    }
    
    LUPrefix(int n) {
        p.resize(n + 1);
        len.resize(n + 1);
        st.resize(n + 2); // 多开2个, 有效下标是 0 ~ n + 1
        for (int i = 1; i <= n; i++) p[i] = len[i] = i;
    }
    
    void upload(int video) {
        st[video] = true;
        
        int pv = find(video);
        
        // 若左侧的点已经出现过
        if (st[video - 1]) {
            // 合并左侧
            int px = find(video - 1);
            // 一定要把左侧的p合并到右侧上去, 这样才能保证len存储的是最右侧的端点
            p[px] = pv;
        }
        
        // 若右侧的点已经出现过
        if (st[video + 1]) {
            // 合并右侧
            int px = find(video + 1);
            // 一定要往右侧合并
            p[pv] = px;
        }
        
        // 若1号点和video连通, 则更新最长前置
        if (find(1) == find(video)) r = len[video];
    }
    
    int longest() {
        return r;
    }
};

Java:

class LUPrefix {

    private int r = 0;

    private int[] p;

    private int[] len;

    private boolean[] st;

    private int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
            len[x] = len[p[x]];
        }
        return p[x];
    }

    public LUPrefix(int n) {
        p = new int[n + 1];
        len = new int[n + 1];
        st = new boolean[n + 2];
        for (int i = 1; i <= n; i++) p[i] = len[i] = i;
    }
    
    public void upload(int video) {
        st[video] = true;
        int pv = find(video);
        if (st[video - 1]) p[find(video - 1)] = pv;
        if (st[video + 1]) p[pv] = find(video + 1);
        if (find(1) == find(video)) r = len[video];
    }
    
    public int longest() {
        return r;
    }
}

其实用并查集还有更简单的写法,用p[i] = 0表示该点还未出现,每次合并时都往右侧合并。则连通区间中每个点ip[i],都一定是指向区间的右端点的。

// C++
class LUPrefix {
public:
    vector<int> p;

    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    LUPrefix(int n) {
        p.resize(n + 2);
    }
    
    void upload(int video) {
        p[video] = video;
        if (p[video - 1]) p[find(video - 1)] = video;
        if (p[video + 1]) p[video] = find(video + 1);
    }
    
    int longest() {
        return find(1);
    }
};

其实这道题还有更简单的做法,因为最长的前缀一定是不断递增的,可以利用这个递增的性质,开个数组维护一下那些出现过的点,然后更新一下最长前缀即可。

// C++
class LUPrefix {
public:
    vector<bool> v;

    int r = 0;

    LUPrefix(int n) {
        v.resize(n + 1);
    }
    
    void upload(int video) {
        v[video] = true;
        while (r + 1 < v.size() && v[r + 1]) r++;
    }
    
    int longest() {
        return r;
    }
};

2425. 所有数对的异或和

给你两个下标从 0 开始的数组 nums1nums2 ,两个数组都只包含非负整数。请你求出另外一个数组 nums3 ,包含 nums1nums2所有数对 的异或和(nums1 中每个整数都跟 nums2 中每个整数 恰好 匹配一次)。

请你返回 nums3 中所有整数的 异或和

提示:

  • 1 <= nums1.length, nums2.length <= 10^5
  • 0 <= nums1[i], nums2[j] <= 10^9

示例

输入:nums1 = [2,1,3], nums2 = [10,2,5,0]
输出:13
解释:
一个可能的 nums3 数组是 [8,0,7,2,11,3,4,1,9,1,6,3] 。
所有这些数字的异或和是 13 ,所以我们返回 13

思路

我们先随便举个样例。假设第一个数组有3个数,分别是x1x2x3;第二个数组有4个数,分别是y1y2y3y4;来看下所有能组合出的数对

x1, y1
x1, y2
x1, y3
x1, y4
---------
x2, y1
x2, y2
x2, y3
x2, y4
---------
x3, y1
x3, y2
x3, y3
x3, y4

上面每一行都是一个数对。由于异或运算满足交换律和结合律。可以观察出规律:

  • 一共有3y1做异或
  • 一共有3y2做异或
  • ......
  • 一共有4x1做异或
  • 一共有4x2做异或
  • ...

设数组一的元素个数为n1,数组二的元素个数为n2。则对于数组一中的每个元素x,共有n2x参与异或;对于数组二中的每个元素y,共有n1y参与异或。

对于一个数x,如果有偶数个x做异或,那么结果是0;如果是奇数个x做异或,那么结果是x

于是,我们只需要判断一下n1n2是奇数还是偶数,然后选择性地将对应数组的全部元素做一次异或即可。

C++:

// C++
class Solution {
public:
    int xorAllNums(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size(), n2 = nums2.size();
        int ans = 0;
        if (n1 % 2) {
            for (auto &x : nums2) ans ^= x;
        }
        if (n2 % 2) {
            for (auto &x : nums1) ans ^= x;
        }
        return ans;
    }
};

Java:

// Java
class Solution {
    public int xorAllNums(int[] nums1, int[] nums2) {
        int n1 = nums1.length, n2 = nums2.length;
        int ans = 0;
        if (n1 % 2 == 1) {
            for (int x : nums2) ans ^= x;
        }
        if (n2 % 2 == 1) {
            for (int x : nums1) ans ^= x;
        }
        return ans;
    }
}

2426. 满足不等式的数对数目

给你两个下标从 0 开始的整数数组 nums1nums2 ,两个数组的大小都为 n ,同时给你一个整数 diff ,统计满足以下条件的 数对 (i, j)

  • 0 <= i < j <= n - 1
  • nums1[i] - nums1[j] <= nums2[i] - nums2[j] + diff

请你返回满足条件的 数对数目

提示:

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

示例

输入:nums1 = [3,2,5], nums2 = [2,2,1], diff = 1
输出:3
解释:
总共有 3 个满足条件的数对:
1. i = 0, j = 13 - 2 <= 2 - 2 + 1 。因为 i < j 且 1 <= 1 ,这个数对满足条件。
2. i = 0, j = 23 - 5 <= 2 - 1 + 1 。因为 i < j 且 -2 <= 2 ,这个数对满足条件。
3. i = 1, j = 22 - 5 <= 2 - 1 + 1 。因为 i < j 且 -3 <= 2 ,这个数对满足条件。
所以,我们返回 3

思路

先对不等式nums1[i] - nums1[j] <= nums2[i] - nums2[j] + diff做一下等价变形。

通过移项,我们把i放在同一边,把j放在同一边。得到nums1[i] - nums2[i] <= nums1[j] - nums2[j] + diff

由于nums1nums2的长度相等,则我们可以计算一个nums3,使得nums3[i] = nums1[i] - nums2[i]

那么问题变成了,对于nums3,求解所有满足如下条件的数对(i, j)

  • 0 <= i < j <= n - 1
  • nums3[i] <= nums3[j] + diff

一开始我的想到这,然后观察这个等式,发现可以利用单调性。于是想着要对nums3进行排序。

因为只要从小到大排好序后,ij之间就具有了单调性。

具体的说,假设nums3从小到大排好了序,我们从左到右枚举i,对于某一个i,假设能找到一个满足nums3[i] <= nums3[j] + diff这个条件的最小的j,那么这个i,和所有大于j的坐标,都能构成有效的数对。对于i,则我们找到了可以和i组成满足条件的所有数对。则我们更新i = i + 1,继续判断。假设不满足nums3[i] <= nums3[j] + diff,则j只能往右侧走,使得nums3[j]变得更大,才有可能满足这个条件。所以我们可以看到随着i不断往右移动,j要么不动,要么往右移动,j不可能走回头路(往左移动)。

指针ij的移动具有单调性,好像有一点双指针的意味在里面。

但是!对nums3排序后,原先的下标已经被打乱了!新的下标已经不是原先的下标了。

意识到这一点,我就开始苦恼了!尝试将原先的下标进行保留,好像也行不通。因为要求i < j

想了一阵子后,突然想到用归并排序求逆序对的思路。

于是便豁然开朗。可以利用归并排序,将区间分为左右两半,先分别求出左半区间内的满足条件的数对数,右半区间内的数对数。

然后再求一下i在左侧区间,j在右侧区间的数对数。由于左侧区间和右侧区间内,元素都分别有序了,那么利用前面分析的单调性,即可统计出跨越左右区间的数对数。

于是,我们写一个归并排序,即可解决此题。

C++:

// C++
class Solution {
public:
    vector<int> tmp;
    
    long long numberOfPairs(vector<int>& nums1, vector<int>& nums2, int diff) {
        int n = nums1.size();
        vector<int> nums3(n);
        for (int i = 0; i < n; i++) nums3[i] = nums1[i] - nums2[i];
        tmp.resize(n);
        return merge_sort(nums3, 0, n - 1, diff);
    }
    
    long long merge_sort(vector<int> &v, int l, int r, int diff) {
        if (l >= r) return 0;
        int mid = l + r >> 1;
        long long ans = merge_sort(v, l, mid, diff) + merge_sort(v, mid + 1, r, diff);
        int i = l, j = mid + 1, k = 0;
        // 先统计数对的数目
        while (i <= mid && j <= r) {
            if (v[i] <= v[j] + diff) {
                ans += r - j + 1;
                i++;
            } else {
                j++;
            }
        }
        
        // 再做归并
        i = l, j = mid + 1;
        while (i <= mid && j <= r) {
            if (v[i] <= v[j]) tmp[k++] = v[i++];
            else tmp[k++] = v[j++];
        }
        while (i <= mid) tmp[k++] = v[i++];
        while (j <= r) tmp[k++] = v[j++];
        for (i = l, k = 0; i <= r; i++, k++) v[i] = tmp[k];
        tmp.clear(); // 不加这句也可以, 因为值会进行覆盖, 并不会取到旧值
        return ans;
    }
};

Java:

class Solution {

    int[] tmp;

    private long mergeSort(int[] arr, int l, int r, int d) {
        if (l >= r) return 0;
        int mid = l + r >> 1;
        long ans = mergeSort(arr, l, mid, d) + mergeSort(arr, mid + 1, r, d);
        int i = l, j = mid + 1, k = 0;
        while (i <= mid && j <= r) {
            if (arr[i] <= arr[j] + d) {
                ans += r - j + 1;
                i++;
            } else j++;
        }
        i = l;
        j = mid + 1;
        while (i <= mid && j <= r) {
            if (arr[i] <= arr[j]) tmp[k++] = arr[i++];
            else tmp[k++] = arr[j++];
        }
        while (i <= mid) tmp[k++] = arr[i++];
        while (j <= r) tmp[k++] = arr[j++];

        for (i = l, k = 0; i <= r; i++, k++) arr[i] = tmp[k];
        return ans;
    }

    public long numberOfPairs(int[] nums1, int[] nums2, int diff) {
        int n = nums1.length;
        tmp = new int[n];
        int[] nums3 = new int[n];
        for (int i = 0; i < n; i++) nums3[i] = nums1[i] - nums2[i];
        return mergeSort(nums3, 0, n - 1, diff);
    }
}

另:这道题还可以使用线段树、树状数组进行解答。

总结

这场周赛是10月1日晚上10点半的。当晚我到达福建某电竞酒店,开始了和小伙伴的峡谷之夜,并准备开始国庆之旅。于是10/1和10/2两天的周赛都没有参加。今天重做。从9点到11点,花了2个小时,把4道题目都做出来了。

哈哈哈!错过一次可能AK的机会!