基础数据结构
栈
基本实现:
int stack[100005],tot=0;
int top(){
if(!tot)return 0;//栈空
return stack[tot];
}
void pop(){
if(!tot)return;//非法
--tot;
return ;
}
void push(int x){
stack[++tot]=x;
return ;
}
bool empty(){
return tot>0;
}
int size(){
return tot;
}
表达式计算:
后缀表达式计算:
1.建立一个栈,逐步扫描1~n
1)如果遇到一个数,入栈
2)如果遇到运算符,就取出栈顶两个数进行计算,完事后结果放回栈顶
2.完成后栈顶即为答案
中缀表达式转后缀:
(1)如果遇到一个数,输出该数
(2)如果遇到左括号,入栈
(3)如果遇到右括号,不断取出栈顶并输出,直到栈顶为左括号,然后将左括号出栈
(4)如果遇到运算符,只要*栈顶符号优先级不低于新符号*,就不断取出栈顶并输出
2.依次取出并输出栈内剩余符号即可
单调栈
思想:借助栈的先进后出结构完成单调性处理
详细的说:在一个栈中单调递减(递增)地存储一列决策,借助单调性及时处理掉不可能的决策
例题:直方图中最大的矩形
首先:如果前面的矩形高度单调递增,答案就是以每一个矩形的高度作为最终高度并延申至边界,对每一个高度取最大值即可
那么如果下一个进入决策序列的矩形比上一个矩形要矮,那么在决策序列中所以比该矩形更高的矩形高出它的那一部分的高度就没有了用处(即它就是以它的高度为答案的矩形的最大高度)
那么既然没用,那就删了ing
维护一个单调递增的矩形高度序列(宽度累加)我们设
a
i
a_i
ai表示第i个矩形高度,设
a
k
a_k
ak表示下一个矩形,设
s
t
a
c
k
stack
stack表示我们的单调栈(内存高度和宽度)
步骤
1.若
a
k
>
s
t
a
c
k
.
t
o
p
.
h
i
g
h
a_k>stack.top.high
ak>stack.top.high直接将
a
k
a_k
ak入栈,宽度为1
2.否则不断弹出栈顶累计宽度乘上栈顶高度更新答案,直到栈顶小于
a
k
a_k
ak
3.将一个宽度为累计宽度+1(
a
k
a_k
ak的宽度),高度为
a
k
a_k
ak的矩形入栈
最后就得到了答案
思想的关键在于及时地排除不可能的决策/选项等,保持决策集合的单调性(秩序性)和高度有效性
C o d e Code Code:
while(cin>>n&&n){
ans=0; p=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
a[n+1]=0;
for(int i=1;i<=n+1;i++){
if(a[i]>s[p]) s[++p]=a[i],w[p]=1;
else{
int width=0;
while(s[p]>a[i]){
width+=w[p];
ans=max(ans,(long long)width*s[p]);
p--;
}
s[++p]=a[i],w[p]=width+1;
}
}
cout<<ans<<endl;
}
队列
一种先进先出的数据结构,C++的queue就是一个队列
基本实现:
int head,tail=1,a[100005];
void push(int val){
a[++head]=val;
}
void pop(){
tail++;
}
int top(){
return a[tail];
}
int size(){
return head-tail+1;
}
bool empty(){//队列是否为空
return tail>head;
}
/*STL queue:
queue<int>q;//建立一个空队列(int型)
q.push(a);//入队
q.top();//取队头
q.pop();//删队头
q.empty();//是否为空
*/
单调队列
例:最大子序和:
问:给定一个长度为N的整数序列,从中找出一段不超过M的连续子序列,使得子序列和最大(
N
,
M
<
=
3
∗
1
0
5
N,M<=3*10^5
N,M<=3∗105)
解:设这个序列是A
计算区间和问题,一般用前缀和,记
S
i
=
∑
k
=
1
i
A
k
S_i=\sum_{k=1}^iA_k
Si=∑k=1iAk,则问题变为求一对
x
,
y
(
x
<
y
)
x,y(x<y)
x,y(x<y)使得
S
y
−
S
x
S_y-S_x
Sy−Sx最大并且
y
−
x
<
=
M
y-x<=M
y−x<=M
设区间右端点为 i i i,当我们枚举 i i i时,问题就是找到一个左端点 j j j,使得 j ϵ [ i − m , i − 1 ] j\epsilon[i-m,i-1] jϵ[i−m,i−1]并且 S j S_j Sj最小
比较一下两个位置 j , k j,k j,k若 k < j < i k<j<i k<j<i并且 S k > = S j S_k>=S_j Sk>=Sj那么对于所有大于等于 i i i的右端点, k k k永远不会是最优选择,那么 k k k就可以被抛弃了
综上,可能成为最优选择的策略集合一定是一个下标位置递增,对应前缀和也递增的序列,用一个队列保存
随着 i i i的从前往后扫描,对于每一个 i i i,执行:
1.判断队头决策与 i i i的距离是否超出 M M M的范围,若超出则出队
2.此时队头就算右端点为i,左端点为j的最优解
3.不断删除队尾决策,直到队尾的 S S S值小于 S i S_i Si,将 S i S_i Si入队
int l=1,r=1;
q[1]=0;
for(int i=1;i<=n;i++){
while(l<=r&&q[l]<i-m)l++;
ans=max(ans,sum[i]-sum[q[l]])
while(l<=r&&sum[q[l]]>=sum)r--;
q[++r]=i;
}
这就是著名的单调队列算法,它的思想是在决策集合(队列)中及时排除一定不是最优解的选项
链表
基础操作:
struct Node{
int val;
int prev,next;
}node[SIZE];
int head,tail,tot;
void init(){
tot=2;
head=1,tail=2;
node[head].next=tail;
node[tail].prev=head;
}
int insert(int p,int val){
int q=++tot;
node[q].val=val;
node[node[p].next].prev=q;
node[q].next=node[p].next;
node[p].next=q;
node[q].prev=p;
}
void remove(int p){
node[node[p].prev].next=node[p].next;
node[node[.].next].prev=node[p].prev;
}
void clear(){
memset(node,0,sizeof node);
head=tail=tot=0;
}
邻接表存图
//加入有向边u->v,边权为z
void add(int u,int v,int z){
node[++tot].v=v,node[tot].val=z,node[tot].next=head[u],head[u]=tot;
}
//访问从u出发的所有边
for(int i=head[u];i;i=node[i].next){
//此时node[i]就是边u->node[i].v
}
HASH
思想:用一定的方法将需要处理的信息建立映射,与散列表类似,一般由hash函数与链表共同实现
Hash冲突:对每一个hash位置开一个链表,hash值相同的就给他放同一个链表上去
字符串HASH
流程:
1.取一固定值 P P P,将字符串看作 P P P进制数,并分配一个大于0的数值,代表每种字符
2.取一固定值 M M M, 求出该数对 M M M的余数为该字符串的 H A S H HASH HASH值
一般取 P = 131 P=131 P=131或 P = 13331 P=13331 P=13331,用 u n s i g n e d l o n g l o n g unsigned \ long \ long unsigned long long 来存这个数,这样就等于 M = 2 64 M=2^{64} M=264
基础操作:记字符串 S S S的 h a s h hash hash值为 H ( S ) H(S) H(S)
1.已知 H ( S ) H(S) H(S)和 H ( C ) H(C) H(C),求 H ( S + C ) H(S+C) H(S+C)
H ( S + C ) = ( H ( S ) ∗ P s i z e ( C ) + H ( C ) ) m o d M H(S+C)=(H(S)*P^{size(C)}+H(C)) mod M H(S+C)=(H(S)∗Psize(C)+H(C))modM
2.已知 H ( S + T ) H(S+T) H(S+T)和 H ( S H(S H(S),求 H ( T ) H(T) H(T)
H
(
T
)
=
(
H
(
S
+
T
)
−
H
(
S
)
∗
P
s
i
z
e
(
T
)
)
m
o
d
M
H(T)=(H(S+T)-H(S)*P^{size(T)}) mod M
H(T)=(H(S+T)−H(S)∗Psize(T))modM
这样,我们就可以在
O
(
n
)
O(n)
O(n)的时间内预处理出字符串的所有前缀的Hash值,并且在
O
(
1
)
O(1)
O(1)时间内查找它任意子串的值
例题:最长回文子串:
题目:给定一个串
S
S
S,求TA的最长回文子串长度
解:
将S正着倒着都做一次
h
a
s
h
hash
hash,记正向
h
a
s
h
hash
hash为
H
H
H,逆向
h
a
s
h
hash
hash为
R
R
R
分回文串奇偶讨论:
1.奇回文串
S
[
1
M
]
,
M
=
2
k
−
1
S[1~M],M=2k-1
S[1 M],M=2k−1(k是整数),
S
[
k
]
S[k]
S[k]即为串的中点
则需满足
H
(
S
[
1
k
−
1
]
)
=
R
(
S
[
k
+
1
M
]
)
H(S[1~k-1])=R(S[k+1~M])
H(S[1 k−1])=R(S[k+1 M])
2.偶回文串
S
[
1
M
]
S[1~M]
S[1 M],
M
=
2
k
M=2k
M=2k,
则需满足
H
(
S
[
1
k
]
)
=
R
(
S
[
k
+
1
M
]
)
H(S[1~k])=R(S[k+1~M])
H(S[1 k])=R(S[k+1 M])
于是乎,我们可以枚举最长回文子串的中心位置
i
i
i
1.寻找一个最大的整数
q
q
q,使得
H
(
S
[
i
−
q
i
]
)
=
R
(
S
[
i
i
+
q
]
)
H(S[i-q~i])=R(S[i~i+q])
H(S[i−q i])=R(S[i i+q])且
q
<
i
q<i
q<i,
i
+
q
<
=
s
i
z
e
(
S
)
i+q<=size(S)
i+q<=size(S)
整个串长度为
2
q
+
1
2q+1
2q+1,
2.寻找一个最大的整数
p
p
p,使得
H
(
S
[
i
−
p
i
−
1
]
)
=
R
(
S
[
i
i
+
p
−
1
]
)
H(S[i-p~i-1])=R(S[i~i+p-1])
H(S[i−p i−1])=R(S[i i+p−1])
a
n
s
=
m
a
x
(
a
n
s
,
m
a
x
(
2
∗
p
,
2
∗
q
+
1
)
)
ans=max(ans,max(2*p,2*q+1))
ans=max(ans,max(2∗p,2∗q+1))
由回文串的性质,我们可以对p,q进行二分答案
总复杂度
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
Code:
//Author:XuHt
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ull unsigned long long
using namespace std;
const int N = 1000006, P = 13331;//or P = 131
char s[N];
ull f1[N], f2[N], p[N];
ull H1(int i, int j) {//hash(i~j)=hash(1~j)-hash(1-i)*(p^(j-i+1)) -->i<j
return (f1[j] - f1[i - 1] * p[j - i + 1]);
}
ull H2(int i, int j) {//hash(j~i)=hash(len~i)-hash(len~j)*(p^(j-i+1)) -->i<j,反
return (f2[i] - f2[j + 1] * p[j - i + 1]);
}
int main() {
int id = 0;
p[0] = 1;
for (int i = 1; i < N; i++) p[i] = p[i - 1] * P;
while (scanf("%s", s + 1) && !(s[1] == 'E' && s[2] == 'N' && s[3] == 'D')) {
++id;
int ans = 0, len = strlen(s + 1);
f2[len+1] = 0;
for (int i = 1; i <= len; i++) f1[i] = f1[i - 1] * P + s[i];//(正)hash
for (int i = len; i; i--) f2[i] = f2[i + 1] * P + s[i];//(反)hash
for (int i = 1; i <= len; i++) {
int l = 0, r = min(i - 1, len - i);//min (size(1~i-1),size(i+1~len)) 注意是min,max会越界
while (l < r) {
int mid = (l + r + 1) >> 1;
if (H1(i - mid, i - 1) == H2(i + 1, i + mid)) l = mid;//hash(正)(i-mid~i-1)==hash(反)(i+1,i+mid)
//size=mid(回文)-->at this time:size(all str)=2mid+1
else r = mid - 1;
}
ans = max(l * 2 + 1, ans);
l = 0, r = min(i - 1, len - i + 1);//[i-mid~i-1]<-->[i,i+mid-1]
while (l < r) {
int mid = (l + r + 1) >> 1;
if (H1(i - mid, i - 1) == H2(i, i + mid - 1)) l = mid;
else r = mid - 1;
}
ans = max(l * 2, ans);
}
printf("Case %d: %d\n", id, ans);
}
return 0;
}
二维字符串hash
具体思想:将矩阵的行先hash成一个值,然后再对所有列的这个值再hash一次
记 h i , j h_{i,j} hi,j为矩形 ( 1 , 1 , i , j ) (1,1,i,j) (1,1,i,j)的hash值, a i , j a_{i,j} ai,j指矩形a中第i行第j列的字符
unsigned h[size1][size2];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
h[i][j]=h[i][j-1]*p1+(int)a[i][j];//这里进行行hash
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
h[i][j]=h[i-1][j]*p2+h[i][j];//这里进行列hash,采用不同的p值以免被卡
}
}
至于矩形 ( x 1 , y 1 , x 2 , y 2 ) (x_1,y_1,x_2,y_2) (x1,y1,x2,y2)的hash值可以这么得到:
a s k ( x 1 , y 1 , x 2 , y 2 ) = h [ x 2 ] [ y 2 ] − h [ x 2 ] [ y 1 − 1 ] ∗ p 1 y 2 − y 1 + 1 − h [ x 1 − 1 ] [ y 2 ] ∗ p 2 x 2 − x 1 + 1 + h [ x 1 − 1 ] [ y 1 − 1 ] ∗ p 1 y 2 − y 1 + 1 ∗ p 2 x 2 − x 1 + 1 ask(x_1,y_1,x_2,y_2)=h[x_2][y_2]-h[x_2][y_1-1]*p1^{y_2-y_1+1}-h[x_1-1][y_2]*p2^{x_2-x_1+1}+h[x_1-1][y_1-1]*p1^{y_2-y_1+1}*p2^{x_2-x_1+1} ask(x1,y1,x2,y2)=h[x2][y2]−h[x2][y1−1]∗p1y2−y1+1−h[x1−1][y2]∗p2x2−x1+1+h[x1−1][y1−1]∗p1y2−y1+1∗p2x2−x1+1
例题:
给定一个 M 行 N 列的 01 矩阵(只包含数字 0 或 1 的矩阵),再执行 Q 次询问,每次询问给出一个 A 行 B 列的 01 矩阵,求该矩阵是否在原矩阵中出现过。
输入格式
第一行四个整数 M,N,A,B。
接下来一个 M 行 N 列的 01 矩阵,数字之间没有空格。
接下来一个整数 Q。
接下来 Q 个 A 行 B 列的 01 矩阵,数字之间没有空格。
输出格式
对于每个询问,输出 1 表示出现过,0 表示没有出现过。
数据范围
A≤100,M,N,B≤1000,Q≤1000
话不多说,二维hash裸题
C
o
d
e
Code
Code:
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
typedef unsigned long long ULL;
const int N = 1010, M = N * N, P = 131;
int n, m, a, b;
ULL hashv[N][N], p[M];
char str[N];
ULL calc(ULL f[], int l, int r){
return f[r] - f[l - 1] * p[r - l + 1];
}
int main(){
scanf("%d%d%d%d", &n, &m, &a, &b);
p[0] = 1;
for (int i = 1; i <= n * m; i ++) p[i] = p[i - 1] * P;
//对整个矩阵计算hash值
for (int i = 1; i <= n; i ++){
scanf("%s", str + 1);
for (int j = 1; j <= m; j ++) hashv[i][j] = hashv[i][j - 1] * P + str[j] - '0';
}
//计算出符合要求的a * b的矩阵,插入到set中,方便查询。
unordered_set<ULL> S;
for (int i = b; i <= m; i ++){
ULL s = 0;
int l = i - b + 1, r = i;
for (int j = 1; j <= n; j ++){
s = s * p[b] + calc(hashv[j], l, r);// 每一行计算,都需要把上一行hash值往上抬,再加上当前行的hash值
if (j - a > 0) s -= calc(hashv[j - a], l, r) * p[a * b]; //如果多出来的需要减掉。
if (j >= a) S.insert(s);//插入到set中,方便查询。
}
}
int Q;
scanf("%d", &Q);
while (Q --){
ULL s = 0;
for (int i = 0; i < a; i ++){
scanf("%s", str);
for (int j = 0; j < b; j ++) s = s * P + str[j] - '0';
}
if (S.count(s)) puts("1");
else puts("0");
}
return 0;
}
KMP
作用:在线性时间内找出字符串A是否是B的子串并且求出A在B中每次出现的位置
步骤:
1.对A进行自我匹配,求出数组
n
e
x
t
next
next,其中
n
e
x
t
i
=
max
j
next_i=\max{j}
nexti=maxj,其中
j
<
i
j<i
j<i且
A
[
i
−
j
+
1
]
A[i-j+1]
A[i−j+1]~
A
[
i
]
=
A[i]=
A[i]=
A
[
1
]
A[1]
A[1] ~
A
[
j
]
A[j]
A[j],特别的,当J不存在,令
n
e
x
t
i
=
0
next_i=0
nexti=0
2.对A,B进行匹配:求出数组
f
f
f,其中
f
[
i
]
=
m
a
x
j
f[i]=max{j}
f[i]=maxj,其中
i
≥
j
i\geq j
i≥j且B[i-j+1i]=A[1j]
C o d e Code Code
next[1]=0;
for(int i=2,j=0;i<=n;i++){
while(j>0 && a[i]!=a[j+1])j=next[j];
if(a[j+1]==a[i])j++;
next[i]=j;
}//求next
for(int i=1;j=0;i<=m;i++){
while(j>0 && (j==n||b[i]!=a[j+1]))j=next[j];
if(b[i]==a[j+1])j++;
f[i]=j;
//if(f[i]==n)匹配成功
}
进一步的,求循环元:
定义:若一个字符串S是由一个字符串T重复K次形成的,那么T是S的循环元,使K最大的循环元叫做字符串的最小循环元,此时K是最大循环次数
定理:
S[1~i]具有长度为len<i的循环元的充分条件是len能整除i,
并且
S
[
i
−
n
e
x
t
[
i
]
+
1
S[i-next[i]+1
S[i−next[i]+1 ~
i
]
=
S
[
1
i]=S[1
i]=S[1 ~
n
e
x
t
[
i
]
]
next[i]]
next[i]],
并且不存在更大的
n
e
x
t
[
i
]
next[i]
next[i]满足这个条件
据定理,得:
若
i
−
n
e
x
t
[
i
]
i-next[i]
i−next[i]能整除
i
i
i,S[1~i]的最小循环元就是S[1至i-next[i]],最大循环次数就是
i
/
(
i
−
n
e
x
t
[
i
]
)
i/(i-next[i])
i/(i−next[i])
拓展,如果S不存在一个可以被整除的循环元,那么 n − n e x t [ n ] n-next[n] n−next[n]就是这个不整除循环元的长度
字符串的最小表示
定义:给定一个字符串S[1至n],若我们不断将它的最后一个字符放在开头,最终会得到n个字符串,则称这n个字符串是循环同构的,其中字典序最小的一个就称为字符串S的最小表示
求法:
设
B
[
i
]
B[i]
B[i]是S的第i个循环同构串,
S
S
=
S
+
S
SS=S+S
SS=S+S,显然
B
[
i
]
B[i]
B[i]=
S
S
[
i
,
i
+
n
−
1
]
SS[i,i+n-1]
SS[i,i+n−1]
1.初始化
i
=
1
,
j
=
2
i=1,j=2
i=1,j=2
2.直接从前向后扫描,比较
B
[
i
]
a
n
d
B
[
j
]
B[i] and B[j]
B[i]andB[j]是否相等
(1)若全相等,则S有更小的循环元,,并且该循环元已经扫描完成,此时
B
[
m
i
n
(
i
,
j
)
]
B[min(i,j)]
B[min(i,j)]即为最小表示
(2)在
i
+
k
i+k
i+k和
b
+
k
b+k
b+k处发现不等
1)若
S
S
[
i
+
k
]
>
S
S
[
j
+
k
]
SS[i+k]>SS[j+k]
SS[i+k]>SS[j+k],令
i
=
i
+
k
+
1
i=i+k+1
i=i+k+1,若此时
i
=
j
i=j
i=j,再令
i
=
i
+
1
i=i+1
i=i+1
2)若
S
S
[
i
+
k
]
<
S
S
[
j
+
k
]
SS[i+k]<SS[j+k]
SS[i+k]<SS[j+k],令
j
=
j
+
k
+
1
j=j+k+1
j=j+k+1,若此时
i
=
j
i=j
i=j,再令
j
=
j
+
1
j=j+1
j=j+1
3.若i>n或者j>n,则
B
[
m
i
n
(
i
,
j
)
]
B[min(i,j)]
B[min(i,j)]为最小表示,否则重复第二步
int n=strlen(s+1);
for(int i=1;i<=n;i++)s[n+i]=s[i];
int i=1,j=2,k;
while(i<=n&&j<=n){
for(k=0;k<n && s[i+k]==s[j+k];k++);
if(k==n)break;
if(s[i+k]>s[j+k]){
i=i+k+1;
if(i==j)i++;
}
else{
j=j+k+1;
if(j==i)j++;
}
}
ans=min(i,j);
//B[ans]是最小表示
堆
定义(以大根堆为例):
1.是一颗完美二叉树
2.树根的权值最大
3.父亲节点的权值永远大于子节点
基本操作:(以大根堆为例)
int heap[SIZE],n;
void up(int p){ //向上调整,保持堆性质,O(logn)
while(p>1){
if(heap[p]>head[p/2]){
swap(heap[p],heap[p/2]);
}
else break;
}
}
void insert(int val){//插入 O(logn)
heap[++n]=val;
up(n);
}
int gettop(){return heap[1];}//返回堆顶权值,复杂度O(1)
void down(int p){ //向下调整,O(logn)
int s=p*2;//p的左子节点
while(s<=n){
if(s<n&&heap[s]<heap[s+1])s++;//左右儿子取较大者
if(heap[s]>heap[p]){
swap(heap[s],heap[p]);
p=s,s=p*2;
}
}
}
void pop(){ //删堆顶O(logn)
heap[1]=heap[n--];
down(1);
}
void Delete(int k){ //删除节点k
heap[k]=heap[n--];
up(k),donw(k);
}
用处:1.辅助各类贪心算法 2.求解最值问题