实现一个 MajorityChecker 的类,它应该具有下述几个 API:
MajorityChecker(int[] arr) 会用给定的数组 arr 来构造一个 MajorityChecker 的实例。
int query(int left, int right, int threshold) 有这么几个参数:
0 <= left <= right < arr.length 表示数组 arr 的子数组的长度。
2 * threshold > right - left + 1,也就是说阈值 threshold 始终比子序列长度的一半还要大。
每次查询 query(...) 会返回在 arr[left], arr[left+1], ..., arr[right] 中至少出现阈值次数 threshold 的元素,如果不存在这样的元素,就返回 -1。
示例:
MajorityChecker majorityChecker = new MajorityChecker([1,1,2,2,1,1]);
majorityChecker.query(0,5,4); // 返回 1
majorityChecker.query(0,3,3); // 返回 -1
majorityChecker.query(2,3,2); // 返回 2
提示:
1 <= arr.length <= 20000
1 <= arr[i] <= 20000
对于每次查询,0 <= left <= right < len(arr)
对于每次查询,2 * threshold > right - left + 1
查询次数最多为 10000
思路:这道题我只想到了线段树,在题解中看到一位大佬用了分块的思想,很厉害,这里仿照它的思想写了一版,我这边简单介绍下其分块思路。
由于我们数组的长度以及查询次数都很大,因此普通的暴力做法必超时,为了降低查询耗费的时间,我们需要针对问题本身寻找能够降低时间复杂度的方法,由于本题我们要寻找的是一个区间的绝对众数,在一个区间中绝对众数要满足其出现的次数大于区间元素总个数的一半,因此对于一个区间来说,绝对众数出现的次数一定是大于len/2,其中len为区间长度,而最大的可能答案数量为2n/len,因此如果我们找到一个合适的阈值len,就能让复杂度降低。由于每次查询我们都要遍历所有可能的答案情况,我们假设我们选择的阈值为x,则复杂度至少为qx,其中q为查询次数(最大能取到10000),为了能够顺利通过该题,证明将x取sqrt(n)是可以的。
class MajorityChecker {
private int n, N, s;
private int[] a, d;
private int[][] b;
private Map map;
public MajorityChecker(int[] arr) {
n = arr.length;
N = 0;
a = new int[20005];
b = new int[205][20005];
d = new int[205];
map=new HashMap<>();
for (int i = 0; i < n; i++)
map.put(a[i] = arr[i], map.getOrDefault(arr[i], 0) + 1);
s = (int) Math.sqrt(2 * n);
for (int key : map.keySet()) {
if (map.get(key) <= s / 2)
continue;
b[++N][0] = 0;
d[N] = key;
for (int j = 0; j < n; j++)
b[N][j + 1] = b[N][j] + (a[j] == d[N] ? 1 : 0);
}
}
public int query(int left, int right, int threshold) {
int i, j, k;
if (right - left <= s) {
j = k = 0;
for (i = left; i <= right; i++) {
if (a[i] == j) k++;
else if (k > 0) k--;
else {
j = a[i];
k = 1;
}
}
for (i = left, k = 0; i <= right; i++) {
if (a[i] == j)
k++;
}
if (k < threshold) j = -1;
return j;
}
for (i = 1; i <= N; i++)
if (b[i][right + 1] - b[i][left] >= threshold)
return d[i];
return -1;
}
}
方法二:线段树,在暴力算法中维护的信息满足可加性(即可以快速合并两个子段的信息得到完整段的信息),因此可以使用线段树维护。
class MajorityChecker {
class node{
int x,y;
public node(int x,int y){
this.x=x;
this.y=y;
}
}
private int n;
private int[] a=new int[20005];
private node[] arr=new node[65536];
private List> list=new ArrayList<>();
private node Add(node a,node b) {
node res = new node(0, 0);
if (a.x == b.x) {
res.x = a.x;
res.y = a.y + b.y;
} else if (a.y < b.y) {
res.x = b.x;
res.y = b.y - a.y;
} else {
res.x = a.x;
res.y = a.y - b.y;
}
return res;
}
private void build(int id,int l,int r) {
if (l == r) {
arr[id] = new node(a[l], 1);
return;
}
int mid = (l + r) / 2;
build(id * 2, l, mid);
build(id * 2 + 1, mid + 1, r);
arr[id] = Add(arr[id * 2], arr[id * 2 + 1]);
}
private node ask(int id,int l,int r,int left,int right) {
if (left == l && right == r)
return arr[id];
int mid = (l + r) / 2;
if (right <= mid) return ask(id * 2, l, mid, left, right);
if (left > mid) return ask(id * 2 + 1, mid + 1, r, left, right);
return Add(ask(id * 2, l, mid, left, mid), ask(id * 2 + 1, mid + 1, r, mid + 1, right));
}
public MajorityChecker(int[] arr) {
n = arr.length;
for (int i = 0; i <= 20000; i++)
list.add(new ArrayList<>());
for (int i = 0; i < n; i++)
list.get(a[i] = arr[i]).add(i);
build(1, 0, n - 1);
}
public int query(int left, int right, int threshold) {
int ans = ask(1, 0, n - 1, left, right).x;
int l = lower_bound(list.get(ans), left);
int r = upper_bound(list.get(ans), right);
if (r - l < threshold) ans = -1;
return ans;
}
private int lower_bound(List list,int index) {
int res = list.size();
int l = 0, r = list.size() - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (list.get(mid) >= index) {
res = mid;
r = mid - 1;
} else
l = mid + 1;
}
return res;
}
private int upper_bound(List list,int index) {
int res = list.size();
int l = 0, r = list.size() - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (list.get(mid) > index) {
res = mid;
r = mid - 1;
} else
l = mid + 1;
}
return res;
}
}