2. 数据结构(I)
2.1 链表
2.1.1 单链表
题目:
实现一个单链表,实现以下
H x
向链表头插入一个数 ;D x
删除第 个插入的数(若 ,表示删除头结点);I k x
在第 个插入的数后插入一个数 (保证 )。
给你
思路:
我们定义,

如图

如图 H x
的方式。将

如图 D x
的方式,即将
删除头结点也可以用类似的方法。

如图 I k x
的方式。即先将
最后输出时,从头结点开始遍历,直到
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int m;
int e[N], ne[N], head, idx;
void add_head(int x) { //头插入(图2.2)
e[idx] = x, ne[idx] = head, head = idx, idx ++;
}
void add(int k, int x) { //中间插入(图2.4)
e[idx] = x, ne[idx] = ne[k], ne[k] = idx, idx ++;
}
void del_head() { //删除头结点
head = ne[head];
}
void del(int k) { //删除中间节点(图2.3)
ne[k] = ne[ne[k]];
}
int main() {
scanf("%d", &m);
head = -1, idx = 0; //初始化
while (m -- ) {
char op;
cin >> op;
if (op == 'H') { //头插入
int x;
scanf("%d", &x);
add_head(x);
} else if (op == 'D') { //删除节点
int x;
scanf("%d", &x);
if (x == 0) del_head();
else del(x-1); //注意删掉的是x-1而非x
} else { //中间插入
int k, x;
scanf("%d%d", &k, &x);
add(k-1, x);
}
}
for (int i = head; i != -1; i = ne[i]) printf("%d ", e[i]);
return 0;
}
2.1.2 双链表
题目:
实现一个双链表,实现以下
L x
向链表头插入一个数 ;R x
向链表尾插入一个数 ;D x
删除第 个插入的数;IL k x
在第 个插入的数左侧插入一个数 ;IR k x
在第 个插入的数右侧插入一个数 。
给你
思路:
我们定义,
仿照单链表,考虑插入操作 IR k x
:
首先将第
插入操作 IL k x
也可以视作在 IR l[k] x
. L x
可以视作在 IR 0 x
;同理 R x
可视作在 IR l[1] x
.
最后考虑删除操作 D x
. 显然
最后从
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int m;
int e[N], l[N], r[N], idx;
//e数组存储每个节点的值,l数组存储每个节点左侧节点的编号,r节点存储每个节点右侧节点的编号
void add(int k, int x) {
e[idx] = x; //将idx号点的值设为x
l[idx] = k, r[idx] = r[k]; //更新idx的左右节点
l[r[k]] = idx, r[k] = idx; //更新idx的左节点的右节点和右节点的左节点
idx ++;
}
void del(int k) {
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main() {
scanf("%d", &m);
r[0] = 1, l[1] = 0, idx = 2; //0表示头结点,1表示尾结点
while (m -- ) {
string op;
cin >> op;
if (op == "L") {
int x;
scanf("%d", &x);
add(0, x); //在头节点插入
} else if (op == "R") {
int x;
scanf("%d", &x);
add(l[1], x); //在尾结点插入
} else if (op == "D") {
int x;
scanf("%d", &x);
del(x+1); //删除第x个插入的数
} else if (op == "IR") {
int k, x;
scanf("%d%d", &k, &x);
add(k+1, x); //在第k个插入的数右侧插入x
} else {
int k, x;
scanf("%d%d", &k, &x);
add(l[k+1], x); //在第k个插入的数左侧插入x,相当于在l[k]右侧插入x
}
}
for (int i = r[0]; i != 1; i = r[i]) printf("%d ", e[i]);
return 0;
}
2.2 栈
2.2.1 模拟栈
题目:
实现一个栈,支持以下
push x
向栈顶插入一个数 ;pop
从栈顶弹出一个元素;empty
判断栈是否为空;query
查询栈顶元素。
给你 empty
或 query
操作,输出对应结果。
思路:
栈是一种先进后出的数据结构。
我们可以维护一个头指针
对于每次 push
操作,将
对于每次 pop
操作,将
对于每次 empty
操作,若
对于每次 query
操作,输出
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 100010;
int m;
int tt = 1; //尾指针
int q[N]; //栈
int main() {
scanf("%d", &m);
while (m -- ) {
string op;
cin >> op;
if (op == "push") {
int x;
scanf("%d", &x);
q[tt ++] = x; //向栈顶插入元素
}
else if (op == "pop") tt --; //弹出栈顶元素
else if (op == "empty") { //判断栈是否为空
if (tt == 1) puts("YES");
else puts("NO");
}
else printf("%d\n", q[tt-1]); //输出栈顶元素
}
return 0;
}
题目:给你一个表达式,其中的符号只包含 +
,-
,*
,/
和左右括号,计算它的值。表达式的长度不超过
思路:
开两个栈,分别存储运算数和运算符。然后扫描整个表达式:
- 若当前字符为数字,则提取出整个数,加入运算数栈中。
- 若当前字符为
(
,直接加入运算符栈中。 - 若当前字符为
)
,计算括号内表达式的值,将其结果加入运算数栈中。 - 若当前字符为四则运算符,将运算符栈顶的运算符与当前字符比较,若当前字符的优先级较高,则直接加入运算符栈中;否则计算之前表达式的值,再将当前字符入栈。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <unordered_map>
using namespace std;
stack<int> nums;
stack<char> op;
void eval() { //计算中缀表达式的值
int b = nums.top(); nums.pop();
char oper = op.top(); op.pop();
int a = nums.top(); nums.pop();
int c;
if (oper == '+') c = a + b;
else if (oper == '-') c = a - b;
else if (oper == '*') c = a * b;
else c = a / b;
nums.push(c);
}
int main() {
string s;
cin >> s;
unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; //每个符号的运算优先级
for (int i = 0; i < s.size(); ++i) {
char c = s[i];
if (isdigit(c)) {
int x = 0; //提取出操作数x
while (isdigit(s[i]) && i < s.size()) {
x = x * 10 + s[i] - '0';
i ++;
}
i --;
nums.push(x);
}
else if (c == '(') op.push(c);
else if (c == ')') { //如果是右括号,从右向左计算出整个括号的值
while (op.top() != '(' && op.size()) eval();
op.pop();
}
else { //如果是运算符,那么按运算顺序计算
while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
op.push(c);
}
}
while (op.size()) eval(); //求出最终表达式的值
cout << nums.top() << endl;
return 0;
}
2.2.2 单调栈
题目:给你一个包含
思路:
首先,我们知道一个结论:若
结合上述结论,我们可以维护一个单调栈
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
using namespace std;
int n;
stack<int> nums; //单调栈
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
if (!nums.size()) printf("-1 "); //栈为空
else {
while (nums.size() && nums.top() >= x) nums.pop(); //找到第一个<x的元素
if (!nums.size()) printf("-1 ");
else printf("%d ", nums.top());
}
nums.push(x); //将x加入栈
}
return 0;
}
2.3 队列
2.3.1 模拟队列
题目:
实现一个队列,支持以下
push x
向队尾插入一个数 ;pop
从队头弹出一个元素;empty
判断队列是否为空;query
查询队头元素。
给你 empty
或 query
操作,输出对应结果。
思路:
维护一个数组
对于 push x
操作,令 pop
操作,empty
操作,若 query
操作,输出
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int m;
int hh, tt; //hh指向队头,tt指向队尾
int q[N]; //模拟队列
int main() {
scanf("%d", &m);
while (m -- ) {
string op;
cin >> op;
if (op == "push") {
int x;
scanf("%d", &x);
q[tt ++] = x; //队尾插入
}
else if (op == "pop") hh ++; //弹出队头
else if (op == "empty") { //判断是否为空
if (hh == tt) puts("YES");
else puts("NO");
}
else printf("%d\n", q[hh]); //询问队头
}
return 0;
}
2.3.2 单调队列
题目:给定一个长度为
思路:
维护一个单调队列
- 若队头
,说明此时队头已超出窗口范围,弹出队头; - 弹出队尾,直到
; - 将
加入队列; - 输出
,即滑动窗口内的最小值。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e6+10;
int n, k, hh, tt;
int a[N], q[N]; //q存储的是原数组下标
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
hh = 0, tt = -1; //初始化队头队尾
for (int i = 1; i <= n; ++i) {
if (hh <= tt && q[hh] <= i-k) hh ++; //如果队头超出了滑动窗口的范围则将其弹出
while (hh <= tt && a[q[tt]] >= a[i]) tt --; //弹出队尾直到ai能够加入队列
q[++ tt] = i; //将t加入队列
if (i >= k) printf("%d ", a[q[hh]]); //输出队头(最小值)
}
puts("");
hh = 0, tt = -1; //与求最小值基本一致
for (int i = 1; i <= n; ++i) {
if (hh <= tt && q[hh] <= i-k) hh ++;
while (hh <= tt && a[q[tt]] <= a[i]) tt --;
q[++ tt] = i;
if (i >= k) printf("%d ", a[q[hh]]);
}
return 0;
}`
2.4 KMP 字符串算法
题目:给定一个长度为
思路:
约定:本题中字符串的下标均从
首先定义
然后我们来思考如何求出

如图
得到

如图
时间复杂度
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10, M = 1e6+10;
int n, m;
char p[N], s[M]; //模式串和匹配串
int ne[N]; //ne[i]存储p中第i个字符的最长公共前后缀长度
int main() {
cin >> n >> p+1 >> m >> s+1;
ne[1] = 0;
for (int i = 2, j = 0; i <= n; ++i) { //求出ne数组
while (j && p[i] != p[j+1]) j = ne[j];
if (p[i] == p[j+1]) j ++;
ne[i] = j;
}
for (int i = 1, j = 0; i <= m; ++i) { //kmp 字符串匹配
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;
}
2.5 Trie 树
2.5.1 字典 Trie
题目:
维护一个字符串集合,支持两种操作:
I s
插入一个字符串 ;Q s
询问字符串 出现的次数。
给你
思路:
维护一棵
如果现在有

其中,
对于每次插入操作,从根节点开始遍历,若某个节点不存在则将其加入
时间复杂度
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n;
int son[N][26], cnt[N], idx; //son存储节点编号,cnt存储每个节点的数量
//0号节点既是根节点,也是空节点
void insert(string s) { //插入一个字符串s
int p = 0; //从根节点开始遍历
for (int i = 0; i < s.size(); ++i) {
int u = s[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx; //如果该节点不存在,则插入新节点
p = son[p][u];
}
cnt[p] ++;
}
int query(string s) { //查询s出现的次数
int p = 0;
for (int i = 0; i < s.size(); ++i) {
int u = s[i] - 'a';
if (!son[p][u]) return 0; //如果该节点不存在,直接返回0
p = son[p][u];
}
return cnt[p];
}
int main() {
scanf("%d", &n);
while (n -- ) {
char op; string s;
cin >> op >> s;
if (op == 'I') insert(s);
else cout << query(s) << endl;
}
return 0;
}
2.5.2 01 Trie
题目:给你
思路;
异或运算
例如,对于数组

以数

如图

如图
通过
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10, M = 3e6+10;
int n, a[N];
int son[M][2], idx; //son存储子节点
void insert(int x) { //将一个数x插入01Trie
int p = 0;
for (int i = 30; i >= 0; --i) {
int s = x >> i & 1;
if (!son[p][s]) son[p][s] = ++ idx; //如果子节点不存在,则创建新节点
p = son[p][s];
}
}
int query(int x) { //查询x的最大异或结果
int p = 0, res = 0;
for (int i = 30; i >= 0; --i) {
int s = x >> i & 1; //存储x的二进制表示的倒数第i+1位的数
if (son[p][!s]) res += 1 << i, p = son[p][!s];
else p = son[p][s];
}
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
insert(a[i]);
}
int maxl = 0;
for (int i = 1; i <= n; ++i) maxl = max(maxl, query(a[i]));
printf("%d\n", maxl);
return 0;
}
2.6 并查集
题目:
初始时一共有
M a b
表示将数 所在的集合和数 所在的集合合并;Q a b
表示询问数 和数 是否在一个集合中。
思路:
并查集的思想是将每个集合视作一棵树,那么所有集合就可以视作一个森林。定义一个数组
对于 M
操作,我们可以令 Q
操作,若
在并查集的
时间复杂度
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N];
int find(int x) { //查找x的祖先
if (p[x] != x) p[x] = find(p[x]); //路径压缩
return p[x];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) p[i] = i;
char op; int a, b;
while (m -- ) {
cin >> op >> a >> b;
if (op == 'M') p[find(a)] = find(b);
else (find(a) == find(b)) ? puts("Yes") : puts("No");
}
return 0;
}
题目:
初始时,给你一个包含
C a b
表示在 和 之间连一条无向边;Q1 a b
表示询问 和 是否位于一个连通块内;Q2 a
表示询问 所在的连通块的大小。
思路:
除了维护一个
合并时,若
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N], s[N]; //s存储每个集合内的连通块数量(只在下标为祖宗节点时有意义)
int find(int x) { //并查集
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) p[i] = i, s[i] = 1;
while (m -- ) {
string op;
cin >> op;
if (op == "C") { //合并集合
int a, b;
cin >> a >> b;
if (find(a) != find(b)) s[find(b)] += s[find(a)], p[find(a)] = find(b); //要先加上数量再合并
//注意,只需要当a,b在不同联通块时才能进行合并,否则会导致一个连通块内点的数量翻倍
} else if (op == "Q1") { //查询是否位于同一个连通块内
int a, b;
cin >> a >> b;
(find(a) == find(b)) ? puts("Yes") : puts("No");
} else { //查询连通块内点的数量
int a;
cin >> a;
printf("%d\n", s[find(a)]);
}
}
return 0;
}
题目:
有
有人用
1 X Y
表示 和 是同类。2 X Y
表示 吃 。
此人对
当一句话满足下列三个条件之一时,这句话就是假话,否则就是真话:
- 当前的话与前面的某些真话冲突,就是假话;
- 当前的话中
或 ,就是假话; - 当前的话表示
吃 ,就是假话。
根据给定的
思路:
我们可以使用“带边权并查集”,令
那么我们可以根据
对于 1 X Y
(
对于 2 X Y
(
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 50010;
int n, k, cnt;
int p[N], d[N]; //带边权并查集
int find(int x) {
if (p[x] != x) {
int tmp = find(p[x]); //递归更新p[x]到根节点的距离(路径压缩后,p[x]的父亲节点即为其祖宗节点)
d[x] += d[p[x]]; //x到根节点(路径压缩后的根节点)的距离=x到p[x]的距离(即d[x])+p[x]到其父节点(即根节点)的距离
p[x] = tmp; //路径压缩
}
return p[x];
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) p[i] = i; //初始时,每个点的父亲是自己,到自己的距离为0
int op, x, y;
while (k -- ) {
scanf("%d%d%d", &op, &x, &y);
if (x > n || y > n) {cnt ++; continue;} //x>n,y>n是假话
int px = find(x), py = find(y);
if (op == 1) {
if (px == py) { //在同一棵树上
//(d[x]-d[y]) % 3 = 0说明是真话,否则是假话
if ((d[x]-d[y]) % 3)
cnt ++;
} else { //不在同一棵树上,说明x,y之前没有联系,视作真话
d[px] = d[y] - d[x];
p[px] = py;
}
} else {
if (px == py) {
if ((d[x]-d[y]+1) % 3)
cnt ++;
} else {
d[px] = d[y] - d[x] - 1;
p[px] = py;
}
}
}
printf("%d\n", cnt);
return 0;
}
2.7 堆
题目:
给你一个长度为
思路:
堆可以看作是一个完全二叉树,所以我们可以用存储二叉树的方式来存储它,若下标为
下图(

堆有两个个关键的操作——down
操作和 up
操作,时间复杂度均为 down(u)
意味着将 up(u)
意味着将 down
操作。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n, m;
int a[N], s;
void down(int u) { //时间复杂度O(log n)
int t = u;
//u*2和u*2+1分别是u的左儿子和右儿子
if (u*2 <= s && a[u*2] < a[t])
t = u*2;
if (u*2+1 <= s && a[u*2+1] < a[t])
t = u*2+1; //若a[u]>min(a[2u],a[2u+1]),则在左儿子和右儿子中取较小值与a[u]交换
if (t != u) {
swap(a[t], a[u]);
down(t);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
s = n;
for (int i = n/2; i > 0; --i) down(i);
//从n/2开始倒序建堆,是因为n/2+1,...,n均为叶子结点,不需要down操作
for (int i = 1; i <= m; ++i) {
printf("%d ", a[1]);
a[1] = a[s], s --; //每次取出堆头后将堆头删去,用最后一个元素替代它
down(1); //下传堆头元素
}
return 0;
}
题目:
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 ;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 个插入的数;C k x
,修改第 个插入的数,将其变为 ;
现在要进行 PM
操作,输出当前集合的最小值。
思路:
本题中的 down
和 up
操作来实现。下面为 up
操作的实现方式:
void up(int u) {
while (u / 2 > 0 && h[u] < h[u/2]) {
//如果u存在父节点且u节点上的值比其父节点上的值要小.则交换这两个点
swap(u, u/2);
u /= 2;
}
}
本题的难点在于如何实现后两个操作。我们可以仿照链表的存储方式,用变量
由于增加了数组 heap_swap
来处理堆中两个编号为
void heap_swap(int a, int b) { //将堆中编号为a,b的两个点互换
swap(ph[hp[a]], ph[hp[b]]); //交换节点编号
swap(hp[a], hp[b]); //交换插入编号
swap(h[a], h[b]);
}
down
操作和 up
操作中的 swap
也需要替换成 heap_swap
.
void down(int u) {
int t = u;
if (u*2 <= s && h[u*2] < h[t]) t = u*2;
if (u*2+1 <= s && h[u*2+1] < h[t]) t = u*2+1;
if (t != u) {
heap_swap(t, u);
down(t);
}
}
void up(int u) {
while (u / 2 > 0 && h[u] < h[u/2]) {
heap_swap(u, u/2);
u /= 2;
}
}
有了这三个核心操作,我们就可以完成题目所给的要求了。
-
I x
:插入一个数 。 堆的大小 和插入序号 都需要增加 ;此时 。 然后再将 号节点up
操作即可。 -
PM
:输出堆中的最小元素。由小根堆的性质,直接输出 即可。 -
DM
:删除堆中的最小元素。将 号节点和 号节点交换, 再减去 (删除一个元素,大小减少 ), 再将 号节点down
操作即可。 -
D k
:删除插入序号为 的节点。插入序号为 的节点在堆中的序号为 ,仿照DM
操作,将 号节点和 号节点交换, 减去 ,再将 号节点分别down
和up
操作一次即可。 -
C k x
:将插入序号为 的节点的值更改为 。 仿照D k
操作,将 设为 ,再将ph_k
号节点分别down
和up
操作一次即可。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n, m, s;
int h[N], ph[N], hp[N];
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 down(int u) {
int t = u;
if (u*2 <= s && h[u*2] < h[t]) t = u*2;
if (u*2+1 <= s && h[u*2+1] < h[t]) t = u*2+1;
if (t != u) {
heap_swap(t, u);
down(t);
}
}
void up(int u) {
while (u / 2 > 0 && h[u] < h[u/2]) {
heap_swap(u, u/2);
u /= 2;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
while (n -- ) {
string op;
cin >> op;
if (op == "I") {
int x; cin >> x;
s ++, m ++;
ph[m] = s, hp[s] = m;
h[s] = x; up(s);
} else if (op == "PM") {
cout << h[1] << endl;
} else if (op == "DM") {
heap_swap(1, s);
s --;
down(1);
} else if (op == "D") {
int k; cin >> k;
k = ph[k];
heap_swap(k, s);
s --;
down(k), up(k);
} else {
int k, x; cin >> k >> x;
k = ph[k];
h[k] = x;
down(k), up(k);
}
}
return 0;
}
2.8 哈希表
2.8.1 普通哈希
题目:
维护一个集合,支持以下两种操作:
I x
插入一个数 ;Q x
询问 是否在集合中出现过。
给定 Q x
操作输出对应结果 Yes
或 No
.
思路:
哈希有两种实现方式:拉链法和开放寻址法。其本质都是通过一个哈希函数
拉链法:对于区间
开放寻址法:若当前
代码:
拉链法:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5+10;
const int p = 100003;
int n;
int h[N], e[N], ne[N], idx;
int f(int x) {
return (long long)x * x % p;
}
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void insert(int x) {
int x_ = f(x);
add(x_, x);
}
bool check(int x) {
int x_ = f(x);
for (int i = h[x_]; i != -1; i = ne[i]) {
int j = e[i];
if (j == x) return 1;
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
memset(h, -1, sizeof h);
char op; int x;
while (n -- ) {
cin >> op >> x;
if (op == 'I') insert(x);
else {
if (check(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
开放寻址法:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e6+10;
const int p = 999987;
int n;
int h[N];
int f(int x) {
return (x % p + p) % p;
}
int find(int x) {
int x_ = f(x);
while (h[x_] != 0x3f3f3f3f && h[x_] != x) {
x_ ++;
if (x_ > p) x_ = 0;
}
return x_;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
memset(h, 0x3f, sizeof h);
char op; int x;
while (n -- ) {
cin >> op >> x;
if (op == 'I') h[find(x)] = x;
else {
if (h[find(x)] != 0x3f3f3f3f) puts("Yes");
else puts("No");
}
}
return 0;
}
2.8.2 字符串哈希
题目:给定一个长度为
思路:
对于字符串,我们有一种常用的哈希方式。将该字符串视作一个 unsigned long long
的溢出取模)。
我们可以先计算
接下来,我们可以通过
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
const int N = 1e5+10;
const int P = 13331;
int n, m;
char s[N];
ull h[N], p[N];
ull get_hash(int l, int r) {
return (h[r] - h[l-1] * p[r-l+1]);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s+1;
h[0] = 0, p[0] = 1;
for (int i = 1; i <= n; ++i) {
h[i] = h[i-1] * P + (s[i] - 'A' + 1);
p[i] = p[i-1] * P;
}
int l1, r1, l2, r2;
while (m -- ) {
cin >> l1 >> r1 >> l2 >> r2;
ull h1 = get_hash(l1, r1), h2 = get_hash(l2, r2);
if (h1 == h2) puts("Yes");
else puts("No");
}
return 0;
}
本文作者:Jasper08
本文链接:https://www.cnblogs.com/Jasper08/p/17461499.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步