二 数据结构
文章目录
单链表
实现一个单链表,链表初始为空,支持三种操作:
(1)向链表头插入一个数
(2)删除第k个插入的数后面的数
(3)在第k个插入的数后插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从头至尾输出整个链表
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入n个数,按照插入时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式:
第一行M,代表操作次数
- ”H x“,表示向链表头插入一个数x。
- “D k”,表示删除第k个输入的数后面的数(当k=0时,表示删除头结点)
- “I k x”,表示在第k个输入的数后面插入一个数x(此操作中k均大于0)。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
代码:
#include <iostream>
using namespace std;
const int N = 100010;
int head, e[N], ne[N],idx;
//head表示头结点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少
//idx存储当前已经用到了哪个点
//初始化
void init()
{
head = -1; //空默认下标为-1
idx = 0;
}
//将x插到头结点
void add_to_head(int x)
{
e[idx] = x,ne[idx] = head,head = idx,idx++;
}
//将x插到下标是k的点后面
void add(int k,int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx, idx++;
}
//将下标是k的点后面的点删除
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m--)
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
remove(k - 1);
//第一次插入的数下标为0, 第二次对应为1,所以第k次对应下标为k-1
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
int i;
for ( i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
双链表
实现一个双链表,初始为空,支持5钟操作
(1)在最左侧插入一个数
(2)在最右侧插入一个数
(3)将第k个插入的数删除
(4)在第k个插入的数左侧插入一个数
(5)在第k个插入的数右侧插入一个数
输入格式:
第一行包含整数M,操作次数
(1)“L x”,表示在链表最左端插入数x
(2)“R x”,表示在链表最右端插入数x
(3)“D x”,表示将第k个插入的数删除
(4)“IL x”,表示在第k个插入的数左侧插入一个数
(5)“IR x”,表示在第k个插入的数右侧插入一个数
输出整个链表
1<=M<=100000
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
代码:
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
// 0表示左端点,1表示右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在下标是k的点的右边,插入x
void add(int k, int x)
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
idx++;
}
//在下标是k的点的左边插入x,可以直接写add(l[k],x);
//下标k-1的数就是l[k],相当于在下标是k-1的点右边插入x
// 删除第k个点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main()
{
int m;
cin >> m;
init();
while (m--)
{
string op; //字符串op表示输入的操作
int k, x;
cin >> op;
if (op == "L")
{
cin >> x;
add(0, x);
}
else if (op == "R")
{
cin >> x;
add(l[1], x); //1是链表结束,l[1]是最后一个数
//add(l[1], x)表示在最后一个数后面再插入一个数
}
else if (op == "D")
{ //这里要注意,输入的k是下标
cin >> k; //但是要删除第k个数,实际上是删除下标为k+1的数,因为idx从2开始,第一个数下标为2。。。。第k个数下标为k+1
remove(k + 1); //下面IL和IR同理,第k个数的下标都是k+1
}
else if (op == "IL")
{
cin >> k >> x;
add(l[k + 1], x);
}
else if (op == "IR")
{
cin >> k >> x;
add(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) { //链表第一个数是0的右指针指向的节点
cout << e[i] << ' ';
}
cout << endl;
return 0;
}
栈和队列
栈:先进后出(桶)
队列:先进先出(没底的)
#include <iostream>
using namespace std;
const int N = 100010;
// **************** 栈
int stk[N], tt; //tt栈点
//插入
skt[ ++ tt] = x;
//弹出
tt --;
//判断栈是否为空
if (tt > 0) not empty;
else empty;
//栈顶
skt[tt];
// **************** 队列
//在队尾插入元素,在队头弹出元素
int q[N],hh,tt=-1;
//hh左 tt右
//插入
q[++ tt]=x;
//弹出
hh ++;
//判断队列是否为空
if(hh<=tt) not empty
else empty
//取出队头,队尾元素
q[hh] q[tt]
思想:
下面两道题的思想就是构造一个严格单调递增的序列
一个用栈顶比较下一个数,满足就替换,输出栈顶
一个用队尾比较下一个数,满足就替换,输出队头
单调栈(少)
返回数组中某一位数前面离它最近且比他小的数
有则输出其值
无则输出 -1
输入用例:
5
3 4 2 7 5
输出:
-1 3 -1 2 2
代码:
这里的 scanf printf 比 cin cout 快很多
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main()
{
scanf_s("%d", &n); //比cin快很多
int i;
for (i = 0; i < n; i++)
{
int x;
scanf_s("%d", &x);
while (tt && stk[tt] >= x) tt--;
if (tt) printf("%d ", stk[tt]);
else printf("-1 ");
stk[++tt] = x; //保持了栈的单调递减性质
}
return 0;
}
单调队列
例题:滑动窗口
给定一个大小为 n<=10^6 ^的数组
有一个大小为 k 的滑动窗口,从数组最左边移到最右边
每次滑动窗口向右移动一个位置。
需要确定并输出滑动窗口谓语每个位置时,窗口中的最大值和最小值。
输入:
8 3
1 3 -1 -3 5 3 6 7
输出:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
代码:
#include <iostream>
using namespace std;
const int N = 1000010;
int n,k;
int a[N], q[N];
//q数组存的是下标
int main()
{
scanf_s("%d %d", &n, &k);
int i;
for (i = 0; i < n; i++) scanf_s("%d", &a[i]);
int hh = 0, tt = -1;
for (i = 0; i < n; i++)
{
//判断队头是否已经滑出窗口
if (hh <= tt && i - k + 1 > q[hh]) hh++;
//滑动窗口只往后移动一位,所以不用while
while (hh <= tt && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
//以防滑动窗口里的数不足k
//整一个a[N]最后是单调递增,求最小值每次弹出队头
}
puts("");
hh = 0, tt = -1;
for (i = 0; i < n; i++)
{
if (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
return 0;
}
KMP字符串
给定一个模式串S,以及一个模版串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模版串P在模式串S中多次作为子串出现。
求出模版串P在模式串S中所有出现位置的起始下标。
输入格式
第一行N,表示P的长度
第二行字符串P
第三行M,表示S长度
第四行字符串S
数据范围
1<=N<=104
1<=M<=105
输入样例:
3
aba
5
ababa
输出样例:
0 2
代码:
#include <iostream>
using namespace std;
const int N = 10010, M = 100010;
int n, m;
char p[N], s[M];
int ne[N]; //next数组
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
//注意从1开始存放
int i,j;
//求next的过程
for (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;
}
//kmp匹配过程
for (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)
{
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}
Trie字符串
Trie:高效的存储和查找字符串集合的数据结构
题目:
维护一个字符串集合,支持两种操作:
- “ I x” 向集合中插入一个字符串x
- “ Q x” 询问一个字符串在集合中出现了多少次。
共有N个操作,输入字符串总长度不会超过105,字符串仅仅包含小写英文字母
对于每个询问指令 “ Q x”,都要输出一个整数作为结果
1<=N<=2*104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
代码:
#include <iostream>
using namespace std;
const int N = 100010;
char str[N];
int son[N][26], cnt[N], idx;
//son[N][26]存的是每一个单词每一次出现的字母,相当于一个分支点
//cnt[N]存放单词出现次数,cnt[p]++代表一个单词insert一次
//idx代表每一个分支点,每一个单词的每一个字母都有其的idx
int i;
void insert(char str[])
{
int p = 0;
for (i = 0; str[i]; i++) //str[i]!='\0'
{
int u = str[i] - 'a'; //a-z映射到0-25
if (!son[p][u]) son[p][u] = ++idx; //idx=0是总起点不动
p = son[p][u];
//p最后代表单词最后一个字母的位置
}
cnt[p]++;
}
int query(char str[])
{
int p = 0;
for (i = 0; str[i]; i++)
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main()
{
int n;
scanf_s("%d", &n);
while (n--)
{
char op[2];
scanf_s("%s%s", op, 2,str,N); //固定格式,不这样写报错
if (op[0] == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
并查集
- 将两个集合合并
- 询问两个元素是否在一个集合当中
近乎O(1)
基本原理:
每个集合用一棵树来表示。
树根的编号就是整个集合的编号。
每个节点存储它的父节点,p[x]表示x的父节点
问题1:如何判断树根:if(p[x]==x)
问题2:如何求x的集合编号:while(p[x]!=x) x=p[x];
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x]=y
优化—路径压缩:查到祖先之后就把px存成祖先(集合中每个元素直接指向根节点)
int find(int x) //返回x的祖宗节点+路径压缩
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作有两种:
- “ M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略此操作。
- “ Q a b”,询问编号为a和b的两个数是否在同一个集合。
1<=n,m<=105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
代码:
#include <iostream>
using namespace std;
const int N = 100010;
int i, j;
int n, m;
int p[N]; //p[x]表示x的父节点
int find(int x) //返回x的祖宗节点+路径压缩
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
for (i = 1; i <= n; i++) p[i] = i;
//刚开始一个数就是一个集合所以树根就是自己
while (m--)
{
char op[2];取
int a, b;
cin >> op >> a >> b;
if (op[0] == 'M') p[find(a)] = find(b); //把一个集合直接插入到另一个集合,然后同一个根节点
else {
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
连通块中点的数量
给定一个包含n个点(编号1~n)的无向图,初始时图中没有边
现在进行m个操作:
- ” C a b“,在点a和点b之间连一条边,a和b可能相等;
- “ Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等
- “ Q2 a”,询问点a所在连通块中点的数量
对于每个询问指令“ Q1 a b”,如果a和b在同一个连通块中,则输出“Yes”,否则“No”
对于每个询问指令“ Q2 a”,输出点a所在连通块中点的数量
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
把连接块看作集合
#include <iostream>
using namespace std;
const int N = 100010;
int i, j;
int n, m;
int p[N], num[N];
//num[N]统计集合的点的数量
int find(int x) //返回x的祖宗节点+路径压缩
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
for (i = 1; i <= n; i++)
{
p[i] = i;
num[i] = 1;
}
while (m--)
{
string op;
int a, b;
cin >> op;
if (op=="C")
{
cin >> a >> b;
if (find(a) == find(b)) continue;
num[find(b)] += num[find(a)];
//连接块变大,更新包含点的个数
p[find(a)] = find(b);
}
else if (op=="Q1")
{
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else
{
cin >> a;
printf("%d\n", num[find(a)]);
}
}
return 0;
}
堆
完全二叉树
存储:用一维数组存储
- x的左儿子:2x
- x的右儿子:2x+1
根节点(下标为1) | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|
x | x的左儿子 | x的右儿子 | x左儿子的左儿子 | x左儿子的右儿子 | x右儿子的左儿子 | x右儿子的右儿子 | … | … |
函数
down(x)、up(x)两个函数
确保从上至下是从小到大
void up(int u)
{
while (u / 2 && h[u / 2] > h[u])
{
swap(h[u / 2], h[u]);
u /= 2;
}
}
void down(int u)
{
//t是最小值下标
int t = u;
//左右儿子是否存在
if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
swap(h[u], h[t]);
down(t);
}
}
如何手写一个堆: | 具体操作 |
---|---|
1.插入一个数 | heap[++num]=x ; up(num); |
2.求集合当中最小值 | heap[1]; |
3.删除最小值 | heap[1] = heap[num]; num- -; down(1); |
4.删除任意一个元素 | heap[k] = heap[num]; num- -; down(k); up(k); |
5.修改任意一个元素 | heap[k] = x; down(k); up(k); |
堆排序
输入一个长度为n的整数数列,从小到大输出前m小的数。
1<=m<=n<=105
1<=数列中元素<=109
输出:包含m个整数,表示数列中前m个小的数
输入样例:
4 3
4 1 3 2
输出样例:
1 2 3
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int i, j;
int n, m;
int h[N],num;
void down(int u)
{
int t = u;
//左右儿子是否存在
if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
swap(h[u], h[t]);
down(t);
}
}
int main()
{
cin >> n >> m;
for (i = 1; i <= n; i++) scanf_s("%d", &h[i]);
num = n;
for (i = n / 2; i; i--) down(i); //排序 近似O(n)
//最后一个非叶子节点的索引是 n/2
while (m--)
{
printf("%d ", h[1]);
h[1] = h[num];
//在每次取出堆顶元素后都会减少,以确保下一次操作不会越界
num--;
down(1);
}
return 0;
}
模拟堆
维护一个集合,初始时集合为空
- “ I x ”,插入一个数x
- “PM”,输出当前集合最小值
- “DM”,删除当前集合中的最小值
- “D K”,删除第k个插入的数
- “C k x”,修改第k个插入的数,将其变为x
对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值
输入样例:
10
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int i, j;
int n, m;
int h[N], ph[N], hp[N], num;
//h[]存的是元素
//ph[i] 表示第 i 个插入的数在堆中的下标。
//hp[j] 表示堆中下标为 j 的元素是第几个插入的数。
//num是堆的大小
void heap_swap(int a, int b)
{
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void up(int u)
{
while (u / 2 && h[u / 2] > h[u])
{
heap_swap(u / 2, u);
u /= 2;
}
}
void down(int u)
{
int t = u;
if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2; //如果左儿子存在且小于就交换
if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
int main()
{
cin >> n;
while (n--)
{
string op;
int k, x;
cin >> op;
if (op=="I")
{
cin >> x;
num++;
m++;
ph[m] = num, hp[num] = m;
//num是整个堆的元素数量,m是插入数个数,映射关系
h[num] = x;
up(num);
}
else if (op == "PM") printf("%d\n", h[1]);
else if (op == "DM")
{
heap_swap(1, num);
num--;
down(1);
}
else if (op == "D")
{
cin >> k;
k = ph[k];
heap_swap(k, num);
num--;
down(k), up(k);
}
else
{
cin >> k >> x;
k = ph[k]; //将k更新为该数位于堆的下标
h[k] = x;
down(k), up(k);
}
}
return 0;
}
哈希表
通过将键(key)映射到表中的某个位置来实现高效的数据检索。
数据范围-109~109 ,映射0到105~106
- **哈希函数(Hash Function):**它接受一个键作为输入,并输出一个固定长度的哈希值。
- 哈希表存储结构: 哈希表通常是一个数组,每个数组元素称为桶(Bucket)。每个桶可以存储一个键值对,或者采用链表、树等数据结构存储多个键值对,以处理哈希冲突。
- 哈希冲突(Hash Collision): 当两个不同的键经过哈希函数映射到相同的索引时,就发生了哈希冲突。解决冲突的常见方法有:
- 链地址法(Separate Chaining):每个桶使用一个链表来存储具有相同哈希值的键值对。
- 开放寻址法(Open Addressing):在发生冲突时,尝试在其他桶中寻找空位
- 哈希表提供了快速的平均时间复杂度(O(1))用于查找、插入和删除操作。
删除:添加一个bool数组,标记一下
存储结构
题目:模拟散列表
维护一个集合
- “ I x“,插入一个数
- “Q x”,查询一个数是否在集合中出现过
数据范围x:-109~109
1<=N<=105
输入范例:
5
I 1
I 2
I 3
Q 2
Q 5
输出范例:
Yes
No
开放寻址法
一维数组,范围最好是规定数据范围的2~3倍的素数
#include <iostream>
#include <cstring>
//提供了一系列处理字符串和内存操作的函数声明
//memset函数需要<cstring>
using namespace std;
const int N = 200003,null=0x3f3f3f3f;
//开一维数组,范围最好是规定数据范围的2~3倍的素数
//0x3f3f3f3f远大于1e5,不存任何数
int n, m;
int i, j;
int h[N];
int find(int x)
{
int k = (x % N + N) % N;
//%N:首先,x 被 N 取模,这将确保 x 的值在取模后范围在 -N+1 到 N-1 之间,包括负数和正数。
//+N:然后,取模后的结果被加上 N,这样就确保结果始终为正数。
//%N:最后,再次对结果进行 N 取模,这将确保结果在 0 到 N-1 的闭区间内。
while (h[k] != null && h[k] != x) //坑上有数往后看
{
k++;
if (k == N) k = 0; //最后一个坑位看完了,循环看第一个坑位
}
return k;
}
//在插入操作中,若x在哈希表中,k就是下标
//若不在,就是k应该存储的位置
int main()
{
cin >> n;
memset(h, 0x3f, sizeof h);
//初始化,memset是按照字节,0x3f就代表0x3f3f3f3f
//memset函数用于设置一块内存区域的值,一般中间写-1或者0
while (n--)
{
string op;
int x;
cin >> op >> x;
int k = find(x);
if (op == "I") h[k] = x;
else
{
if (h[k]!=null) puts("Yes");
else puts("No");
}
}
return 0;
}
拉链法
一维数组,数组每个元素都单开一个单链表
#include <iostream>
#include <cstring> //提供了一系列处理字符串和内存操作的函数声明
using namespace std;
const int N = 100003;
//哈希表数组范围取大于数据范围且最接近的质数,确保冲突最小
int n, m;
int i, j;
int h[N], e[N], ne[N], idx;
void insert(int x)
{
int k = (x % N + N) % N; //使余数是正数
//可能存的数是负数,那x % N就是负数
// 但映射到0-1e5,所以要(x % N + N) % N
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x)
{
int k = (x % N + N) % N;
for (i = h[k]; i != -1; i = ne[i])
if (e[i] == x) return true;
return false;
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
//初始化,单链表指向-1,数组所有的值都初始为-1
//memset函数用于设置一块内存区域的值,一般中间写-1或者0
while (n--)
{
string op;
int x;
cin >> op >> x;
if (op == "I") insert(x);
else
{
if (find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
字符串哈希
思想:
- 把字符串str每个位置前缀的哈希值存到数组里
- 用p进制存,p 存储了 P 的幂次
题目:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,
请你判断 [ l1 , r1 ], [ l2 , r2 ]两个区间的字符串子串是否完全相同。
相同输出 Yes,否则No
输入样例:
8 3
aabbaab
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
#include <iostream>
using namespace std;
const int N = 100010,P=131;
//P取131或13331 大概率不发生冲突
typedef unsigned long long ULL;
int i, j;
int n, m;
char str[N];
ULL h[N], p[N];
//h存储了每个位置的前缀哈希值,而 p 存储了 P 的幂次
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
cin >> n >> m >> str + 1;
p[0] = 1;
for (i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i];
}
while (m--)
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
std
vector,变长数组,倍增的思想
string,字符串,substr( ) , c-str( )
quene,队列,push(),front(),pop()
priority_quene,优先队列,push(),top(),pop()
stack,栈,push(),top(),pop()
deque,双端队列
set,map,multiset,multimap,基于平衡二叉树(红黑树),动态维护有序序列
unordered_set,unordered_map,unordered_multiset,unordered_multimap,哈希表
bitset,压位
思想:
- 把字符串str每个位置前缀的哈希值存到数组里
- 用p进制存,p 存储了 P 的幂次
[外链图片转存中…(img-HtGIDHvC-1710417460150)]
[外链图片转存中…(img-ukNEZsB6-1710417460150)]
题目:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,
请你判断 [ l1 , r1 ], [ l2 , r2 ]两个区间的字符串子串是否完全相同。
相同输出 Yes,否则No
输入样例:
8 3
aabbaab
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
#include <iostream>
using namespace std;
const int N = 100010,P=131;
//P取131或13331 大概率不发生冲突
typedef unsigned long long ULL;
int i, j;
int n, m;
char str[N];
ULL h[N], p[N];
//h存储了每个位置的前缀哈希值,而 p 存储了 P 的幂次
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
cin >> n >> m >> str + 1;
p[0] = 1;
for (i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i];
}
while (m--)
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
std
vector,变长数组,倍增的思想
string,字符串,substr( ) , c-str( )
quene,队列,push(),front(),pop()
priority_quene,优先队列,push(),top(),pop()
stack,栈,push(),top(),pop()
deque,双端队列
set,map,multiset,multimap,基于平衡二叉树(红黑树),动态维护有序序列
unordered_set,unordered_map,unordered_multiset,unordered_multimap,哈希表
bitset,压位