最值分治简介
直接从题开始吧
Luogu P4755
题意
给出一个序列,对这样的长度不为 \(0\) 的区间计数:
- \(a_l\times a_r \le \max_{i=l}^r \{a_i\}\)
分析
我们考虑这样一件事:如果我们已经知道 \(\max_{i=l}^r{a_i}\),那么我们判断 \(\langle l,r\rangle\) 是否合法是容易的。那么我们如何从一个点出发得到所有以这个点为最大值的合法区间呢?
我们可以采用启发式合并的思路。这个点将这个点对应的区间分为两部分(显然,这个点对应的区间为最大值为这个点的区间),即左部分和右部分。设这个点 \(a_i\) 对应的区间长度为 \(len\),那么左部分和右部分的长度较小值就一定小于等于 \(\frac{len}{2}\)。然后我们可以枚举这个较小的部分作为 \(l\)(或者 \(r\)),从另一部分中快速统计 \(r\)(或者 \(l\)) 的数量。怎么快速统计呢?我们直接在右边部分查找值小于等于 \(\frac{a_i}{a_l}\) 的数就行(假设枚举的是左边,枚举的是右边同理)。然后递归查找两部分。对于每个数,它最多只会被枚举 \(\Theta(\log n)\) 次(在一个区间被枚举之后在一个长度至少为这个区间两倍的区间中被枚举,最大枚举到 \(n\) 的长度)。
而这是一个二维偏序,我们可以离线下来处理避免复杂的数据结构。总共有 \(\Theta(n\log n)\) 个点,进行二维偏序就是 \(\Theta(n\log^2n)\)。
实现时,我们考虑到实际上只需要知道对于每个点,它为最值的区间就可以了,并不需要真的进行递归。我们可以通过单调栈对每个点求出对应的区间。为了避免计数重复,我们还令其对应的区间为其为最值并且左侧没有等于其的数的区间。
如下是 C++ 代码:
/*********************************************************************
程序名:
版权:
作者:
日期: 2025-10-28 13:46
说明:
*********************************************************************/
#include <bits/stdc++.h>
#define mp make_pair
#define pb push_back
#define fst first
#define sec second
#define p_q priority_queue
#define u_map unordered_map
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define drep(i,x,y) for(int i=x;i>=y;i--)
using namespace std;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
const int N = 1e6+50;
int a[N], tl, cnt;
ll ans;
vector<int> que[N];
int stk[N], gl[N], gr[N];
int lsh[N], nn;
int fwk[N];
void tc(int u, int k) {
for (; u <= nn; u += u & -u)
fwk[u] += k;
}
ll tq(int u) {
ll x = 0;
for (; u; u -= u & -u)
x += fwk[u];
return x;
}
int gi(int x) {
return upper_bound(lsh + 1, lsh + nn + 1, x) - lsh - 1;
}
int main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
rep(i, 1, n) cin >> a[i];
rep(i, 1, n) {
while (tl && a[stk[tl]] < a[i])
tl--;
gl[i] = tl ? stk[tl] + 1 : 1;
stk[++tl] = i;
}
tl = 0;
drep(i, n, 1) {
while (tl && a[stk[tl]] <= a[i])
tl--;
gr[i] = tl ? stk[tl] - 1 : n;
stk[++tl] = i;
}
// rep(i, 1, n) {
// cout << gl[i] << " " << gr[i] << endl;
// }
rep(i, 1, n) {
int ql = gl[i], qr = gr[i];
//// ans += a[i] == 1;
// cout << "F:" << i << " " << ql << " " << qr << endl;
if (i - ql <= qr - i) {
rep(j, ql, i) {
int qwq = a[i] / a[j];
que[i - 1].pb(-qwq);
que[qr].pb(qwq);
}
} else {
rep(j, i, qr) {
int qwq = a[i] / a[j];
que[ql - 1].pb(-qwq);
que[i].pb(qwq);
}
}
}
rep(i, 1, n) lsh[i] = a[i];
sort(lsh + 1, lsh + n + 1);
nn = unique(lsh + 1, lsh + n + 1) - lsh-1;
// cout << cnt << endl << "FUCK:";
rep(i, 1, n) {
tc(gi(a[i]), 1);
for (int v : que[i]) {
if (v < 0) {
ans -= tq(gi(-v));
} else
ans += tq(gi(v));
}
}
// cout << endl;
cout << ans << endl;
return 0;
}
类似的很多包含区间最值的约束的题目也可以使用这种分治方法解决,如 AT_abc282_h,CF1156E,P3246。
这种做法是不难理解的,那么它是如何想到的呢?
直接做区间最值是难以处理的,所以我们不考虑怎么去直接做区间最值,而是考虑每一个值的贡献(在这里是对哪些区间而言这个值是最值)。这是第一重思想。然后,我们考虑到怎么对一个点进行统计。显然,需要区间的左端点在这个点的左边(包括这个点),右端点在这个点的右边(包括这个点),那么我们可以枚举左边的端点,然后对于右边统计它的可行右端点,这个可以直接查找右边小于等于 \(\frac{a_i}{a_l}\) 的数来实现。但是如果左边的太长,复杂度无法保证。于是我们通过类似启发式合并的思路(枚举较小的一边)来做到更好的复杂度。
本质上,这就是在笛卡尔树进行分治。
那么我们再来看几道题:
模拟赛题
给出一个数列,对于一个区间 \([l,r]\),若区间内所有数可以通过如下规则合并成一个数,则称这样的区间是合法的:
- 选择一个数 \(i\) 和 \(j\)(\(i\neq j\)),若 \(a_i=a_j\),将 \(a_i\) 增加 \(1\) 并删去 \(a_j\)。
求数列非空子串中有多少个合法的区间。\(1\le n\le 10^5,1\le a_i\le 10^9\)。
分析
这题虽然可以通过普通的分治做到更优的复杂度,但最值分治却更好想!
显然 \([l,r]\) 合法当且仅当 \(\log_2(\sum_{i=l}^r 2^{a_i})\in\N\)。但是 \(a_i\) 可以取到 \(10^9\),直接维护 \(2^{a_i}\) 的和显然是不现实的。
那么我们考虑哈希!也就是把和模上一个数!这样一个区间的 \(2^{a_i}\) 的和就可以维护了!但是这样还是很寄,因为我们难以判断这段区间的和到底是不是 \(2\) 的整次幂了。枚举 \(2\) 的 \(1\) 到 \(10^9\) 次幂显然会超时,而且哈希冲突的概率也很高。
我们稍加思考后可以发现,这个区间如果可以合并成一个数,那么这个数显然必须比区间最大值大。然后我们再来玩一玩,就不难发现这个数不可能比区间最大值大超过 \(\log_2(r-l+1)\)。那么我们就可以枚举这 \(\Theta(\log n)\) 个数,判断区间和是否等于它就行。那么我们转化成最值分治,也就是对于前缀和 \(S_{l-1}\),找 \(S_r\) 满足 \(S_r-S_{l-1}=2^p\),其中 \(p\) 是枚举的数,等于是模意义下的。需要一个 map 和类似二维偏序的东西来维护。时间复杂度为 \(\Theta(n\log^3 n)\)。

浙公网安备 33010602011771号