题意
题目链接
给了一个数组,有多组询问,每次问在区间
[
l
,
r
]
[l, r]
[l,r]内出现最多次数的数,次数是否
≥
t
h
r
e
s
h
o
l
d
\ge threshold
≥threshold的
数组长度
≤
2
∗
1
0
4
\le 2 * 10^4
≤2∗104
数组元素大小
1
≤
x
≤
1
0
4
1\le x \le 10^4
1≤x≤104
询问次数
≤
1
0
4
\le 10^4
≤104
思路
思路一
暴力做的话复杂度大概是 O ( N Q ) O(NQ) O(NQ)的, N N N为数组长度, Q Q Q为询问次数, 大概无法通过本题
思路二
分
块
分块
分块
根据询问的长度,考虑不同的做法
如果区间的长度
<
=
<=
<= s, 直接暴力计算, 时间复杂度为
O
(
s
)
O(s)
O(s)
如果区间长度 > s, 那么众数出现的次数
>
s
2
> \frac{s}{2}
>2s(题目保证), 那么可能的答案的个数
≤
2
N
s
\leq \frac{2N}{s}
≤s2N(一共N个数,出现次数
>
s
2
> \frac{s}{2}
>2s的数的个数
<
n
/
s
2
=
2
N
s
< n / \frac{s}{2} = \frac{2N}{s}
<n/2s=s2N
统计每个数在每个前缀出现的次数,枚举每个可能的数,可以在
O
(
1
)
O(1)
O(1)内得到答案,总的时间复杂度为
O
(
2
N
/
s
)
O(2N/s)
O(2N/s)
两种情况的复杂度为 O ( s ) + O ( 2 n / 2 ) O(s) + O(2n/2) O(s)+O(2n/2), 取 s = 2 N s = \sqrt {2N} s=2N, 得到最优时间复杂度 O ( n ) O(\sqrt{n}) O(n), 总复杂度为 ( ( N + Q N ) ) ((N+Q\sqrt{N})) ((N+QN))
int n, N, s;
int[][] b = new int[205][20005];
int[] d = new int[205];
int[] a;
Map<Integer, Integer> mp = new HashMap<>();
public MajorityChecker(int[] arr) {
n = arr.length;
N = 0;
this.a = arr;
for (int i = 0; i < n; i++) {
mp.put(arr[i], mp.getOrDefault(arr[i], 0) + 1);
}
int s = (int) Math.sqrt(n * 2);
mp.forEach((k, v) -> {
if (v > s / 2) {
b[++N][0] = 0;
d[N] = k;
for (int j = 0; j < n; j++) b[N][j + 1] = b[N][j] + (arr[j] == k ? 1 : 0);
}
});
}
public int query(int left, int right, int threshold) {
int len = right - left + 1;
if (len <= s) {
int res = -1;
int cnt = 0;
//求众数
for (int i = left; i <= right; i++) {
if (a[i] == res) cnt++;
else if (cnt > 0) cnt--;
else {
res = a[i];
cnt = 1;
}
}
cnt = 0;
for (int i = left; i <= right; i++) {
if (a[i] == res) cnt++;
}
if (cnt < threshold) res = -1;
return res;
} else {
for (int i = 1; i <= N; i++) {
if (b[i][right + 1] - b[i][left] >= threshold) return d[i];
}
return -1;
}
}
思路三
分
块
分块
分块
和思路二差不多,考虑对
t
h
r
e
s
h
o
l
d
threshold
threshold的分情况求解,本质上和思路二相同
考虑一个阈值
s
s
s,当
t
h
r
e
s
h
o
l
d
>
s
threshold>s
threshold>s的时候,可能的结果只有
n
s
\frac{n}{s}
sn个,这个时候使用前缀记录下这些数出现的次数,枚举每个数
O
(
1
)
O(1)
O(1)求解
当
t
h
r
e
s
h
o
l
d
<
s
threshold<s
threshold<s的时候,长度是小于
2
∗
s
2*s
2∗s的,暴力求解,复杂度和思路二也是差不多的
int N = 20005;
int len, s = 100, bsize;
int[] c = new int[N];
int[] a;
List<Integer> b;
int[][] cnt;
public MajorityChecker(int[] arr) {
len = arr.length;
this.a = arr;
b = new ArrayList<>();
for (int i = 0; i < len; i++) c[arr[i]]++;
for (int i = 1; i <= 20000; i++) if (c[i] > s) b.add(i);
bsize = b.size();
cnt = new int[N][bsize];
for (int i = 0; i < bsize; i++) {
int num = b.get(i);
for (int j = 0;j < len; j++) {
cnt[j + 1][i] = cnt[j][i] + (a[j] == num ? 1 : 0);
}
}
}
public int query(int left, int right, int threshold) {
if (threshold > s) {
for (int i = 0; i < bsize; i++) {
if (cnt[right + 1][i] - cnt[left][i] >= threshold) return b.get(i);
}
} else {
int[] d = new int[20005];
for (int i = left; i <= right; i++) {
d[a[i]]++;
if (d[a[i]] >= threshold) return a[i];
}
}
return -1;
}
思路四
线
段
树
线段树
线段树
每个节点维护区间内出现次数最大的那个数, 在用一个
v
e
c
t
o
r
vector
vector维护每个数出现的下标,那么就可以通过二分快速计算出在一个区间内某个数出现的次数了,具体实现看https://zhuanlan.zhihu.com/p/77768691吧
参考资料
https://zhuanlan.zhihu.com/p/77768691
https://leetcode.com/contest/weekly-contest-149/ranking/1/
http://longrm.com/2019/08/16/2019-08-16-segment-tree/