目录
作为一个菜鸟(很菜很菜的那种),算法这种东西对我来说,可以算是“天方夜谭”(真的,是真的!),不过该讲的课迟早都得讲。最近我稍微研究了一下有关倍增算法和ST表的知识,下面的内容(多半儿都是抄的)就主要围绕着这个算法展开。
倍增
讲这个算法之前,得先了解什么是倍增。
倍增,即成倍增长,指我们在递推过程中,如果状态空间很大,线性递推没法满足复杂度是要求,那么我们可以通过成倍增长的方式,只递推状态空间中在2的整数次幂位置上的值作为代表。
依据:任何整数都可以表示为若干个2的次幂项之和。
RMQ问题
关于这个算法,最常用的应用就是RMQ问题和求LCA。这里主要说说RMQ问题。
RMQ,Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值。简单来说,RMQ问题就是求一个区间最值的一个问题,用题面的语言来说,就是给定一个长度为n的序列,求区间 [l, r] 的最大(最小)值。
ST表
ST表是解决RMQ问题的一个非常高效的工具,它基于倍增思想,可以做到O(nlogn)预处理,O(1)回答每个询问,对于询问次数很多的题,可以大大减少时间开销。换句话说,这个ST表实质上是用“空间换时间”的一个基本思想,同时它是用于解决可重复贡献问题的数据结构。
- 关于可重复贡献问题:可重复贡献问题是指对于运算opt,满足x opt x=x,则对应的区间询问就是一个可重复贡献问题。例如,最大值有max(x,x)=x,最大公约数有gcd(x,x)=x,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。
正文部分
前面说的主要是些概念,大家看看就好,不必过于认真。接下来就主要来说说ST表的打表方法,以及如何利用ST表解决RMQ问题。
Step 1:初始化
初始化分为两部分的初始化:第一部分是lg数组,记录的数值;第二部分是st数组,记录当前区间的最值(这里以最大值为例)。
先说一说lg数组,为什么要设置lg数组呢?(直接用库函数它不香吗?)因为自己定义了lg数组之后,可以避免浮点运算,加快运算速度以及消除浮点误差。下面给出lg数组的初始化方法。
lg[1] = 0;
for (int i = 2; i <= n; i++)
lg[i] = lg[i-1] + (1 << (lg[i-1] + 1) == i);
再来说说st数组。st数组是一个二维数组,初始的定义方法是 ,其中,N是指最大数据量,即数组a的大小;m是指
,它记录的是2的多少次幂,只要保证
能够包含题目所要求的数据范围就行,一般m的值设为
。我们规定,
为以 i 为左端点长度为
的序列所求的最值,即
。
for(int i=1;i<=n;i++){
st[i][0]=a[i];
}
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++)
st[i][j]=max(st[i][j-1], st[i+(1<<j)][j-1]);
}
Step 2:查询
当询问任意区间 的最值时,我们先计算出一个k,满足
,也就是使2的k次幂小于区间长度的前提下最大的k。那么“从
开始的
个数”和“以
结尾的
个数”这两段一定覆盖了整个区间
,这两段的最大值分别是
和
,二者中较大的那个就是整个区间
的最值。
int search(int l, int r)
{
int k=lg[r-l+1];
return max(st[l][k], st[r-(1<<k)+1][k]);
}
一道经典的模板题
题目背景
这是一道 ST 表经典题——静态区间最大值
请注意最大数据时限只有 0.8s,数据强度不低,请务必保证你的每次查询复杂度为 O(1)。若使用更高时间复杂度算法不保证能通过。
如果您认为您的代码时间复杂度正确但是 TLE,可以尝试使用快速读入:
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
函数返回值为读入的第一个整数。
快速读入作用仅为加快读入,并非强制使用。
题目描述
给定一个长度为 N 的数列,和 M 次询问,求出每一次询问的区间内数字的最大值。
输入格式
第一行包含两个整数 N,M,分别表示数列的长度和询问的个数。
第二行包含 N 个整数(记为 ai),依次表示数列的第 i 项。
接下来 M 行,每行包含两个整数 ,表示查询的区间为 [
] 。
输出格式
输出包含 M 行,每行一个整数,依次表示每一次询问的结果。
输入输出样例
输入 #1
8 8 9 3 1 7 5 6 0 8 1 6 1 5 2 7 2 6 1 8 4 8 3 7 1 8
输出 #1
9 9 7 7 9 8 7 9
说明/提示
对于 30% 的数据,满足1≤N,M≤10。
对于 70% 的数据,满足1≤N,M≤。
对于 100% 的数据,满足1≤N≤,1≤M≤2×
,ai∈[0,
],1≤li≤ri≤N。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int a[N];//原始输入数组
//st表,表示需要查询的数组的从下标i到下标i+2^j-1的最值
int mx[N][30];//最大值
//int mn[N][30];//最小值
//lg数组
int lg[N];
//预处理
void init(int n)
{
for (int i=1;i<=n;i++){
mx[i][0]=a[i];
// mn[i][0]=a[i];
}
lg[1]=0;
for (int i=2;i<=n;i++)
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);
for (int j=1;(1<<j)<=n;j++)
{
for (int i=1;i+(1<<j)-1<=n;i++){
// mn[i][j]=min(mn[i][j-1],mn[i+(1<<(j-1))][j-1]);
mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);
}
}
}
//查询函数
int search(int l,int r)
{
int k=lg[r-l+1];
// int k=log2(r-l+1); //这一步也可以写成log2(r-l+1);
// int t1=min(mn[l][k],mn[r-(1<<k)+1][k]);//区间最小值
int t2=max(mx[l][k],mx[r-(1<<k)+1][k]);//区间最大值
return t2;
}
int main(){
int n,q;
scanf("%d%d",&n,&q);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
init(n);
while(q--)
{
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",search(l,r));
}
return 0;
}
几个有关倍增ST表的题目
A题
题目描述
老管家是一个聪明能干的人。他为财主工作了整整 10 年。财主为了让自已账目更加清楚,要求管家每天记 k 次账。由于管家聪明能干,因而管家总是让财主十分满意。但是由于一些人的挑拨,财主还是对管家产生了怀疑。于是他决定用一种特别的方法来判断管家的忠诚,他把每次的账目按 1,2,3… 编号,然后不定时的问管家问题,问题是这样的:在 a 到 b 号账中最少的一笔是多少?为了让管家没时间作假,他总是一次问多个问题。
输入格式
输入中第一行有两个数 m,n,表示有 m 笔账 n 表示有 n 个问题。
第二行为 m 个数,分别是账目的钱数。
后面 n 行分别是 n 个问题,每行有 2 个数字说明开始结束的账目编号。
输出格式
在一行中输出每个问题的答案,以一个空格分割。
输入输出样例
输入 #1
10 3 1 2 3 4 5 6 7 8 9 10 2 7 3 9 1 10
输出 #1
2 3 1
说明/提示
对于 100% 的数据,m≤,n≤
。
题解:
额……这道题过于简单,几乎就是套用模板就行,在所有的测试点中并没有遇到a>b的情况,所以在search函数中暂时不需要判断a>b的情况,最好加上这种情况的结果,以防程序出问题。
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int M=1e5+5;
int a[M],ans[M];
int lg[M];
int st[M][35];
void init_st(int n)
{
for (int i=1;i<=n;i++)
st[i][0]=a[i];
lg[1]=0;
for (int i=2;i<=n;i++)
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);
for (int j=1;(1<<j)<=n;j++)
{
for (int i=1;i+(1<<j)-1<=n;i++)
st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
int search(int l,int r)
{
int k=lg[r-l+1];
int t=min(st[l][k],st[r-(1<<k)+1][k]);
return t;
}
int main()
{
int m,n;
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
init_st(n);
int k=0;
while (m--)
{
int l,r;
scanf("%d%d",&l,&r);
ans[k++]=search(l,r);
}
for (int i=0;i<k;i++)
printf("%d ",ans[i]);
return 0;
}
B题
Description
For the daily milking, Farmer John's N cows (1 ≤ N ≤ 50,000) always line up in the same order. One day Farmer John decides to organize a game of Ultimate Frisbee with some of the cows. To keep things simple, he will take a contiguous range of cows from the milking lineup to play the game. However, for all the cows to have fun they should not differ too much in height.
Farmer John has made a list of Q (1 ≤ Q ≤ 200,000) potential groups of cows and their heights (1 ≤ height ≤ 1,000,000). For each group, he wants your help to determine the difference in height between the shortest and the tallest cow in the group.
Input
Line 1: Two space-separated integers, N and Q.
Lines 2..N+1: Line i+1 contains a single integer that is the height of cow i
Lines N+2..N+Q+1: Two integers A and B (1 ≤ A ≤ B ≤ N), representing the range of cows from A to B inclusive.
Output
Lines 1..Q: Each line contains a single integer that is a response to a reply and indicates the difference in height between the tallest and shortest cow in the range.
Sample Input
6 3 1 7 3 4 2 5 1 5 4 6 2 2
Sample Output
6 3 0
题解:
这道题也比较的简单,题目的大意就不翻译了,这道题是要求区间 [l, r] 的最大值与最小值的差,设立两个st表,一个记录最大值,一个记录最小值,在search函数中返回这两个数的差就行。
AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=5e4+5;
int a[N];
int lg[N];
int mx[N][20],mn[N][20];
void init_st(int n)
{
for (int i=1;i<=n;i++)
{
mx[i][0]=a[i];
mn[i][0]=a[i];
}
lg[1]=0;
for (int i=2;i<=n;i++)
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);
for (int j=1;(1<<j)<=n;j++)
{
for (int i=1;i+(1<<j)-1<=n;i++)
{
mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);
mn[i][j]=min(mn[i][j-1],mn[i+(1<<(j-1))][j-1]);
}
}
}
int search(int l,int r)
{
int k=lg[r-l+1];
int t1=max(mx[l][k],mx[r-(1<<k)+1][k]);
int t2=min(mn[l][k],mn[r-(1<<k)+1][k]);
return t1-t2;
}
int main()
{
int n,q;
scanf("%d%d",&n,&q);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
init_st(n);
while (q--)
{
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",search(l,r));
}
return 0;
}
C题
Description
You are given a sequence of n integers a1 , a2 , ... , an in non-decreasing order. In addition to that, you are given several queries consisting of indices i and j (1 ≤ i ≤ j ≤ n). For each query, determine the most frequent value among the integers ai , ... , aj.
Input
The input consists of several test cases. Each test case starts with a line containing two integers n and q (1 ≤ n, q ≤ 100000). The next line contains n integers a1 , ... , an (-100000 ≤ ai ≤ 100000, for each i ∈ {1, ..., n}) separated by spaces. You can assume that for each i ∈ {1, ..., n-1}: ai ≤ ai+1. The following q lines contain one query each, consisting of two integers i and j (1 ≤ i ≤ j ≤ n), which indicate the boundary indices for the
query.
The last test case is followed by a line containing a single 0.
Output
For each query, print one line with one integer: The number of occurrences of the most frequent value within the given range.
Sample Input
10 3 -1 -1 1 1 1 1 3 10 10 10 2 3 1 10 5 10 0
Sample Output
1 4 3
题解:
从这道题开始就比较有质量了,这道题是说给定一个非递减的序列,统计区间 [l, r] 元素出现次数最多是多少。这个时候的st数组就不再存储原数组元素了,而是存储从头到尾各个元素出现的次数。需要注意的是,在答案的选取中,要避免算上 l 前面所有元素出现的次数,也就是说,需要的单独计算一下 l 这个位置上已经出现的元素次数,换句话说,l 这个位置的元素次数应当为1(这段话的解释在代码中会体现出来)。
AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+5;
int a[N],lg[N];
int st[N][20];
int cnt[N];
void init_st(int n)
{
for (int i=1;i<=n;i++)
st[i][0]=cnt[i];
lg[1]=0;
for (int i=2;i<=n;i++)
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);
for (int j=1;(1<<j)<=n;j++)
{
for (int i=1;i+(1<<j)-1<=n;i++)
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
int search(int l,int r)
{
if (l>r)
return 0;
int k=lg[r-l+1];
return max(st[l][k],st[r-(1<<k)+1][k]);
}
int main()
{
int n;
while (scanf("%d",&n)&&n)
{
int q;
scanf("%d",&q);
cnt[0]=0;
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
if (a[i]==a[i-1])
cnt[i]=cnt[i-1]+1;
else
cnt[i]=1;
}
init_st(n);
int l,r;
while (q--)
{
scanf("%d%d",&l,&r);
int t=l;
while (t<=r&&a[t]==a[t-1]) //这一部分要单独判断 l 位置的元素在区间内出现了几次
t++;
int ans=max(t-l,search(t,r)); //要记得把位置 l 的元素去除,从后面与位置 l 不一样的元素开始计算
printf("%d\n",ans);
}
}
return 0;
}
D题
You are given an array of positive integers a=[a0,a1,…,an−1] (n≥2).
In one step, the array a is replaced with another array of length n, in which each element is the greatest common divisor (GCD) of two neighboring elements (the element itself and its right neighbor; consider that the right neighbor of the (n−1)-th element is the 0-th element).
Formally speaking, a new arrayb=[b0,b1,…,bn−1] is being built from array a=[a0,a1,…,an−1] such that bi =gcd(ai,a(i+1)modn)=gcd(ai,a(i+1)modn), where gcd(x,y) is the greatest common divisor of x and y, and x mod y is the remainder of x dividing by y. In one step the array b is built and then the array a is replaced with b (that is, the assignment a := b is taking place).
For example, if a=[16,24,10,5] thenb=[gcd(16,24), gcd(24,10), gcd(10,5), gcd(5,16)] =[8,2,5,1]. Thus, after one step the array a=[16,24,10,5] will be equal to [8,2,5,1].
For a given array a, find the minimum number of steps after which all values ai become equal (that is, a0=a1=⋯=an−1). If the original array a consists of identical elements then consider the number of steps is equal to 0.
Input
The first line contains an integer t (1≤t≤). Then t test cases follow.
Each test case contains two lines. The first line contains an integer n (2≤n≤2⋅) — length of the sequence a. The second line contains n integers a0,a1,…,an−1(1≤ai≤
).
It is guaranteed that the sum of nn over all test cases doesn't exceed 2⋅.
Output
Print t numbers — answers for each test case.
Example
input
5 4 16 24 10 5 4 42 42 42 42 3 4 6 4 5 1 2 3 4 5 6 9 9 27 9 9 63
output
3 0 2 1 1
题解:
相信大家可以看懂这道题要做什么,在这儿就不翻译了。首先可以分析出,最后n个相同的数是最初的n个数取gcd,设为g,对于题目给的操作我们进行简单的分析可以发现,对于n个数任意一个数,他进行1次操作是与后面一个数去gcd,进行两次操作就是与后面两个数取gcd,进行x次操作就是与后面x个数取gcd,显然,一个数与后面n-1的数取gcd一定是g,所以答案的取值范围是[0,n-1],我们二分答案即可。
如何判断mid次是否能满足要求呢,我们就对每个数进行mid次操作,即[i,i+mid]区间的数取gcd,如果任意一个取gcd不等于g,那说明mid次不满足要求。我们发现gcd具有线性的性质,对于一个区间查询,我们就可以用线段树来维护,但是考虑到我们不需要修改,我们用ST表来维护更为简单快捷,最后的复杂度为O(nlogn)。
AC代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=4e5+5;
ll a[N];
ll gcd(ll a,ll b)
{
return b==0?a:gcd(b,a%b);
}
ll g;
int n;
ll lg[N],st[N][30];
void init_st(int n)
{
for (int i=1;i<=2*n;i++)
st[i][0]=a[i];
lg[1]=0;
for (int i=2;i<=2*n;i++)
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);
for (int j=1;(1<<j)<=2*n;j++)
{
for (int i=1;i+(1<<j)-1<=2*n;i++)
st[i][j]=gcd(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
ll search(int l,int r)
{
int k=lg[r-l+1];
return gcd(st[l][k],st[r-(1<<k)+1][k]);
}
bool check(int mid)
{
for (int i=1;i<=n;i++)
{
if (g!=search(i,i+mid))
return false;
}
return true;
}
int main()
{
int t;
scanf("%d",&t);
while (t--)
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
a[n+i]=a[i];
if (i==1)
g=a[i];
else
g=gcd(g,a[i]);
}
init_st(n);
int l=0,r=n-1;
while (l<r)
{
int mid=(l+r)>>1;
if (check(mid))
r=mid;
else
l=mid+1;
}
printf("%d\n",l);
}
return 0;
}
E题
British mathematician John Littlewood once said about Indian mathematician Srinivasa Ramanujan that "every positive integer was one of his personal friends."
It turns out that positive integers can also be friends with each other! You are given an array aa of distinct positive integers.
Define a subarray ai,ai+1,…,aj to be a friend group if and only if there exists an integer m≥2 such that ai mod m=ai+1 mod m=…=aj mod m, where xmodyxmody denotes the remainder when x is divided by y.
Your friend Gregor wants to know the size of the largest friend group in a.
Input
Each test contains multiple test cases. The first line contains the number of test cases t (1≤t≤2⋅).
Each test case begins with a line containing the integer n (1≤n≤2⋅), the size of the array a.
The next line contains n positive integers a1,a2,…,an (1≤ai≤), representing the contents of the array a. It is guaranteed that all the numbers in aa are distinct.
It is guaranteed that the sum of n over all test cases is less than 2⋅.
Output
Your output should consist of t lines. Each line should consist of a single integer, the size of the largest friend group in a.
Example
input
4 5 1 5 2 4 6 4 8 2 5 10 2 1000 2000 8 465 55 3 54 234 12 45 78
output
3 3 2 6
Note
In the first test case, the array is [1,5,2,4,6]. The largest friend group is [2,4,6], since all those numbers are congruent to 0 modulo 2, so m=2.
In the second test case, the array is [8,2,5,10]. The largest friend group is [8,2,5], since all those numbers are congruent to 2 modulo 3, so m=3.
In the third case, the largest friend group is [1000,2000]. There are clearly many possible values of m that work.
题解:
给定 n 和一个长度为 n 的数组 a,求一个最长的区间 [l,r],使得存在 m≥2 和 k,对于所有 l≤i≤r,ai≡k(mod m)(即区间内所有数对 m 取模余数相等),输出最长区间长度(区间长度定义为 r-l+1)。将题目转化一下,我们发现,满足答案的区间,相邻两个数的差分的最大公约数不为1。利用这个性质,我们将原数组进行一个差分(同时要取绝对值),得到一个差分数组,差分数组比原数组少一个数,因为n个数的差分是n-1个数。我们用ST表来维护差分数组的GCD,然后我们二分答案,二分答案区间的长度,然后暴力判断长度为mid是否可行,最后给答案加上1,复杂度O(nlogn)。
AC代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
ll gcd(ll a, ll b)
{
a=abs(a);
b=abs(b);
return b?gcd(b,a%b):a;
}
ll cf[MAXN];
int n;
ll st[MAXN][35];
int lg[MAXN];
void initST(int n)
{
for (int i=1;i<=n;i++)
st[i][0]=cf[i];
lg[1]=0;
for (int i=2;i<=n;i++)
lg[i]= lg[i-1]+(1<<(lg[i-1]+1)==i);
for (int j=1;(1<<j)<=n;j++)
{
for (int i=1;i+(1<<j)-1<=n;i++)
st[i][j]=gcd(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
ll search(int l,int r)
{
int k=lg[r-l+1];
return gcd(st[l][k],st[r-(1<<k)+1][k]);
}
int check(int x)
{
for (int i=1;i+x-1<=n;i++)
{
if (search(i,i+x-1)!=1)
return 1;
}
return 0;
}
int main()
{
int t;
scanf("%d",&t);
while (t--)
{
scanf("%d",&n);
ll pre=0;
for (int i=1;i<=n;i++)
{
ll x;
scanf("%lld",&x);
if (i!=1)
cf[i-1]=x-pre;
pre=x;
}
n--;
initST(n);
int l=0,r=n;
while(l<r)
{
int mid=(l+r+1)>>1;
if (check(mid))
l=mid;
else
r=mid-1;
}
printf("%d\n",l+1);
}
return 0;
}
ps:本人实力较菜,也是第一次写这种算法的博客,如果这里面有讲的不清楚或者有错误的,望及时指出!