数据结构模板
RMQ(ST表查询区间最值)
//以查询最大值为例
状态表示: 集合:f(i,j)表示从位置i开始长度为2^j的区间的最大值;
属性:MAX
状态转移: f(i,j)=max(f(i,j-1),f(i+(1<<(j-1)),j-1));
含义:把区间[i,i+2^j],分成两半,[i,i+2^(j-1)]和[i+(1<<(j-1)),2^j],整个区间最大值就是这两段区间最大值的最大值
const int N=2e5+7,M=20;
int dp[N][M]; //存储区间最大值
int a[N];//存放每个点的值
//dp求从位置i开始长度为2^j的区间的最大值
for(int j=0;j<M;j++)
{
for(int i=1;i+(1<<j)-1<=n;i++)
{
if(!j) dp[i][j]=a[i];
else dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
}
//求任意区间的最大值;(可以预处理log)
int res=log(b-a+1)/log(2);
cout <<max(dp[a][res],dp[b-(1<<res)+1][res])<<endl;
滑动窗口
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
int read() {
int x=0; int 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-'0'; ch=getchar();}
return x*f;
}
const int maxn=5e6+100;
int num[maxn];
int q[maxn];
int Max[maxn];
int Min[maxn];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&num[i]);
}
int s=1,e=0;//起点和终点//记录下标
for(int i=1;i<=n;i++){
while(s<=e&&num[i]>=num[q[e]]) e--;//永无出头之日
q[++e]=i;
while(s<=e&&q[e]-q[s]+1>m) s++;
Max[i]=num[q[s]];
}//最大值
//维护一个单调递减的对列
s=1,e=0;
for(int i=1;i<=n;i++){
while(s<=e&&num[i]<=num[q[e]]) e--;
q[++e]=i;
while(s<=e&&q[e]-q[s]+1>m) s++;
Min[i]=num[q[s]];
}//维护一个递增的队列
for(int i=m;i<=n;i++){
printf("%d ",Min[i]);
}
cout<<endl;
for(int i=m;i<=n;i++){
printf("%d ",Max[i]);
}
}
并查集
朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
维护集合中的边的个数和点的个数
int pre[maxn];
int b[maxn];//边的个数
int d[maxn];//点的个数
for(int i=1;i<=n;i++){
b[i]=0;
d[i]=1;
pre[i]=i;
}
int find(int x) {
if (pre[x] == x) return x;
return pre[x] = find(pre[x]);
}
void marge(int a,int b){
int t1=find(a);
int t2=find(b);
b[t2]++;
if(t1!=t2)
{
pre[t1]=t2;
b[t2]+=b[t1];
d[t2]+=d[t1];
}
}
维护到祖宗节点距离的并查集
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
单调栈
单调队列是一种思想, 每次取队头,队头如果满足条件,那么一定是最优的(最小或最大的,最长最远的,数量最多的,潜力更大,拓展性更强,生存能力更高,节点入队时间短的,等等....);然后把队尾那些"劣质”的节点信息给弹出;(一般队列里都存放下标)
滑动窗口
int num[maxn];
int q[maxn];
int Max[maxn];
int Min[maxn];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&num[i]);
}
int s=1,e=0;//起点和终点//记录下标
for(int i=1;i<=n;i++){
while(s<=e&&num[i]>=num[q[e]]) e--;//永无出头之日
q[++e]=i;
while(s<=e&&q[e]-q[s]+1>m) s++;
Max[i]=num[q[s]];
}//最大值
//维护一个单调递减的对列
s=1,e=0;
for(int i=1;i<=n;i++){
while(s<=e&&num[i]<=num[q[e]]) e--;
q[++e]=i;
while(s<=e&&q[e]-q[s]+1>m) s++;
Min[i]=num[q[s]];
}//维护一个递增的队列
for(int i=m;i<=n;i++){
printf("%d ",Min[i]);
}
cout<<endl;
for(int i=m;i<=n;i++){
printf("%d ",Max[i]);
}
}
最大子段和(限制区间长度)
这个就是滑动窗口的应用,首先维护前缀和,然后对于s[i]
,我们需要维护规定区间内的s[j]
的最小值。最大字段和就是\(s[i]-s[j]_{min}\)
for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]), s[i] += s[i - 1];
int res = -INF;
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )//核心代码
{
if (q[hh] < i - m) hh ++ ;
res = max(res, s[i] - s[q[hh]]);
while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
q[ ++ tt] = i;
}
字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
查询子串再减去子串中的一个字符
int get_s(int l, int r, int x)
//去掉x位置的字符得到的字符串的hash值
{
return get_hash(l, x-1) * p[r-x] + get_hash(x+1, r);
}
查询两个子串拼接的hash
int get_s1_s2(int l1, int r1, int l2, int r2){
return get_hash(l1, r1) * p[r2-l2+1] + get_hash(l2, r2);
}
KMP——字符串匹配
例题
KMP最小循环节、循环周期:
定理:假设S的长度为 len,则S存在最小循环节,循环节的长度L为 len-next[len] ,子串为S[0…len-next[len]-1]。
(1)如果 len 可以被 len - next[len] 整除,则表明字符串S可以完全由循环节循环组成,循环周期 T=len/L。
(2)如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数 L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。
(3) 循环节出现的长度就是当前位置的长度
模板题
void getnext(){
int j=0,k=-1;
ne[0]=-1;
while(j<lena){
if(k==-1||a[j]==a[k]){
j++;
k++;
ne[j]=k;
}
else{
k=ne[k];
}
}
}
int kmp(){
int i=0,j=0;
int ans=0;
getnext();
while(j<lenb){
if(i==-1||a[i]==b[j]){
i++;
j++;
}
else{
i=ne[i];
}
if(i==lena){
//i=ne[i];
ans++;
}
}
return ans;
}
求循环节
int m=len-Next[len];
if(len%m==0)//这个是代表是不是真好够整个循环
cout<<m <<endl; //每个循环节的长度
cout<<len/m<<endl; //循环几次
m=lena-ne[lena],这个是循环节,
lena%m==0&&lena/m>=2这个是说正好是一个循环,并且循环长度大于2
m-lena%m//是补充几个构成循环