目录
1.Trie字符串统计(快速存储单词和查找单词数量以及是否存在)
一.单链表
单链表一共由四部分组成:
1.head:表头
2.e[i]:节点权值
3.ne[i]:下标为i的节点的下一个节点的下标
4.idx:节点的下标,并且表示一共开创了多少个节点
链表可以实现以下几个操作:
1.向链表表头插入一个数x:只要让x的下一个节点下标指向表头,同时更新表头节点下标即可
void add_head(int x){
e[idx]=x,ne[idx]=head,head=idx++;
}
2.在节点下标为k的节点后面插入的一个数x:
只需要让x的ne[]指向节点下标为k的节点后一个节点下标,同时让下标为k的节点指向的下一个节点的下标指向x的节点下标即可
void add(int k,int x){
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
3.删除节点下标为k的节点的后一个节点:
只需要让节点下标为k的节点的ne[]跳过后一个节点指向下下个节点下标即可
void remove__(int k){
ne[k]=ne[ne[k]];
}
二.栈
1.栈(stack)的操作:
栈是先进后出的容器
1.栈的定义:
stack<int> stk
2.stk.push(x):插入x
stk.pop():弹出栈顶元素
stk.top():查询栈顶元素
stk.size():栈中元素的个数
stk.empty():查询栈是否为空(栈为空返回true,栈不为空返回false)
2.栈的应用:表达式求值
题目链接:活动 - AcWing
小技巧:
1.用unordered_map存储符号的优先级
unordered_map<char,int> h={{'+',1},{'-',1},{'*',2},{'/',2}};
2.isdigit(c)判断字符c是否是数字
题解:
1.计算表达式,需要考虑符号的优先级,先计算括号里面的,之后再计算优先级高的
2.因此需要建两个栈,一个符号栈,一个数字栈
3.如果符号栈顶的符号优先级小于即将入栈的符号的优先级,则先不计算,让优先级高的符号入栈即可,如果符号栈栈顶符号优先级大于等于即将入栈的符号的优先级,则需要先将这个优先级大的符号计算,直到栈为空或者栈顶符号的优先级小于即将入栈的符号的优先级时,就将这个符号入栈
4.如果碰到左括号,直接使之入栈即可,碰到右括号,就向做一直计算(因为已经保证了栈内左面的优先级一定小于右边的优先级),直到碰到左括号时,就结束运算,再次按照顺序遍历
5.最后不要忘记将栈内存留的符号一定要清理(计算)完;
代码模板如下:
#include<bits/stdc++.h>
using namespace std;
stack<int> num;
stack<char> op;
void eval(){
int b=num.top();
num.pop();
int a=num.top();
num.pop();
char o=op.top();
op.pop();
int r=0;
if(o=='+') r=a+b;
else if(o=='-') r=a-b;
else if(o=='*') r=a*b;
else if(o=='/') r=a/b;
num.push(r);
}
int main(){
unordered_map<char,int> h={{'+',1},{'-',1},{'*',2},{'/',2}};
string str;
cin>>str;
for(int i=0;i<str.size();i++){
auto c=str[i];
if(isdigit(c)){
int x=0,j=i;
while((j<str.size())&&isdigit(str[j])){
x=x*10+str[j]-'0';
j++;
}
num.push(x);
i=j-1;
}
else if(str[i]=='(') op.push(str[i]);
else if(str[i]==')'){
while(op.top()!='('){
eval();
}
op.pop();
}
else {
while(op.size()&&h[op.top()]>=h[str[i]]){
eval();
}
op.push(str[i]);
}
}
while(op.size()) eval();
cout<<num.top()<<endl;
}
三.队列
队列是先进先出(队尾进,队头出)的容器:
1.队列的定义:
deque<int> q;
2.队列的操作:
q.front():查询队头元素
q.back():查询队尾元素
q.pop():弹出队头元素
q.push():向队尾压入元素
q.size():返回队列的大小
q.empty():查询队列是否为空
四.单调栈:
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
单调栈原理:
(1)如果第i个数比左边的数大,则在后边这个第i个数可能为最小值,因此需要将他入栈,但是如果第i个数比左边的数小,则这个数左边的数字在后面一定用不到,那么就可以将这个数弹出,直到栈为空或者小于第i个数为止,这样操作之后栈里的元素就变成了单调递增的数
代码如下:
stack<int> stk;
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
int x;
cin>>x;
while(stk.size()&&stk.top()>=x) stk.pop();
if(stk.size()) cout<<stk.top()<<" ";
else cout<<-1<<" ";
stk.push(x);
}
}
五.单调队列
题目链接:154. 滑动窗口 - AcWing题库
单调队列思路:
1.如果输出最小值:在范围内如果左边的数比右边的数大,则左边的数一定没有用处,因此可以弹出,如果左边的数比右边的数小,则需要压入右边的数,因为右边的数可能会成为后边的最小值
2.如果输出最大值时,与上述思路相反即可
求最小值代码如下:
for(int i=0;i<n;i++){
if(q1.size()&&q1.front()<i-k+1) q1.pop_front();
while(q1.size()&&a[q1.back()]>=a[i]) q1.pop_back();
q1.push_back(i);
if(q1.back()>=k-1) cout<<a[q1.front()]<<" ";
}
求最大值代码如下:
for(int i=0;i<n;i++){
if(q2.size()&&q2.front()<i-k+1) q2.pop_front();
while(q2.size()&&a[q2.back()]<=a[i]) q2.pop_back();
q2.push_back(i);
if(q2.back()>=k-1) cout<<a[q2.front()]<<" ";
}
六.KMP字符串
kmp题目链接:活动 - AcWing
kmp最透彻链接推荐:AcWing 831. KMP字符串 - AcWing
kmp主要由两个函数构成,求ne[]数组的函数和求相同字符串的函数
ne[i]数组就是i之前前缀与后缀相同的最大长度
分成函数的KMP算法写法
nex函数:(自己和自己匹配就从2开始)
1.不退到头并且不匹配就退
2.匹配就j++
3.更新ne[i]=j
kmp函数:(为了凑出s[i]!=p[j+1],i从1开始)
1.不退到头并且不匹配就退
2.匹配就j++
3.匹配完成再退重开
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e6+10;
char p[N],s[M];
int n,m,ne[N];
void nex(){
for(int i=2,j=0;i<=n;i++){
while(j&&p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
}
void kmp(){
for(int i=1,j=0;i<=m;i++){
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n){
cout<<i-n<<" ";
j=ne[j];
}
}
}
int main(){
cin>>n>>p+1>>m>>s+1;
nex();
kmp();
}
七.Trie(字典)树
字典树作用:
高效的存储和查找字符串集合,主要通过利用字符串的公共前缀来减少查询时间
字典树的三个基本性质(来自百度百科)
1.根节点不包含字符,除根节点外每一个节点都只包含一个字符;
2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
3.每个节点的所有子节点包含的字符都不相同。
1.Trie字符串统计(快速存储单词和查找单词数量以及是否存在)
题目链接:活动 - AcWing
son[p][i]代表一个节点的多个儿子节点
p代表某个节点,i代表这个节点的第i个儿子节点
一共包括插入和查询两个操作:
插入操作代码如下:
int insert(char str[]){
int p=0; //根节点
for(int i=0;str[i];i++){
int u=str[i]-'a'; //将字母转化成数字0--25记录
if(!son[p][u]) son[p][u]=++idx; //如果没有这个节点,就新建一个,节点数量idx+1
p=son[p][u]; //将指针更新到这个新的节点
}
cnt[p]++; //单词最后一个字母次数+1
}
查询单词次数代码如下:
int query(char str[]){
int p=0;
for(int i=0;str[i];i++){
int u=str[i]-'a';
if(!son[p][u]) return 0; //如果没有开创这个子节点,就说明没有这个单词
p=son[p][u];
}
return cnt[p]; //返回单词最后一个字母的数量,即单词数量
}
2.Trie也可以用来记录二进制树
将十进制数字转化为二进制数字并插入字典树代码:
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=x>>i&1;
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
}
查询与x异或最大的二进制数并转化为十进制数:
int query(int x){
int p=0,res=0;
for(int i=30;i>=0;i--){
int u=x>>i&1;
if(son[p][!u])
{
res=(res<<1)+!u;
p=son[p][!u];
}
else
{
res=(res<<1)+u;
p=son[p][u];
}
}
return res;
}
八.并查集(重点!!!)
1.合并集合
题目链接:活动 - AcWing
并查集主要是通过祖宗节点将一个集合里的点联系起来,并且通过祖宗节点来插入,合并集合,通过查询是否是同一个祖宗节点来判断是否在一个集合
find(x)是查询x所在集合的祖宗节点,通过递归来找祖宗节点
并查集核心操作:
int find(int x){
if(p[x]!=x) p[x]=find(p[x]); //查找祖宗节点+路径压缩
return p[x];
}
路径压缩就是找到这个节点后,将这个节点直接连接到祖宗节点上,也就是让节点父节点直接变成祖宗节点,以后再找的时候就不用一个一个遍历。
2.计算连通块中点的数量(并查集模板的应用)
题目链接:活动 - AcWing
3.食物链!!!(并查集额外信息的维护,带权值的并查集)
本题最重要的是要维护节点到祖宗节点的距离,并且在路径压缩的时候,在将节点直接与祖宗节点连接后,还要记录这个节点到祖宗节点的距离。
设d[x]为x到其父节点的距离,因此路径压缩时,要在x节点与祖宗节点连接后也要改变同时改变更新d[x]的值为x到祖宗节点(也就是路径压缩后的父节点)的距离。
利用递归更新d[x];
核心代码:
int find(int x){
if(p[x]!=x){
//先递归到祖宗节点再将路径距离相加
//如果先加再递归到祖宗节点,
//只能将父亲节点与父亲节点的父亲节点之间的距离加起来,
//而不能将父亲节点到祖宗节点的距离加起来,所有会计算错误
int t=find(p[x]);
d[x]+=d[p[x]]; //由于上面先递归到了祖宗节点,因此这里会从祖宗节点的距离开始累加
p[x]=t; //将父亲节点更新为祖宗节点,即路径压缩
}
return p[x]; //返回祖宗节点
}
九.哈希表
1.模拟散列表(用开放寻址法)(不需要排序的离散化)(减小范围)
题目连接:活动 - AcWing
建h[]数组,初始化为无穷,每次将x映射到下标为t的位置上面,如果h数组中有x,返回的t则是x的下标,如果x不存在,返回的则是应该将x存在的位置的下标。
find(x)函数返回下标
find()函数代码:
(N是大于h数组容量的质数)h数组需要初始化为无穷大
每次从t开始找,如果找到结尾还没找到就再从头开始找。
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
2.字符串哈希
题目链接:活动 - AcWing
思路就是将字符串转化为p进制的数,然后通过取模2的64次方来离散化为范围内的数,之后利用前缀和来计算某一区间的哈希值,通过比较两个区间的哈希值即可判断是否相等
小技巧:
1.用unsigned long long 来存储哈希值,溢出后就相当于对2的64次方取模,用p数组先预处理出p的多少次方
2.P需要取131或者13331,因为这时所引起冲突概率最小
对p数组预处理以及求哈希值前缀和h数组代码如下:
p[0]=1; //p[0]=1,便于求次方
for(int i=1;i<=n;i++){
p[i]=p[i-1]*P;
//求前i个字符串哈希值,不要忘记加上str[i],因为此时str[i]权值为0,因此不用乘以p
h[i]=h[i-1]*P+str[i];
}
求l--r区间的哈希值大小代码:
ULL get(int l,int r){
return h[r]-h[l-1]*p[r-l+1]; //l--r的哈希值
}
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,P=131; //P取131或者13331冲突概率最小
typedef unsigned long long ULL; //溢出后就相当于对2的64次方取模
char str[N];
ULL h[N],p[N]; //p[i]存的是P的i次方,h[i]存的是前i个字符串的哈希值
ULL get(int l,int r){
return h[r]-h[l-1]*p[r-l+1]; //l--r的哈希值
}
int main(){
int n,m;
cin>>n>>m>>(str+1);
p[0]=1; //p[0]=1,便于求次方
for(int i=1;i<=n;i++){
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+str[i]; //求前i个字符串哈希值,不要忘记加上str[i],因为此时str[i]权值为0,因此不用×p
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(get(l1,r1)==get(l2,r2)) cout<<"Yes\n";
else cout<<"No\n";
}
}