Acwing 算法基础课 chapter 2
lecture 2 数据结构
模板
单链表
数组模拟 比 动态分配链表【struct Node {}; new Node();】 快
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点 |
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点 |
栈
// tt表示栈顶 |
队列
// hh 表示队头,tt表示队尾 |
单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0; |
单调队列
常见模型:找出滑动窗口中的最大值/最小值
删除,直到它严格单调。
int hh = 0, tt = -1; |
KMP
求Next数组: |
Trie树
高效地存储和查找字符串集合的数据结构。
int son[N][26], cnt[N], idx; |
并查集
- 将两个集合合并;
- 询问两个元素是否在一个集合当中。
普通暴力的做法:
belong[x] = a 集合编号
if(belong[x] == belong[y])
查询是否一个集合易 O(1) ,合并则比较复杂 O(n) 。
并查集可近似 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。
问题 2 的时间复杂度较高,可进行 路径压缩(如图) 或 按值合并(让高度小的树接到高度大的树) 优化。
朴素并查集
int p[N]; //存储每个点的祖宗节点 |
维护size的并查集
int p[N], size[N]; |
维护到祖宗节点距离的并查集
int p[N], d[N]; |
堆
手写堆,STL中形式是优先队列。
形式
完全二叉树;小根堆(根节点 <= 左右子节点);大根堆
存储
一维数组
操作
-
down(x) {} 往下调 [O(logn)]
使用场景:某一个值变大了,需要下移。
-
up(x) {} 往上调
使用场景:某一个值变小了,需要上移。
- 插入一个数
heap[++size] = x; up(size); - 求集合当中的最小值[O(1)]
heap[1]; - 删除最小值
heap[1] = heap[size]; size--; down(1); - *删除任意一个元素
heap[k] = heap[size]; size--; down(k); up(k); - *修改任意一个元素
heap[k] = x; down(k); up(k);
建堆
有两种建堆的方法:
- 一种是从一个空树开始,每次输入一个数,就按照插入操作插入这个树,时间复杂度 nlogn (一共n个元素,每个元素logn)。
- 还有一种建堆方法是已经把数据都输入到了数组 a[N] ,怎么根据这个数组直接建堆。就是从 n/2 开始down(),因为 n/2 是倒数第二层,如果是倒数第一层其实每个节点自然成堆了,所以从倒数第二层开始,是最简单的一个二层树结构,down 完形成一个堆。从右下角往左边开始建堆,这样从下往上之后,轮到每个节点他下面的子树肯定已经成堆了,满足 down 的条件。
复杂度分析:(以完全二叉树为例)
为什么是 n/4 * 1 + n/8 * 2 +… ?
根据每一层的节点树乘以需要往下down的迭代次数(其实就是往下的层树)。假设一共有n个元素的完全二叉树,那么最后一层有n/2个元素(满二叉树情况),不需要往下down(), 倒数第二层(含)之上总共n/2个节点。i = n / 2是最后一个拥有孩子的节点,n/2以下的节点一定是一个子节点,就从非子节点开始向上建立堆。
倒数第二层有n/4个元素,每个元素最多往下down一次,倒数第三层有n/8个元素,每个元素最多往下down两次……以此类推。最终是一个等差等比混合数列的求和,为**O(n)**的复杂度。
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1 |
哈希表
一般哈希(存储结构)
x ∈ (-109,109),h(x) ∈ (0,105)
- x mod 105 ∈ [0,105]
- 冲突问题。
方法
-
拉链法
操作:
-
添加(插入)
-
查找
-
删除(一般不直接删除)。使用标识符
关于哈希表长度选取:
比如大部分是偶数,这时候如果HASH数组容量是偶数,容易使原始数据HASH后不会均匀分布。
比如 2 4 6 8 10 12这6个数,如果对 6 取余 得到 2 4 0 2 4 0 只会得到3种HASH值,冲突会很多
如果对 7 取余 得到 2 4 6 1 3 5 得到6种HASH值,冲突较小。同样地,如果数据都是3的倍数,而HASH数组容量是3的倍数,HASH后也容易有冲突。
用一个质数则会减少冲突的概率。
选素数操作
using namespace std;
const int N = 100010;
int main()
{
for(int i = 100000;;++i)
{
bool flag = true;
for(int j = 2;j*j <= i;++j)
{
if(i % j == 0)
{
flag = false;
break;
}
}
if(flag == true) //质数
{
cout << i << endl;
break;
}
}
return 0;
}模板代码:
int h[N], e[N], ne[N], idx;
// 向哈希表中插入一个数
void insert(int x)
{
int k = (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 (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
} -
-
开放寻址法
开辟长度应为题目给出的 2~3 倍,质数。
基本思路:
操作:
- 添加
- 查找
- 删除(打标志,同上),可将之归为查找一种
int h[N];
// 如果x在哈希表中存在,返回x的所在的位置(下标);如果x不在哈希表中,返回x应该插入的位置
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;
}
字符串哈希
字符串前缀哈希法:
-
先把每一个前缀的哈希值求出来(字符串 --> 数字)。
-
==将字符串看成 P进制 数。==
-
将 P进制 的数转为十进制的数。
小技巧:
通过这样的方式,把任何一个字符串映射到从 0 ~ Q - 1 的一个数。
注意:
- 一般情况下不能映射成 0。否则易冲突。 e.g. A 0; AA 0。
- 此类哈希不考虑冲突。
- P 的经验值是131或13331,取这两个值的冲突概率低;
- 取模的数 Q 用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果。
-
-
利用前缀哈希算出任意子串哈希
-
将 h[L - 1] 往左移若干位,与 h[R] 对齐。
-
从 L ~ R 段的哈希值 h[R] - h[L-1] * p(R-L+1)
此处用 unsigned long long 存储,便无需对Q(264)取模,溢出即为取模。
-
关于预处理 h[i] = h[i - 1] * p + str[i]
-
当要 判断两个字符串是否相等 的时候可以使用这种方法。
模板代码
typedef unsigned long long ULL; |
能用KMP者大都可以字符串哈希实现,4060. 字符串循环节 - AcWing题库 例外。
STL简介
vector
变长数组,倍增思想。
系统为某一程序分配空间时,所需时间与空间大小无关,与申请次数有关。故应减少倍增次数。
- size() 返回元素个数(所有容器都有)
- empty() 返回是否为空(所有容器都有)
- clear() 清空
- front()/back()
- push_back()/pop_back()
- begin()/end() (end() 最后一个数的下一个位置)
- []
- 支持比较运算,按字典序(首字母)
pair<int, int>
存储二元组
-
first, 第一个元素
-
second, 第二个元素
-
支持比较运算,以 first 为第一关键字,以 second 为第二关键字(字典序)
-
赋值
p = make_pair(10,“abc”);
p = {20,“abc”};
-
可以嵌套
string
字符串
- szie()/length() 返回字符串长度
- empty()
- clear()
- substr(起始下标(从零开始),(子串长度)) 返回子串
- c_str() 返回字符串所在字符数组的起始地址(头指针)
queue
队列
- size()
- empty()
- push() 向队尾插入一个元素
- front() 返回队头元素
- back() 返回队尾元素
- pop() 弹出队头元素
- 没有clear
priority_queue
优先队列,默认是大根堆
-
push() 插入一个元素
-
top() 返回堆顶元素
-
pop() 弹出堆顶元素
-
定义成 小根堆 的方式:
priority_queue<int, vector<int>, greater<int>> q;表示优先队列后面的元素都要大于优先队列前面的元素
-
没有clear
stack
栈
- size()
- empty()
- push() 向栈顶插入一个元素
- top() 返回栈顶元素
- pop() 弹出栈顶元素
- 没有clear
deque
(加强版vector) 双端队列。效率相对低
- size()
- empty()
- clear()
- front()/back()
- push_back()/pop_back()
- push_front()/pop_front()
- begin()/end()
- []
set, map, multiset, multimap
基于平衡二叉树(红黑树),动态维护有序序列
- size()
- empty()
- clear()
- begin()/end()
- ++, – 返回前驱和后继,时间复杂度 O(logn)
multi- :支持重复元素
-
set/multiset
insert() 插入一个数
find() 查找一个数 (不存在返回end迭代器)
count() 返回某一个数的个数
erase()- 输入是一个数x,删除所有x O(k + logn) (k为个数)
- 输入一个迭代器,删除这个迭代器
==lower_bound()/upper_bound()==
-
lower_bound(x) 返回大于等于x的最小的数的迭代器
-
upper_bound(x) 返回大于x的最小的数的迭代器
-
lower_bound( begin,end,num,greater
() ): 二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater
() ): 查找第一个小于num的数字
-
map/multimap
- insert() 插入的数是一个pair
- erase() 输入的参数是pair或者迭代器
- find()
- [] 时间复杂度是 O(logn) (数组是O(1))
- lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap
哈希表
和上面类似,增删改查的时间复杂度是 O(1)。
不支持 lower_bound()/upper_bound(),迭代器的++,–
bitset
圧位
使用情况:bool 存储一个字节,使用压位。能够节省8倍空间。
bitset<10000> s; |
题目
单链表
AcWing 826. 单链表
实现一个单链表,链表初始为空,支持三种操作:
-
向链表头插入一个数;
-
删除第 k 个插入的数后面的数;
-
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入样例
10 |
输出样例
6 4 6 5 |
解析
|
双链表
AcWing 827. 双链表
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x,表示在链表的最左端插入数 x。R x,表示在链表的最右端插入数 x。D k,表示将第 k 个插入的数删除。IL k x,表示在第 k 个插入的数左侧插入一个数。IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。
解析
实际为两个操作。add,remove [ add(k,x),add(left,x),add(right,x) ]
|
栈
AcWing 828. 模拟栈
实现一个栈,栈初始为空,支持四种操作:
- push x – 向栈顶插入一个数 x;
- pop – 从栈顶弹出一个数;
- empty – 判断栈是否为空;
- query – 查询栈顶元素。
输入样例
10 |
输出样例
5 |
解析
|
AcWing 3302. 表达式求值
给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
输入样例
(2+2)*(1+1) |
输出样例
8 |
解析
中间节点都是运算符,叶节点都是数字。
中缀表达 后缀表达
|
队列
AcWing 829. 模拟队列
实现一个队列,队列初始为空,支持四种操作:
- push x – 向队尾插入一个数 x;
- pop – 从队头弹出一个数;
- empty – 判断队列是否为空;
- query – 查询队头元素。
输入样例
10 |
输出样例
NO |
解析
|
单调栈
AcWing 830. 单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入样例
5 |
输出样例
-1 3 -1 2 2 |
解析
每一个元素只进栈一次,每一个元素最多只会出栈一次。总操作2n,复杂度为O(n)。
|
复杂度:
内层循环每循环一次,tt-1;外层循环每循环一次,tt+1。
最多 +n 次,最多 -n 次。
整个时间复杂度是O(n)。
单调栈应用 —— Acwing 131. 直方图中最大的矩形
输入样例
7 2 1 4 5 1 3 3 |
输出样例
8 |
解析
|
单调队列
AcWing 154. 滑动窗口
给定一个大小为 n≤106 的数组。有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。你只能在窗口中看到 k 个数字。每次滑动窗口向右移动一个位置。
输入样例
8 3 //两个整数 n 和 k,分别代表数组长度和滑动窗口的长度 |
输出样例
-1 -3 -3 -3 3 3 //从左至右,每个位置滑动窗口中的最小值。 |
解析
|
注释:
|
KMP
AcWing 831. KMP字符串
给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模板串 P 在模式串 S 中多次作为子串出现。求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入样例
3 |
输出样例
0 2 |
解析
|
j = ne[j];————重新开始匹配时,可以把j最多移动多少
时间复杂度是O(n)。以第二个循环的j为例。j最多加m次,最多减m次.O(2m)----->O(m)。
上述方法所得下标:
| 字符串 | a b a | a b a b a |
|---|---|---|
| 下标 | 0 1 2 3 | 0 1 2 3 4 5 |
| next[] | 0 0 1 | 0 0 1 2 3 |
法二
严版KMP字符串匹配算法 求next数组(单次匹配)
void get_next(String T,int next[]) |
上述方法对应next数组
| 字符串 | a b a a b c a c |
|---|---|
| 下标 | 1 2 3 4 5 6 7 8 |
| next[] | 0 1 1 2 2 3 1 2 |
推荐 法一 。
Trie
AcWing 835. Trie字符串统计
维护一个字符串集合,支持两种操作:
- I x 向集合中插入一个字符串 x;
- Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入样例
5 |
输出样例
1 |
解析
|
AcWing 143. 最大异或对
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入样例
3 |
输出样例
3 |
解析
-
暴力 O(n) * n
int res = 0;
for(int i = 0;i < n;++i) //枚举第一个数
{
for(int j = 0;j < i;++j) //枚举第二个数
res = max(res,a[i] ^ a[j]);
} -
使用Trie树 O(31) * n(十万) ≈ nlogn
思路:从前往后枚举,先 插入 再 查询 (也可先查询再插入,但开始空要加判断)。
查询:查询 ai 前面和 ai 异或最大的值。
整体代码
|
并查集
AcWing 836. 合并集合
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
- M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
- Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中。
输入样例
4 5 |
输出样例
Yes |
解析
|
find() 函数
p[x] = find(p[x])在回溯过程中路径压缩优化
AcWing 837. 连通块中点的数量
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
- C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
- Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
- Q2 a,询问点 a 所在连通块中点的数量。
输入样例
5 5 |
输出样例
Yes |
解析
同一个连通块之意:从 a 可以走到 b ,从 b 可以走到 a。
|
AcWing 240. 食物链
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。A 吃 B,B 吃 C,C 吃 A。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 X 和 Y 是同类。
第二种说法是 2 X Y,表示 X 吃 Y。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X 或 Y 比 N 大,就是假话;
- 当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入样例
100 7 |
输出样例
3 |
解析
样例图示
思路:
并查集中,维护每个点到根节点的 距离 。将距离分为三大类:
- 何为距离,即是
关于代的关系,对 3 取模以示之。
-
每个点到父节点距离如何维护?
存的时候存其对父节点距离。在做 路径压缩 时,将每个点到 父节点 的距离更新成对 根节点 的距离。
-
只需要知道每个点与根的关系,即可判断两个点之间的关系。
-
关于find
-
存下父节点的根节点;
再加上父节点到其根节点的距离(把d[x]更新成到根节点的距离);
把当前节点的根节点指向最终根节点。
d[i]:第 i 个节点到其父节点距离 ref:find()函数调用过程
整体代码
|
堆
AcWing 838. 堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入样例
5 3 |
输出样例
1 2 3 |
解析
|
AcWing 839. 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
I x,插入一个数 x;PM,输出当前集合中的最小值;DM,删除当前集合中的最小值(数据保证此时的最小值唯一);D k,删除第 k 个插入的数;C k x,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
输入样例
8 |
输出样例
-10 |
解析
-
难点
从第 k 个插入点找到对应元素,又要从堆里的元素找回来。
ph[k] 与 hp[j] :
第 k 个插入点的对应下标与堆中下标为j的点对应的k。(两者如反函数关系)[如何理解模拟堆中的heap_swap,hpN], ph[N]? - AcWing
-
idx
h[k] = x, h数组存的是节点的值,按理来说应该h[idx]来存,但是节点位置总是在变的,因此需维护k和idx的映射关系。用
ph数组来表示ph[idx] = k(idx到下标), 那么结点值为h[ph[idx]], 儿子为ph[idx] * 2和ph[idx] * 2 + 1, 这样值和儿子结点通过idx联系在一起。 -
hp与ph数组
-
ph数组 主要用于帮助从idx映射到下标k;
-
在
swap操作中我们输入是堆数组的下标,需要hp数组方便查找idx。即:hp数组 查找每个堆数组的k下标 对应idx(第idx个插入)
-
-
操作实例
交换函数
void heap_swap(int a, int b)
{
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}插入
if (op == "I")
{
scanf("%d", &x);
size ++ ;
idx ++ ; //记录第几次插入(设置新的idx)
ph[idx] = size, hp[size] = idx; //每次插入都是在堆尾插入(设置ph与hp)
h[ph[idx]] = x; //记录插入的值
up(ph[idx]);
}删除
- 找到第idx个插入元素在堆数组中的位置(堆数组下标)
- 与堆尾元素交换
- 在原来第idx个元素所在的位置进行down和up操作。(up,down,swap操作的都输入都是下标)
if (op == "D")
{
scanf("%d", &idx);
k = ph[idx]; //必须要保存当前被删除结点的下标
heap_swap(k, size); //第idx个插入的元素移到了堆尾,此时ph[idx]指向堆尾
size --; //删除堆尾
up(k); //k是之前记录被删除的结点的下标
down(k);
}
-
-
整体代码
using namespace std;
const int N = 100010;
int h[N],ph[N],hp[N],cnt;
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;
while(u*2 <= cnt && h[u*2] < h[t]) t = u*2;
while(u*2+1 <= cnt && h[u*2+1] < h[t]) t = u*2+1;
if(u != t)
{
heap_swap(u,t);
down(t);
}
}
void up(int u)
{
while(u/2 && h[u/2] > h[u])
{
heap_swap(u/2,u);
u /= 2;
}
}
int main()
{
int n,m = 0;
scanf("%d", &n);
while (n -- )
{
char op[5];
int x,k;
scanf("%s", op);
if(!strcmp(op, "I"))
{
scanf("%d",&x);
cnt ++;
m ++;
ph[m] = cnt,hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
else if(!strcmp(op, "PM"))
{
printf("%d\n",h[1]);
}
else if(!strcmp(op, "DM"))
{
heap_swap(1,cnt);
cnt --;
down(1);
}
else if(!strcmp(op, "D"))
{
// scanf("%d", &k);
// heap_swap(ph[k],idx);
// idx --;
// down(ph[k]),up(ph[k]); //0223这么做错误,k已经改变
scanf("%d", &k);
k = ph[k]; //0216遗漏
heap_swap(k,cnt);
cnt --;
down(k),up(k); //这两个函数有且只有一个执行
}
else if(!strcmp(op, "C"))
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k),up(k);
}
}
return 0;
}
哈希表
AcWing 840. 模拟散列表
维护一个集合,支持如下几种操作:
I x,插入一个数 x;- Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入样例
5 |
输出样例
Yes |
解析
-
拉链法
using namespace std;
const int N = 100003;
int h[N],e[N],ne[N],idx; //此处 e 和 ne 同单链表是一样的
void insert(int x)
{
int k = (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(int i = h[k];i != -1;i = ne[i])
//h[k]存的链表第一个节点下标,e[i]为当前点的值;做完操作后,ne[i]为下一个点的下标。空指针的下标为 -1
{
if(e[i] == x)
{
return true;
}
}
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);
while (n -- )
{
char op[2];
int x;
scanf("%s%d",op,&x);
if(*op == 'I')
insert(x);
else
{
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
} -
开放寻址法
using namespace std;
const int N = 200003;
int h[N],null = 0x3f3f3f3f; //null 不在 x 范围内的数
int find(int x)
{
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x)
{
k++;
if(k == N) k = 0;
}
return k;
}
int main()
{
memset(h, 0x3f, sizeof h); //memset函数按字节,h为int,四个字节,每个皆是0x3f(8位)
int n;
scanf("%d", &n);
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
int k = find(x);
if(*op == 'I')
h[k] = x;
else
{
if(h[k] != null) puts("Yes");
else puts("No");
}
}
return 0;
}
AcWing 841. 字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。字符串中只包含大小写英文字母和数字。
输入样例
8 3 |
输出样例
Yes |
解析
p 数组的意义,便是用来存储 p 的次方。因为公式内有 p 的次方,故先将 p 预处理出来。
h 数组:某一个前缀的哈希值。
|
输入样例
abcbabcbabcba |
输出样例
Case 1: 13 |
解析
前期步骤同上,而后
- 长度是奇数
-
枚举中点;
-
二分半径。
每次二分一个长度,判断左右两边的哈希值是否一样。若同,扩长度;否,缩。
-
长度是偶数
在每一个字母之间插入符号 ,将其化为奇数。
下标的计算
//hl 正序字符串所有前缀的哈希值;hr 逆序字符串所有前缀的哈希值。 |
chapter 2 END。







































