颈高级前端工程师必备的 JS 设计模式入门教程,常用设计模式案例分享L

区间众数问题就是一个典型的适合用分块解决的问题。由于众数不满足区间可加性,直接使用传统数据结构(如线段树)较为困难。但块状数组通过预处理块内信息,结合零散点处理,能在亚线性时间内完成查询。

问题描述

给定长度为

n 的序列

a 和

m 次查询,每次查询区间

[

,

]

[l,r] 的众数(出现次数最多,相同取最小)。

数据范围:

,

4

×

10

4

n,m≤4×10

4

10

9

a

i

?

≤10

9

(强制在线,仅查询而没有修改)

分块设置

块大小

=

?

?

B=?

n

?

?,块数

=

?

/

?

T=?n/B?。

int B = ceil(sqrt(n)), T = (n + B - 1) / B; // B: 块大小 T: 块数量

vector b(n), L(T), R(T);

for (int i = 0; i < n; i++) b[i] = i / B;

for (int i = 0; i < T; i++) {

L[i] = i * B;

R[i] = min(n - 1, (i + 1) * B - 1); // 闭区间

}

预处理

由于众数是无法由子区间众数合并的,因此无论如何我们总需要查询一个数的区间出现次数。

预处理的内容基于这样一个事实:一个区间的众数,要么是所有中间完整块(他们一起)的众数,要么是一个出现在不完整块中的数(想想看)。

image

所以候选数的总数量是根号数量级,我们要快速解决单个数出现次数的查询

(1) 前缀和数组

cnt[k][x] 表示前

k 块中元素

x 的出现次数:

vector> cnt(T + 1);

for (int k = 0; k < T; k++) {

auto &r = cnt[k];

if (k) r = cnt[k - 1];

for (int i = L[k]; i <= R[k]; i++) {

r[a[i]]++;

}

}

这样一来,统计时我们可以前缀和相减,快速查询某个数的次数,再加上不完整块的次数即可。

(2) 块区间众数 mode

mode[i][j] 存储块

i 到块

j 的众数(离散化值):

vector> mode(T, vector(T, 0));

for (int i = 0; i < T; i++) {

unordered_map cnt;

int cur_mode = 0; // 当前众数

for (int j = i; j < T; j++) {

for (int pos = L[j]; pos <= R[j]; pos++) {

cnt[a[pos]]++;

if (!cur_mode || cnt[a[pos]] > cnt[cur_mode] ||

(cnt[a[pos]] == cnt[cur_mode] && a[pos] < cur_mode)) {

cur_mode = a[pos];

}

}

mode[i][j] = cur_mode;

}

}

查询处理

查询区间

[

,

]

[l,r] 的众数:

相邻块:暴力统计。

跨块查询:

中间完整块

[

+

1

,

?

1

]

[p+1,q?1] 的众数(通过 mode 获取)和零散点(左右边界)的所有数构成候选集。

对候选集中的每个元素,计算其在完整块中的出现次数(通过 cnt_block 差分),加上零散部分出现次数(统计一次),比较得众数。

while (m--) {

int l, r; cin >> l >> r;

l = (l + lastans - 1) % n; r = (r + lastans - 1) % n;

if (l > r) swap(l, r);

int p = b[l], q = b[r];

if (q - p <= 1) { // 相邻块,暴力统计

unordered_map cnt;

int res = 0;

for (int i = l; i <= r; i++) {

cnt[a[i]]++;

if (cnt[a[i]] > cnt[res] || (cnt[a[i]] == cnt[res] && a[i] < res)) {

res = a[i];

}

}

cout << (lastans = res) << "\n";

continue;

}

// 跨块查询

vector cand;

cand.push_back(mode[p + 1][q - 1]);

unordered_map tmp_cnt;

// 处理零散点

for (int i = l; i <= R[p]; i++) {

tmp_cnt[a[i]]++;

cand.push_back(a[i]);

}

for (int i = L[q]; i <= r; i++) {

tmp_cnt[a[i]]++;

cand.push_back(a[i]);

}

// 去重

sort(cand.begin(), cand.end());

cand.erase(unique(cand.begin(), cand.end()), cand.end());

int best = 0, max_cnt = 0;

for (int x : cand) {

int total = tmp_cnt[x] + cnt[q-1][x] - cnt[p][x];

if (total > max_cnt || (total == max_cnt && x < best)) {

max_cnt = total;

best = x;

}

}

cout << (lastans = best) << "\n";

}

离散化

参考题目:https://www.luogu.com.cn/problem/P4168。但是这道题是由于元素值范围大,要先将序列离散化至

[

1

,

]

[1,t](

t≤n),将哈希表改为数组才能通过:

vector val = a;

sort(val.begin(), val.end());

val.erase(unique(val.begin(), val.end()), val.end());

for (int i = 0; i < n; i++) {

a[i] = lower_bound(val.begin(), val.end(), a[i]) - val.begin() + 1;

}

复杂度分析

预处理:频率处理

(

)

O(n),分块众数

(

2

)

=

(

)

O(T

2

B)=O(n

n

?

)。

查询:

(

)

O(

n

?

)

本文题核心在于分块与预处理平衡。分块可以让预处理的规模降维。将序列划分为若干块,将问题分解为“块内”和“块间”两部分处理,将全局复杂问题转化为了局部简单问题。

而我们还在分块问题里引入了预处理,预处理的东西由思考问题性质得到,由于预处理的数组规模和块数相关,其大小也不会很大。这样需要一些思维量,但降低查询时开销。

这一思想可推广至其他无区间可加性或需动态维护统计量的问题,例如:

区间数值出现次数问题:如查询区间内某值的出现次数、大于某阈值的元素个数。

区间最小绝对差:维护块内有序结构,结合二分查找。

其他非线性统计量:需要自己构造巧思,维护合适的东西来降低复杂度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值