ST表 总结

倍增基本思想

倍增是一种通过预先计算和存储指数级间隔的信息,将线性时间操作优化为对数时间的技术。核心思想是"以2的幂次为步长"进行跳跃或查询。

ST表

概念

ST 表是一种用于解决 RMQ (区间最值查询) 问题的数据结构,通过预处理 \(O(n\log_2n)\) 时间,支持 \(O(1)\) 查询。

优点:单次询问是 \(O(1)\)

缺点:无法在低时间复杂度修改 \(ST\)

步骤

像区间最值这样的问题,我们把他们称为可重复贡献问题

可重复贡献问题就是指 \(x\)\(x\) 做运算得到的结果还是 \(x\),如 \(\min(x, x) = x\)\(\gcd(x, x) = x\) 等。

预处理

\(f_i,_j\) 为区间 \([i, i + 2^j)\) 的最大值。

\(f_i,_j\) 可以由 \(f_{i},_{j-1}\) (跳 \(2^{j-1}\) 步)和 \(f_{i+2^{j-1}},_{j-1}\) (再跳 \(2^{j-1}\) 步)转移过来。

这就是倍增。

请把指数写前面!!!!!!!

查询
  • 暴力查询

可以得知,任何数都可以拆分成若干个 \(2^k\) 的长度,每次暴力跳跃即可。

时间复杂度(\(q\) 次询问):\(O(qk\log_2n)\)

和莫队一样,k 一般取 1,但如果是 \(\gcd\) 就不好说了!

  • \(O(1)\) 查询

ST 表的核心是,任意一个区间都可以表示成至多两个长度为 \(2^k\) 的区间的并集

因为交集的部分多次运算还等于本身,所以凑出的两个区间即使有交集,也不会影响最终的答案。

处理出倍增数组之后,对于任意一个区间查询,我们都可以找到两个已经预处理好结果的区间的并集等于原区间,所以对这两个区间的结果做 1 次运算即可求出原区间的结果。

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define ll long long
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m, a[N], f[20][N];
void solve() {
  cin >> n >> m;
  rep (i, 1, n) cin >> a[i];
  rep (i, 1, n) f[0][i] = a[i];
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
  while (m--) {
    int l, r;
    cin >> l >> r;
    int j = __lg(r - l + 1);
    cout << max(f[j][l], f[j][r - (1 << j) + 1]) << '\n';
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

警示后人,如果你用的 cin,请关闭同步流!!!

例题

B - 查单词

就是 ST 表变成字符串,再手写 MIN 比较即可!

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define ll long long
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m;
string s[N], f[20][N];
string MIN(string a, string b) {
  string xx = a, yy = b;
  for (auto &x : xx) if (x >= 'a' &&  x <= 'z') x -= 32;
  for (auto &x : yy) if (x >= 'a' &&  x <= 'z') x -= 32;
  if (xx > yy) return a;
  else return b; 
}
void solve() {
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    cin >> s[i];
    f[0][i] = s[i];
  }
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = MIN(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
  while (m--) {
    int l, r;
    cin >> l >> r;
    int j = __lg(r - l + 1);
    cout << MIN(f[j][l], f[j][r - (1 << j) + 1]) << '\n';
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

C - Bad Hair Day S

可以发现,如果右端点越大,就越难满足条件,所以可以二分答案。

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define ll long long
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m, a[N], f[20][N];
int calc(int l, int r) {
  int j = __lg(r - l + 1);
  return max(f[j][l], f[j][r - (1 << j) + 1]);
}
bool check(int l,int r){
	int res = calc(l, r);
	return res < a[l - 1];
}
void solve() {
  cin >> n;
  rep (i, 1, n) cin >> a[i];
  rep (i, 1, n) f[0][i] = a[i];
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
  ll ans = 0;
  for (int i = 1; i <= n; i++) {
    int l = i + 1, r = n;
    while (l <= r) {
      int mid = (l + r) / 2;
      if(check(i + 1, mid))
        l = mid + 1;
      else
        r = mid - 1;
    }
    ans += (r - i);
  }
  cout << ans << endl;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

这里的二分其实是错的,详见彩蛋!

D - gcd 区间

RMQ 问题改一改就好了。

注意时间复杂度!

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define ll long long
#define gcd __gcd
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m, a[N], f[20][N];
void solve() {
  cin >> n >> m;
  rep (i, 1, n) cin >> a[i];
  rep (i, 1, n) f[0][i] = a[i];
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = gcd(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
  while (m--) {
    int l, r;
    cin >> l >> r;
    int j = __lg(r - l + 1);
    cout << gcd(f[j][l], f[j][r - (1 << j) + 1]) << '\n';
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

E - Sound 静音问题

这道题好毒瘤!

本来是单调队列的题目,非要用 st 表,怎么办,会报空间。

还能怎么办,m 是定长,所以指数开到 \(\log_2 m\) 就可以,或者用一个数组存下来,这样子也可以离线做!

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define ll long long
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 1e6 + 7;
constexpr int P = 998244353;
int n, m, c, a[N];
vector <vector <int>> f;
int calc(int l, int r) {
	int j = __lg(r - l + 1);
	return max(f[j][l], f[j][r - (1 << j) + 1]);
}
int calc1(int l, int r) {
	int j = __lg(r - l + 1);
	return min(f[j][l], f[j][r - (1 << j) + 1]);
}
void solve() {
  cin >> n >> m >> c;
	int k = __lg(n) + 1;
	f.resize(k);
	for (int i = 0; i < k; i++) f[i].resize(n + 1);
	rep (i, 1, n) {
		cin >> a[i];
		f[0][i] = a[i];
	}
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
	int cnt = 0;
	vector <int> ve;
  for (int i = 1; i + m - 1 <= n; i++) {
		ve.pb(calc(i, i + m - 1));
	}
	rep (i, 1, n) {
		f[0][i] = a[i];
	}
	for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = min(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
	for (int i = 0; i < (int)ve.size(); i++) {
		if (ve[i] - calc1(i + 1, i + m) <= c) {
			cnt++;
			cout << i + 1 << endl;
		}
	}
	if (!cnt) cout << "NONE" << endl;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

G - Max Min

本质和 C 没有任何区别,就是最后的答案求一个交集。

#include <bits/stdc++.h>

using namespace std;
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
#define per(i, x, y) for (int i = (x) ;i >= (y); i--)
#define lll long long
#define ull unsigned long long
#define db double
#define sz(x) ((int)x.size())
#define inf (1 << 30)
#define pb push_back
typedef pair<int, int> PII;
constexpr int N = 2e5 + 7;
constexpr int P = 998244353;
int n, x, y, a[N], f[20][N], dp[20][N];
int calc(int l, int r) {
  int j = __lg(r - l + 1);
  return max(f[j][l], f[j][r - (1 << j) + 1]);
}
int calc1(int l, int r) {
  int j = __lg(r - l + 1);
  return min(dp[j][l], dp[j][r - (1 << j) + 1]);
}
void solve() {
  cin >> n >> x >> y;
  rep (i, 1, n) cin >> a[i];
  rep (i, 1, n) f[0][i] = a[i], dp[0][i] = a[i];
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
    }
  }
  for (int j = 1; (1 << j) <= n; j++) {
    for (int i = 1; i + (1 << j) - 1 <= n; i++) {
      dp[j][i] = min(dp[j - 1][i], dp[j - 1][i + (1 << (j - 1))]);
    }
  }
  long long ans = 0;
  for (int i = 1; i <= n; i++) {
    if (calc(i, n) < x || calc1(i, n) > y) continue;
    int L = i, R = n, l = i, r = n, LL = i, RR = n, ll = i, rr = n;
    while (L < R) {
      int mid = (L + R) / 2;
      if (calc(i, mid) >= x) R = mid; // a[r] >= x;
      else L = mid + 1;
    }
    if (calc(i, n) <= x) r = n;
    else {
      while (l < r) {
        int mid = (l + r) / 2;
        if (calc(i, mid) > x) r = mid; // a[r] > x
        else l = mid + 1;
      }
      r--;
    }
    while (LL < RR) {
      int mid = (LL + RR) / 2;
      if (calc1(i, mid) <= y)
        RR = mid;
      else
        LL = mid + 1;
    }
    if (calc1(i, n) >= y) rr = n;
    else {
      while (ll < rr) {
        int mid = (ll + rr) / 2;
        if (calc1(i, mid) < y) rr = mid;
        else ll = mid + 1;
      }
      rr--;
    }
    // printf("%d %d %d %d\n", L, R, l, r);
    // printf("%d %d %d %d\n", LL, RR, ll, rr);
    ans += max(0LL, 1LL * min(r, rr) - max(R, RR) + 1);
  }
  // if (x == y) ans += n;
  cout << ans << endl;
}
int main() {
  // ios::sync_with_stdio(0);
  // cin.tie(0), cout.tie(0);
  int oT_To = 1;
  // cin >> oT_To;
  while (oT_To--) solve();
  return 0;
}

彩蛋(惨淡)

昨天上课,Jol3r 老师帮我看了我的二分,发现错完了!

int L = 0, R = n + 1;
while (L + 1 < R) {
    int mid = (L + R) / 2;
    if (check(mid))
        R = mid;
    else
        L = mid;
}

这个代码求二分查找没有任何问题,但是二分答案时可能会错过答案。

其实我 C 题代码的二分也是错的,只不过碰巧对了。

如果是二分答案,建议写

int L = 1, R = MAXN;
while (L < R) {
    int mid = (L + R) / 2;
    if (check(i, mid)) R = mid;
    else L = mid + 1;
}

这里 L 一定是答案的上界。

不然可能会错过答案。

不过问一下,这个二分是什么类型的?(左闭右闭?)

posted @ 2026-01-11 15:15  AKCoder  阅读(4)  评论(0)    收藏  举报