前置知识

java
public static void show1(int x, int m) {
for (int p = m - 1, t = x; p >= 0; p--) {
if (1 << p <= t) {
t -= 1 << p;
System.out.println(x + "的第" + p + "位是1");
} else {
System.out.println(x + "的第" + p + "位是0");
}
}
}
java
public static void show2(int x) {
int power = 0;
// 以下注释掉的写法不对,没有考虑溢出
// while ((1 << power) <= x) {
// power++;
// }
// power--;
// 防止溢出的写法
while ((1 << power) <= (x >> 1)) {
power++;
}
System.out.println("<=" + x + "最大的2的幂,是2的" + power + "次方");
}

1.建立一张表,横向表示位置,纵向表示从某个点跳2^i次方步最远到达的位置。
第一列跳一步最远到达的点题目给出,后面的每一列都可以由状态转移方程推出

2.求从a-b需要的最少步数,例如从17-77:
首先从17-77最多要走60步,将60转换为二进制需要0-5位,从5开始不停的枚举最远到达的位置但要小于77,最后得到的二进制数+1就是最少需要的步数

题目1

P4155 [SCOI2015] 国旗计划 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
建立st表:每个线段走对应的步数能到达的最远的线段

java
package class117;
// 国旗计划
// 给定点的数量m,点的编号1~m,所有点围成一个环
// i号点一定顺时针到达i+1号点,最终m号点顺指针回到1号点
// 给定n条线段,每条线段(a, b),表示线段从点a顺时针到点b
// 输入数据保证所有线段可以把整个环覆盖
// 输入数据保证每条线段不会完全在另一条线段的内部
// 也就是线段之间可能有重合但一定互不包含
// 返回一个长度为n的结果数组ans,ans[x]表示一定选x号线段的情况下
// 至少选几条线段能覆盖整个环
// 测试链接 : https://www.luogu.com.cn/problem/P4155
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;
public class Code01_FlagPlan {
public static int MAXN = 200001;
public static int LIMIT = 18;
public static int power;
// 每条线段3个信息 : 线段编号、线段左边界、线段右边界
public static int[][] line = new int[MAXN << 1][3];
// stjump[i][p] : 从i号线段出发,跳的次数是2的p次方,能到达的最右线段的编号
public static int[][] stjump = new int[MAXN << 1][LIMIT];
public static int[] ans = new int[MAXN];
public static int n, m;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
for (int i = 1; i <= n; i++) {
line[i][0] = i;
in.nextToken();
line[i][1] = (int) in.nval;
in.nextToken();
line[i][2] = (int) in.nval;
}
compute();
out.print(ans[1]);
for (int i = 2; i <= n; i++) {
out.print(" " + ans[i]);
}
out.println();
out.flush();
out.close();
br.close();
}
public static void compute() {
power = log2(n);
build();
for (int i = 1; i <= n; i++) {
ans[line[i][0]] = jump(i);
}
}
// <=n最接近的2的幂,是2的几次方
public static int log2(int n) {
int ans = 0;
while ((1 << ans) <= (n >> 1)) {
ans++;
}
return ans;
}
public static void build() {
for (int i = 1; i <= n; i++) {
if (line[i][1] > line[i][2]) {
line[i][2] += m;
}
}
Arrays.sort(line, 1, n + 1, (a, b) -> a[1] - b[1]);
for (int i = 1; i <= n; i++) {
line[i + n][0] = line[i][0];
line[i + n][1] = line[i][1] + m;
line[i + n][2] = line[i][2] + m;
}
int e = n << 1;
for (int i = 1, arrive = 1; i <= e; i++) {
while (arrive + 1 <= e && line[arrive + 1][1] <= line[i][2]) {
arrive++;
}
stjump[i][0] = arrive;
}
for (int p = 1; p <= power; p++) {
for (int i = 1; i <= e; i++) {
stjump[i][p] = stjump[stjump[i][p - 1]][p - 1];
}
}
}
public static int jump(int i) {
int aim = line[i][1] + m, cur = i, next, ans = 0;
for (int p = power; p >= 0; p--) {
next = stjump[cur][p];
if (next != 0 && line[next][2] < aim) {
ans += 1 << p;
cur = next;
}
}
return ans + 1 + 1;
}
}
题目2

P2880 [USACO07JAN] Balanced Lineup G - 洛谷
1.建立关于最大值或最小值的st表,表示从i位置起(包含i位置)向右2^p范围上的最大值是多少。转移方程如下

2.查询从 i 到 j 的最大值:以从17-37举例:
17-37长度为21,最接近21的2的幂是4,先求出17---32(17+16-1)的最大值,然后求22(37-16+1)---37的最大值,两者取最大就是最大值;最小值同理
java
// ST表查询最大值和最小值
// 给定一个长度为n的数组arr,一共有m次查询
// 每次查询arr[l~r]上的最大值和最小值
// 每次查询只需要打印最大值-最小值的结果
// 测试链接 : https://www.luogu.com.cn/problem/P2880
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code02_SparseTableMaximumMinimum {
public static int MAXN = 50001;
// 2的15次方是<=50001且最接近的
// 所以次方可能是0~15
// 于是准备16长度够用了
public static int LIMIT = 16;
public static int[] arr = new int[MAXN];
// log2[i] : 查询<=i情况下,最大的2的幂,是2的几次方
public static int[] log2 = new int[MAXN];
public static int[][] stmax = new int[MAXN][LIMIT];
public static int[][] stmin = new int[MAXN][LIMIT];
public static void build(int n) {
log2[0] = -1;
for (int i = 1; i <= n; i++) {
log2[i] = log2[i >> 1] + 1;
stmax[i][0] = arr[i];
stmin[i][0] = arr[i];
}
for (int p = 1; p <= log2[n]; p++) {
for (int i = 1; i + (1 << p) - 1 <= n; i++) {
stmax[i][p] = Math.max(stmax[i][p - 1], stmax[i + (1 << (p - 1))][p - 1]);
stmin[i][p] = Math.min(stmin[i][p - 1], stmin[i + (1 << (p - 1))][p - 1]);
}
}
}
public static int query(int l, int r) {
int p = log2[r - l + 1];
int a = Math.max(stmax[l][p], stmax[r - (1 << p) + 1][p]);
int b = Math.min(stmin[l][p], stmin[r - (1 << p) + 1][p]);
return a - b;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
build(n);
for (int i = 1, l, r; i <= m; i++) {
in.nextToken();
l = (int) in.nval;
in.nextToken();
r = (int) in.nval;
out.println(query(l, r));
}
out.flush();
out.close();
br.close();
}
}
适用范围

题目3

java
// ST表查询最大公约数
// 给定一个长度为n的数组arr,一共有m次查询
// 每次查询arr[l~r]上所有数的最大公约数
// 测试链接 : https://www.luogu.com.cn/problem/P1890
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code03_SparseTableGCD {
public static int MAXN = 1001;
public static int LIMIT = 10;
public static int[] arr = new int[MAXN];
public static int[] log2 = new int[MAXN];
public static int[][] stgcd = new int[MAXN][LIMIT];
public static void build(int n) {
log2[0] = -1;
for (int i = 1; i <= n; i++) {
log2[i] = log2[i >> 1] + 1;
stgcd[i][0] = arr[i];
}
for (int p = 1; p <= log2[n]; p++) {
for (int i = 1; i + (1 << p) - 1 <= n; i++) {
stgcd[i][p] = gcd(stgcd[i][p - 1], stgcd[i + (1 << (p - 1))][p - 1]);
}
}
}
// 算法讲解041 - 辗转相除法求最大公约数
public static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
public static int query(int l, int r) {
int p = log2[r - l + 1];
return gcd(stgcd[l][p], stgcd[r - (1 << p) + 1][p]);
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
for (int i = 1; i <= n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
build(n);
for (int i = 1, l, r; i <= m; i++) {
in.nextToken();
l = (int) in.nval;
in.nextToken();
r = (int) in.nval;
out.println(query(l, r));
}
out.flush();
out.close();
br.close();
}
}
题目4

建立几个预处理机构:根据数组是有序的,把相同的数字分到一个桶里,统计每个桶里有多少个数字,每个桶的下标范围,每个下标对应的桶的编号。例如,求下表6~26出现次数最多的数字的个数,首先找到6的桶的编号2,2号桶的下标范围右边界到7,所以有2个2;26的桶是8,8号桶的左边界是25,所以有2个6;然后再求8-24对应的桶3-7中词频的最大值,用st表处理

java
// 出现次数最多的数有几个
// 给定一个长度为n的数组arr,该数组一定是有序的
// 一共有m次查询,每次查询arr[l~r]上出现次数最多的数有几个
// 对数器验证
import java.util.Arrays;
import java.util.HashMap;
public class Code04_FrequentValues1 {
public static int MAXN = 100001;
public static int LIMIT = 17;
public static int[] arr = new int[MAXN];
public static int[] log2 = new int[MAXN];
public static int[] bucket = new int[MAXN];
public static int[] left = new int[MAXN];
public static int[] right = new int[MAXN];
public static int[][] stmax = new int[MAXN][LIMIT];
public static void build(int n) {
// 题目给定的数值范围-100000 ~ +100000
// 把arr[0]设置成不会到达的数字即可
arr[0] = -23333333;
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (arr[i - 1] != arr[i]) {
right[cnt] = i - 1;
left[++cnt] = i;
}
bucket[i] = cnt;
}
right[cnt] = n;
log2[0] = -1;
for (int i = 1; i <= cnt; i++) {
log2[i] = log2[i >> 1] + 1;
stmax[i][0] = right[i] - left[i] + 1;
}
for (int p = 1; p <= log2[cnt]; p++) {
for (int i = 1; i + (1 << p) - 1 <= cnt; i++) {
stmax[i][p] = Math.max(stmax[i][p - 1], stmax[i + (1 << (p - 1))][p - 1]);
}
}
}
public static int query(int l, int r) {
if (l > r) {
int tmp = l;
l = r;
r = tmp;
}
int lbucket = bucket[l];
int rbucket = bucket[r];
if (lbucket == rbucket) {
return r - l + 1;
}
// a : 最左侧桶在此时l~r范围上的数字有几个
// b : 最右侧桶在此时l~r范围上的数字有几个
int a = right[lbucket] - l + 1, b = r - left[rbucket] + 1, c = 0;
if (lbucket + 1 < rbucket) {
int from = lbucket + 1, to = rbucket - 1, p = log2[to - from + 1];
c = Math.max(stmax[from][p], stmax[to - (1 << p) + 1][p]);
}
return Math.max(Math.max(a, b), c);
}
// 对数器
// 为了验证
public static void main(String[] args) {
System.out.println("测试开始");
int n = 10000;
int v = 100;
int m = 5000;
randomArray(n, v);
build(n);
for (int i = 1, l, r; i <= m; i++) {
l = (int) (Math.random() * n) + 1;
r = (int) (Math.random() * n) + 1;
if (query(l, r) != checkQuery(l, r)) {
System.out.println("出错了!");
}
}
System.out.println("测试结束");
}
// 得到随机数组
// 为了验证
public static void randomArray(int n, int v) {
for (int i = 1; i <= n; i++) {
arr[i] = (int) (Math.random() * 2 * v) - v;
}
Arrays.sort(arr, 1, n + 1);
}
// 暴力方法
// 直接遍历统计词频
// 为了验证
public static int checkQuery(int l, int r) {
if (l > r) {
int tmp = l;
l = r;
r = tmp;
}
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = l; i <= r; i++) {
map.put(arr[i], map.getOrDefault(arr[i], 0) + 1);
}
int ans = 0;
for (int v : map.values()) {
ans = Math.max(ans, v);
}
return ans;
}
}