题单
序言
ST 表可以离线查询区间最值,是解决RMQ问题(区间最值问题) 的一种强有力的工具。它可以做到 O ( n log n ) \mathcal O(n\log n) O(nlogn) 预处理, O ( 1 ) \mathcal O(1) O(1) 查询。
P3865 【模板】ST 表
思路
- 核心思想是倍增。
- 预处理阶段。用 f [ i ] [ j ] f[i][j] f[i][j] 存储区间 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j−1] 中的最大值。易得,初始化为: f [ i ] [ 0 ] = a [ i ] f[i][0]=a[i] f[i][0]=a[i],转移方程为 f [ i ] [ j ] = max ( f [ i ] [ j − 1 ] , f [ i + 2 j − 1 ] [ j − 1 ] ) f[i][j] = \max(f[i][j-1],f[i +2^{j-1}][j-1]) f[i][j]=max(f[i][j−1],f[i+2j−1][j−1])。
- 查询阶段。我们记区间 [ l , r ] [l,r] [l,r] 中的最大值为 a n s ans ans, p p p 为 [ log 2 ( r − l + 1 ) ] \left[ \log_{2}({r -l+1})\right] [log2(r−l+1)]。则因为 l + 2 p − 1 − 1 < l + 2 p − 1 ≤ r l+2^{p-1}-1<l +2^p-1\leq r l+2p−1−1<l+2p−1≤r,且 l ≤ r − 2 p + 1 < l + 2 p − 1 − 1 l\leq r-2^p+1<l+2^{p-1}-1 l≤r−2p+1<l+2p−1−1,所以 a n s ans ans 为 max ( f [ l ] [ p ] , f [ r − 2 p + 1 ] [ p ] ) \max(f[l][p],f[r-2^p+1][p]) max(f[l][p],f[r−2p+1][p])。
- 时间复杂度为 O ( n log n + m ) \mathcal O(n\log n +m) O(nlogn+m)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 100, maxlg = 20;
int n, m, f[maxn][maxlg];
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
{
int a;
scanf ("%d", &a);
f[i][0] = a;
}
int lm = log2 (n);
for (int j = 1; j <= lm; j++)
for (int i = 1; i + (1 << j) <= n + 1; i++)
f[i][j] = max (f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
for (int i = 1; i <= m; i++)
{
int l, r;
scanf ("%d %d", &l, &r);
int p = log2 (r - l + 1);
printf ("%d\n", max (f[l][p], f[r - (1 << p) + 1][p]));
}
return 0;
}
收获
- 理解了最后 a n s ans ans 不直接为 f [ l ] [ p ] f[l][p] f[l][p] 的原因。
P2251 质量检测
思路
- ST 表板子题,不过是把最大值换成了求最小值。
- 时间复杂度为 O ( n log n ) \mathcal O(n\log n) O(nlogn)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 100, maxlg = 20;
int n, m, f[maxn][maxlg];
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
{
int a;
scanf ("%d", &a);
f[i][0] = a;
}
int lm = log2 (n);
for (int j = 1; j <= lm; j++)
for (int i = 1; i + (1 << j) <= n + 1; i++)
f[i][j] = min (f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
int p = log2 (m);
for (int i = 1; i + m - 1 <= n; i++)
printf ("%d\n", min (f[i][p], f[i + m - (1 << p)][p]));
return 0;
}
P1816 忠诚
思路
- 板子题 + 1 +1 +1。
- 时间复杂度为 O ( m log m ) \mathcal O(m\log m ) O(mlogm)
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 100, maxlg = 20;
int m, n, f[maxn][maxlg];
int main ()
{
scanf ("%d %d", &m, &n);
for (int i = 1; i <= m; i++)
scanf ("%d", &f[i][0]);
int lc = log2 (m);
for (int j = 1; j <= lc; j++)
for (int i = 1; i + (1 << j) <= m + 1; i++)
f[i][j] = min (f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
for (int i = 1; i <= n; i++)
{
int l, r;
scanf ("%d %d", &l, &r);
int p = log2 (r - l + 1);
printf ("%d ", min (f[l][p], f[r - (1 << p) + 1][p]));
}
return 0;
}
P1198 [JSOI2008] 最大数
思路
- 发现题目每次都是向序列最后面加数,这使得序列的区间最值没有后效性,即前面的没有涉及到新加数的区间最值不会受到影响。于是,相当于一个动态构建 ST 表的过程,每次只需要更新涉及新数的区间,而新加数是序列最后一个数使这一操作变得简单。
- 最坏时间复杂度为 O ( M log M ) \mathcal O(M\log M) O(MlogM)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 100, maxlg = 20;
char ch;
int n, m, MOD;
long long f[maxn][maxlg], t;
int main ()
{
scanf ("%d %d", &m, &MOD);
for (int i = 1; i <= m; i++)
{
long long x;
cin >> ch; scanf ("%lld", &x);
if (ch == 'A')
{
f[++ n][0] = (x + t) % MOD;
int lc = log2 (n);
for (int j = 1; j <= lc; j++)
{
int l = n - (1 << j) + 1;
f[l][j] = max (f[l][j - 1], f[l + (1 << (j - 1))][j - 1]);
}
}
else
{
int l = n - x + 1, p = log2 (x);
t = max (f[l][p], f[n - (1 << p) + 1][p]);
printf ("%lld\n", t);
}
}
return 0;
}
收获
- 知道了 ST 表不一定在已知所有数据的情况下才能建立,特殊情况下也可以动态构建 ST 表。
P2880 [USACO07JAN] Balanced Lineup G
思路
- 构建 ST 表分别求区间最大值与区间最小值。
- 时间复杂度为 O ( n log n + q ) \mathcal O(n\log n + q) O(nlogn+q)
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 100, maxlg = 20;
int n, q, f[maxn][maxlg], fi[maxn][maxlg];
int main ()
{
scanf ("%d %d", &n, &q);
for (int i = 1; i <= n; i++)
scanf ("%d", &f[i][0]), fi[i][0] = f[i][0];
int lc = log2 (n);
for (int j = 1; j <= lc; j++)
for (int i = 1; i + (1 << j) <= n + 1; i++)
{
int r = i + (1 << (j - 1));
f[i][j] = max (f[i][j - 1], f[r][j -1]);
fi[i][j] = min (fi[i][j - 1], fi[r][j -1]);
}
while (q --)
{
int l, r;
scanf ("%d %d", &l, &r);
int p = log2 (r - l + 1), ans, ansi;
ans = max (f[l][p], f[r - (1 << p) + 1][p]);
ansi = min (fi[l][p], fi[r - (1 << p) + 1][p]);
printf ("%d\n", ans - ansi);
}
return 0;
}
P2048 [NOI2010] 超级钢琴
思路
- 易得,我们需要任意区间 [ l , r ] [l,r] [l,r] 中的元素和,故我们需要用到前缀和,即用 s u m [ i ] sum[i] sum[i] 表示前 i i i 个元素的和。
- 思考如何找满足题意的区间和前 k k k 大的区间。最朴素的做法是,找出所有满足题意的区间,并排序找前 k k k 大,时间复杂度高达 O ( n 2 log n ) \mathcal O(n^2\log n) O(n2logn)。
- 考虑左端点满足题意的区间和,即 s u m [ t ] − s u m [ o − 1 ] , t ∈ [ o + l − 1 , min ( o + r − 1 , n ) ] sum[t]-sum[o-1],t\in[o+l-1,\min(o+r-1,n)] sum[t]−sum[o−1],t∈[o+l−1,min(o+r−1,n)]。不难发现在 o o o 固定时,要使区间和最大,则要使 s u m [ t ] sum[t] sum[t] 最大,是 RMQ 问题,用 ST 表解决。
- 考虑 ST 表存储的是什么。易得,我们可以用大根堆去存储满足题意的区间和。但为了满足时间复杂度的要求,我们不能计算出所有的区间和,只能把每一个可能的左端点的满足题意的最大区间和压入堆中。而因为若某左端点的最大区间和不满足前 k k k 大,以其为左端点的其他区间和不可能满足,故这样压入堆具有合理性。但是,若其满足第 k k k 大,以其为左端点的其他区间仍有可能是前 k k k 大。从而可知,ST 表中的 f [ i ] [ j ] f[i][j] f[i][j] 存储的不再是 max { s u m [ i ] ∣ i ∈ [ i , i + 2 j − 1 ] } \max\{sum[i]|i\in[i,i+2^j-1]\} max{sum[i]∣i∈[i,i+2j−1]},而是 k , s u m [ k ] ≥ s u m [ l ] , l ∈ [ i , i + 2 j − 1 ] k,sum[k]\geq sum[l],l\in[i,i+2^j-1] k,sum[k]≥sum[l],l∈[i,i+2j−1]。
- 在弄清楚 ST 表存储的元素后,考虑压入堆的元素是什么。应该为 ( o , l , r , t ) (o,l,r,t) (o,l,r,t),其中 o o o 为左端点, l l l 为满足题意的区间的最小右端点, r r r 为满足题意的区间的最大右端点, t t t 为区间和最大的区间的右端点。这样某个区间在满足前 k k k 大的前提下,就可以把 ( o , l , t − 1 , t 1 ) (o,l,t-1,t_1) (o,l,t−1,t1)(如果 t t t 不为 l l l 的话)和 ( o , t + 1 , r , t 2 ) (o,t+1,r,t_2) (o,t+1,r,t2)(如果 t t t 不为 r r r 的话)压入堆中。
- 时间复杂度为 O ( ( n + m ) log n ) \mathcal O((n+m)\log n) O((n+m)logn)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 100, maxlg = 25;
int n, k, l, r;
long long ans;
int sum[maxn], f[maxn][maxlg];
struct e
{
int o, l, r, t;
bool operator < (e a) const
{
return sum[a.t] - sum[a.o - 1] > sum[t] - sum[o - 1];
}
};
priority_queue <e, vector <e>, less <e> > q;
void Init ()
{
for (int i = 1; i <= n; i++) f[i][0] = i;
int lc = log2 (n);
for (int j = 1; j <= lc; j++)
for (int i = 1; i + (1 << j) <= n + 1; i++)
{
int x = f[i][j - 1], y = f[i + (1 << (j - 1))][j - 1];
f[i][j] = (sum[x] > sum[y] ? x : y);
}
}
int query (int li, int ri)
{
int p = log2 (ri - li + 1);
int x = f[li][p], y = f[ri - (1 << p) + 1][p];
return (sum[x] > sum[y] ? x : y);
}
int main ()
{
scanf ("%d %d %d %d", &n, &k, &l, &r);
for (int i = 1; i <= n; i++)
scanf ("%d", &sum[i]), sum[i] += sum[i - 1];
Init ();
for (int i = 1; i <= n; i++)
{
if (i + l - 1 > n) break;
int li = i + l - 1, ri = min (i + r - 1, n);
q.push ((e) {i, li, ri, query (li, ri)});
}
while (k --)
{
int o = q.top ().o, li = q.top ().l, ri = q.top ().r, t = q.top ().t;
q.pop ();
ans += (sum[t] - sum[o - 1]);
if (t != li) q.push ((e) {o, li, t - 1, query (li, t - 1)});
if (t != ri) q.push ((e) {o, t + 1, ri, query (t + 1, ri)});
}
printf ("%lld", ans);
return 0;
}
收获
- 知道了 ST 表存储的不一定是区间最值,还可以存储其下标。
- 知道了结构体优先队列大根堆和小根堆中重载时,都是重载小于号。