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的字符共有b
和c
两种,而我们只能删除一个字符。
所以,我们应当在上述条件的基础上,至少增加一个判断条件,即出现频率为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
个视频的上传序列,每个视频编号为 1
到 n
之间的 不同 数字,你需要依次将这些视频上传到服务器。请你实现一个数据结构,在上传的过程中计算 最长上传前缀 。
如果 闭区间 1
到 i
之间的视频全部都已经被上传到服务器,那么我们称 i
是上传前缀。最长上传前缀指的是符合定义的 i
中的 最大值 。
请你实现 LUPrefix
类:
LUPrefix(int n)
初始化一个n
个视频的流对象。void upload(int video)
上传video
到服务器。int longest()
返回上述定义的 最长上传前缀 的长度。
提示:
1 <= n <= 10^5
1 <= video <= 10^5
video
中所有值 互不相同 。upload
和longest
总调用 次数至多不超过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 。
思路
要1
到i
之间视频全部上传成功,i
才是一个前缀。
这道题有点类似区间合并。我们需要实时维护,从1开始,往右最远能到达的位置i
,[1, i]
中间不能断。
对于合并,很自然的想到并查集,对于1
到i
之间视频是否全部上传,我们可以判断1
和i
的连通性。而对于同一个连通块(连通区间),我们维护一下这个区间的最右侧的端点即可。
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
表示该点还未出现,每次合并时都往右侧合并。则连通区间中每个点i
的p[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 开始的数组 nums1
和 nums2
,两个数组都只包含非负整数。请你求出另外一个数组 nums3
,包含 nums1
和 nums2
中 所有数对 的异或和(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个数,分别是x1
,x2
,x3
;第二个数组有4个数,分别是y1
,y2
,y3
,y4
;来看下所有能组合出的数对
x1, y1
x1, y2
x1, y3
x1, y4
---------
x2, y1
x2, y2
x2, y3
x2, y4
---------
x3, y1
x3, y2
x3, y3
x3, y4
上面每一行都是一个数对。由于异或运算满足交换律和结合律。可以观察出规律:
- 一共有
3
个y1
做异或 - 一共有
3
个y2
做异或 - ......
- 一共有
4
个x1
做异或 - 一共有
4
个x2
做异或 - ...
设数组一的元素个数为n1
,数组二的元素个数为n2
。则对于数组一中的每个元素x
,共有n2
个x
参与异或;对于数组二中的每个元素y
,共有n1
个y
参与异或。
对于一个数x
,如果有偶数个x
做异或,那么结果是0;如果是奇数个x
做异或,那么结果是x
。
于是,我们只需要判断一下n1
和n2
是奇数还是偶数,然后选择性地将对应数组的全部元素做一次异或即可。
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 开始的整数数组 nums1
和 nums2
,两个数组的大小都为 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 = 1:3 - 2 <= 2 - 2 + 1 。因为 i < j 且 1 <= 1 ,这个数对满足条件。
2. i = 0, j = 2:3 - 5 <= 2 - 1 + 1 。因为 i < j 且 -2 <= 2 ,这个数对满足条件。
3. i = 1, j = 2:2 - 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
由于nums1
和nums2
的长度相等,则我们可以计算一个nums3
,使得nums3[i] = nums1[i] - nums2[i]
那么问题变成了,对于nums3
,求解所有满足如下条件的数对(i, j)
0 <= i < j <= n - 1
nums3[i] <= nums3[j] + diff
一开始我的想到这,然后观察这个等式,发现可以利用单调性。于是想着要对nums3
进行排序。
因为只要从小到大排好序后,i
和j
之间就具有了单调性。
具体的说,假设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
不可能走回头路(往左移动)。
指针i
和j
的移动具有单调性,好像有一点双指针的意味在里面。
但是!对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的机会!