第二讲 数据结构
单链表
为什么要用数组模拟链表?—快!
用的最多是 邻接表(实际上是 n 个链表)–> 用来存储 图 和 树
单链表初始状态:
head-> 空结点
插入数据后:
head -> node1 -> node2 -> node3 -> null
每个 node 都有一个 value 和 一个 next 指针
举例
例如我有如下链表
value 3 5 7
head -> node1 -> node2 -> node3 -> null
下标 0 1 2
那么我 e[ 0 ] = 3 ne[ 0 ] = 1, e[ 1 ] = 5 ne[ 1 ] = 2, e[ 2 ] = 7 ne[ 2 ] = -1;
此处空结点的坐标用 -1 来表示
假使我现在有一个结点 a ,想插入 头结点 和 第一个结点之间
- 让 a 的next 指向 第一个结点
- 让 head 的next 指向 a
基本操作
int head,e[N],ne[N],n,idx = 0;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将 x 插入头结点
void add_to_head(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx ++;
}
// 将 x 插入下标是 k 的点的后面
void add(int x,int k)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
// 将 k 后面那个点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
练习
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k 个插入的数后面的数;
- 在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 x。D k
,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。I k x
,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。输入样例:
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;
// head 表示头结点的下标
// e[i] 表示节点 i 的值
// ne[i]表示节点 i 的 next 指针是多少
// idx 存储当前用到了哪个点
int head,e[N],ne[N],n,idx = 0;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将 x 插入头结点
void add_to_head(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx ++;
}
// 将 x 插入下标是 k 的点的后面
void add(int x,int k)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
// 将 k 后面那个点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
cin >> n;
init();
for(int i = 0;i < n;i++)
{
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];
}else{
remove(k-1);
}
}else if(op == 'I')
{
cin >> k >> x;
add(x,k-1);
}
}
for(int i = head;i != -1;i = ne[i])
{
cout << e[i] << ' ';
}
return 0;
}
双链表
原理
用来优化某些问题
有两个指针,一个指向前,一个指向后
一个 e[N] 数组存储值 l[N] 代表他左边是什么 r[N] 代表他右边是谁
模板
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
练习
实现一个双链表,双链表初始为空,支持 5 种操作:
- 在最左侧插入一个数;
- 在最右侧插入一个数;
- 将第 k 个插入的数删除;
- 在第 k个插入的数左侧插入一个数;
- 在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x
,表示在链表的最左端插入数 x。R x
,表示在链表的最右端插入数 x。D k
,表示将第 k 个插入的数删除。IL k x
,表示在第 k 个插入的数左侧插入一个数。IR k 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()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}
void insert(int k,int x)
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx++;
}
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main()
{
int k,x;
init();
cin >> m;
while(m--)
{
string op;
cin >> op;
if(op == "L")
{
cin >> x;
insert(0,x);
}else if(op == "R")
{
cin >> x;
insert(l[1],x);
}else if(op == "D")
{
cin >> k;
remove(k+1);
}else if(op == "IL")
{
cin >> k >> x;
insert(l[k+1],x);
}else if(op == "IR")
{
cin >> k >> x;
insert(k+1,x);
}
}
for(int i = r[0];i != 1;i = r[i])
{
cout << e[i] << " ";
}
return 0;
}
连结表
其实就是 n 个单链表
栈
原理
先进后出,可以理解成一个单口的罐子,每次只有两个操作,放进去和拿出来最上面的一个
模板
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
练习
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数 x;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为
push x
,pop
,empty
,query
中的一种。输出格式
对于每个
empty
和query
操作都要输出一个查询结果,每个结果占一行。其中,
empty
操作的查询结果为YES
或NO
,query
操作的查询结果为一个整数,表示栈顶元素的值。数据范围
1≤M≤100000,
1≤x≤109
所有操作保证合法。输入样例:
10 push 5 query push 6 pop query pop empty push 4 query empty
输出样例:
5 5 YES 4 NO
#include<iostream>
using namespace std;
const int N = 100010;
int stk[N],tt,m;
// tt 表示栈顶下标
// 插入: stk[++tt] = x;
// 弹出: tt--;
// 判断栈空: if(tt>0) --> 不空
// 栈顶元素: stk[tt];
void init()
{
tt = 0;
}
bool isempty()
{
if(tt==0)
{
return true;
}
return false;
}
void push(int x)
{
stk[++tt] = x;
}
void pop()
{
if(!isempty())
{
tt--;
}
}
int query()
{
cout << stk[tt] << endl;
}
int main()
{
cin >> m;
init();
while(m--)
{
int x;
string op;
cin >> op;
if(op == "push")
{
cin >> x;
push(x);
}else if(op == "pop")
{
pop();
}else if(op == "empty")
{
if(isempty())
{
cout << "YES" << endl;
}else{
cout << "NO" << endl;
}
}else if(op == "query")
{
query();
}
}
return 0;
}
给定一个表达式,其中运算符仅包含
+,-,*,/
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。注意:
- 数据保证给定的表达式合法。
- 题目保证符号
-
只作为减号出现,不会作为负号出现,例如,-1+2
,(2+2)*(-(1+1)+2)
之类表达式均不会出现。- 题目保证表达式中所有数字均为正整数。
- 题目保证表达式在中间计算过程以及结果中,均不超过 231-1。
- 题目中的整除是指向 00 取整,也就是说对于大于 00 的结果向下取整,例如 5/3=15/3=1,对于小于 00 的结果向上取整,例如 5/(1−4)=−15/(1−4)=−1。
- C++和Java中的整除默认是向零取整;Python中的整除
//
默认向下取整,因此Python的eval()
函数中的整除也是向下取整,在本题中不能直接使用。输入格式
共一行,为给定表达式。
输出格式
共一行,为表达式的结果。
数据范围
表达式的长度不超过 105。
输入样例:
(2+2)*(1+1)
输出样例:
8
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
void eval()
{
auto b = num.top(); num.pop();
auto a = num.top(); num.pop();
auto c = op.top(); op.pop();
int x;
if (c == '+') x = a + b;
else if (c == '-') x = a - b;
else if (c == '*') x = a * b;
else x = a / b;
num.push(x);
}
int main()
{
// 定义运算符的优先级
unordered_map<char, int> pr{{'+', 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';
i = j - 1;
num.push(x);
}
else if (c == '(') op.push(c);
else if (c == ')')
{
while (op.top() != '(') eval();
op.pop();
}
else
{
// 处理优先级
while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
op.push(c);
}
}
//操作剩余的计算
while (op.size()) eval();
cout << num.top() << endl;
return 0;
}
队列
原理
先进先出,可以理解为一个双头的筒,每次也只有两个操作,从后面塞入一个东西和从前面拿出一个东西
模板
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh <= tt)
{
//不是空的
}
//2. 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh != tt)
{
//不是空的
}
练习
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为
push x
,pop
,empty
,query
中的一种。输出格式
对于每个
empty
和query
操作都要输出一个查询结果,每个结果占一行。其中,
empty
操作的查询结果为YES
或NO
,query
操作的查询结果为一个整数,表示队头元素的值。数据范围
1≤M≤100000,
1≤x≤109
所有操作保证合法。输入样例:
10 push 6 empty query pop empty push 3 push 4 pop query push 6
输出样例:
NO 6 YES 4
#include<iostream>
using namespace std;
const int N = 1000010;
int q[N],hh,tt;
int m;
// 队尾插入,队头弹出
void init()
{
hh = 0;
tt=-1;
}
void push(int x)
{
q[++tt] = x;
}
void pop()
{
hh++;
}
bool isempty()
{
if(hh <= tt)
{
cout << "NO" << endl;
return false;
}
cout << "YES" << endl;
return true;
}
int query()
{
return q[hh];
}
int last()
{
return q[tt];
}
int main()
{
init();
cin >> m;
while(m--)
{
string op;
cin >> op;
if(op == "push")
{
int x;
cin >> x;
push(x);
}else if(op == "pop")
{
pop();
}else if(op == "empty")
{
isempty();
}else if(op == "query")
{
cout << query() << endl;
}
}
return 0;
}
单调栈
常用于:给定一个序列,求每一个数,左(右)边离他最近的且比他小(大)的数
举例
有序列:3 4 2 7 5
找到每一个数左边第一个比他小的数,没有就返回 -1
那么 3 的左边没有数所有返回 -1
4 的左边第一个是 3
2 的左边没有比他小的 返回 -1
7 的左边比他小的最近数为 2
5 的左边比他小的最近数为 2
所有最后返回 [-1,3,-1,2,2]
先考虑暴力做法:两重循环
第一重循环 i从 0-n
第二重从 i-1 开始枚举,一直往左走,知道找到第一个比他小的数停止
如果我们的栈中存在如下关系:
a
x
>
=
a
y
∣
∣
x
<
y
那么
a
x
就会被删掉
a_x >= a_y ||x <y\\ 那么 a_x就会被删掉
ax>=ay∣∣x<y那么ax就会被删掉
所以最后剩下的序列,一定是一个单调序列
那么此时如果进来一个 i,要找到左边第一个 比 i 小的数
if stk[tt] >= i
把 stk[tt] 删了,一直删
直到找到一个 stk[tt] < i
这个 stk[tt] 就是我们要找的值,再把 i 插入单调栈
模板
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
高效输入输出
cin.tie(0);
ios::sync_with_stdio(false);
练习
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤105
1≤数列中元素≤109输入样例:
5 3 4 2 7 5
输出样例:
-1 3 -1 2 2
#include<iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N],tt;
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n;
for(int i = 0;i < n;i++)
{
int x;
cin >> x;
while(tt && stk[tt] >= x)
{
tt--;
}
if(tt)
{
cout << stk[tt] << " ";
}else{
cout << -1 << " ";
}
stk[++tt] = x;
}
return 0;
}
单调队列
经典应用:滑动窗口里的最大值/最小值
举例
假设有序列:
1 3 -1 -3 5 3 6 7
第一次滑动窗口是 【1 3 -1】最小值是 -1
第二次滑动窗口是 【3 -1 -3】最小值是 -3
以此类推最后一次滑动窗口是 【3 6 7】 最小值是 3
我们用队列来维护这个窗口,保证队列中存储的时时刻刻都是我们窗口中的元素
每一次求极值的时候,暴力做法是遍历窗口中的元素,时间复杂度是O(k)
单调队列定律:当一个选手比你小还比你强的时候,你永远无法超越他
那么只要我们的队列中存在如下情况:
前面有一个数,比我后面的数大,那么前面的点一定没有用
因此,我们就可以把大的点删了,最后会变成一个严格单调上升的队列
模板
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
练习
给定一个大小为 n≤106 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为
[1 3 -1 -3 5 3 6 7]
,kk 为 33。
窗口位置 最小值 最大值 [1 3 -1] -3 5 3 6 7 -1 3 1 [3 -1 -3] 5 3 6 7 -3 3 1 3 [-1 -3 5] 3 6 7 -3 5 1 3 -1 [-3 5 3] 6 7 -3 5 1 3 -1 -3 [5 3 6] 7 3 6 1 3 -1 -3 5 [3 6 7] 3 7 你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
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];
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> k;
for(int i = 0;i < n;i ++)
{
cin >> a[i];
}
int hh = 0,tt = -1;
for(int 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)
{
cout << a[q[hh]] << " ";
}
}
cout << endl;
hh = 0,tt = -1;
for(int 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)
{
cout << a[q[hh]] << " ";
}
}
return 0;
}
KMP
原理
失败了退一步,再尝试
假设我们有一个字符串
暴力枚举法
假设 S[N] 是原串,P[M] 是模式串
for(int i = 1;i <= n;i++)
{
bool flag = true;
for(int j = 1;j <= m;j++)
{
if(s[i]!=p[j])
{
flag = false;
break;
}
}
}
在暴力做法中,若出现在匹配不合适的情况,只会将 匹配的起点往后移动一位
那么在我失败后,我新的模板串往后移动多少位可以开始匹配?
也就是我新的模板串往后移动多少位使得 我新的模板串从 起点到我上一次失败的点的串 和 原串相等
引入 next 数组
而 next[ i ] 的意义就是 ,以 i 为终点的后缀 和 从 1 开始的前缀相等,而且后缀的长度最长
假设 next[ i ] = j 那么其含义就是p[1,j] = p[i-j+1,i]
假设我们匹配出错的点是 i,对于模式串来说出错的点是 j+1,也就是 s[i] != p[j+1],那么这个时候我们需要移动我们的模式串,也就是调用我们的next数组,找到以这个点为终点的后缀 和 从 1 开始的前缀,最长的相等,也就是 next[ j ]
模板
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
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 <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
举例
假设有数组
S = " abababc"
P = “abababab”
那么我们有
next[1] = 0 //我不能等于自己
next[2] = 0 //ab 的前缀是 a,后缀是 b,不相等
next[3] = 1 //aba 长度是 1 的时候 前缀是 a,后缀是 a;长度是 2 的时候 前缀是 ab ,后缀是 ba,不匹配,所以 值为 1
next[4] = 2 //abab 长度为 2 的时候 前缀是 ab,后缀是 ab,长度为 3 的时候 前缀是 aba,后缀是 bab,不匹配,所以值为 2
依次类推
next[5] = 3
next[6] = 4
next[7] = 5
next[8] = 6
当我们进行 kmp 匹配的时候,当我们下标是 7 的时候不相等了【也就是 {s[7] = c} != {p[7]=a},i = 7,j = 6】
那么我找到 j = ne[j] == ne[6] = 4
为什么?
因为这个时候 是长度为6的字符串,他的前缀和后缀相等的最大 也就是 我们 next[6]的值也就是 4
练习
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106输入样例:
3 aba 5 ababa
输出样例:
0 2
#include<iostream>
using namespace std;
const int N = 1e6+10;
int ne[N];
char s[N],p[N];
int n,m;
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> p + 1 >> m >> s + 1;
// 求 next 过程
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;
}
// kmp 匹配过程
for(int i = 1,j = 0;i <= m;i++)
{
// j 没有退回起点 并且 s[i] 不能和 p[j+1] 匹配
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];
}
}
return 0;
}
Trie
基本用法&作用
快速地存储和查找字符串集合的数据结构
我们在使用 trie 的过程中,我们使用的字符串一定是 都是 大写/小写,并且长度不长
比分说我们有字符串:
abcdef
abdef
aced
bcdf
cdaa
bcdc
bcff
首先 trie 树有一个根节点 root
我们现在将第一个字符串存进来,我们在存的时候是从前往后遍历每一个字符
首先遍历到的是 字符 a,检查有没有 a 作为子节点,没有的话就把 a 结点创建下来
再遍历到 b,检查 a 下有没有 b 作为子节点,没有就把 b 结点创建下来
再接下来 存储 第二个字符串 abdef
同样我们检查 root 下有没有 a 作为子结点,如果没有就创立一个 a 子结点,那么显然是有的,那就不用操作继续往下走,直到走到 d,发现 b 下没有 d 作为子结点,依次类推检查 d 下有没有 e 作为子结点
最后我们构造的树如下:
当然我们要在每个单词的结尾做一个标记,不然假设我们现在 进来一个 abc ,我们就无法检测出来这个字符串
此时模拟查找的过程
- 查找 bcdf,首先在 root的子节点中查找 b,再在 b 的子节点中查询 c,依次类推查找到 f ,检查有无终止标记
- 查找 abc,确实查到了 c 结点,但是 c 没有终止标记,所以返回查找失败
- 查找 bacd,在 b 的子节点中,并没有查找到 a 作为子节点,所以该字符串不存在,返回查找失败
模板
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}
// 查询字符串出现的次数
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];
}
练习
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式
第一行包含整数 N,表示操作数。
接下来 N 行,每行包含一个操作指令,指令为
I x
或Q x
中的一种。输出格式
对于每个询问指令
Q x
,都要输出一个整数作为结果,表示 x 在集合中出现的次数。每个结果占一行。
数据范围
1≤N≤2∗104
输入样例:
5 I abc Q abc Q ab I ab Q ab
输出样例:
1 0 1
#include<iostream>
using namespace std;
int n;
const int N = 100010;
int son[N][26],cnt[N],idx;
char str[N];
// idx 当前用到的是哪个下标,下标是 0 的点,既是根节点,又是空结点
void insert(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i] - 'a';
if(!son[p][u])
{
son[p][u] = ++idx;
}
p = son[p][u];
}
cnt[p]++;
}
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];
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n;
while(n--)
{
char op[2];
cin >> op >> str;
if(op[0] == 'I')
{
insert(str);
}else{
cout << query(str) << endl;
}
}
return 0;
}
在给定的 NN 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤105
0≤Ai<231输入样例:
3 1 2 3
输出样例:
3
#include<iostream>
using namespace std;
const int N=100010,M=31*N;
int n;
int a[N];
int son[M][2],idx = 0;
// M 代表一个数字串二进制可以有多长
void insert(int x)
{
int p = 0;
for(int i = 30;i >= 0;i--)
{
int u = x>>i&1; // 取出 x 的第i位的二进制数是什么
if(!son[p][u])
{
son[p][u] = ++idx; // 插入的时候发现没有该子节点,就开辟一个新路
}
p = son[p][u]; // 指针指向下一层
}
}
int search(int x)
{
int p = 0;int res = 0;
for(int i = 30;i >= 0;i--)
{
int u = x>>i&1;
if(son[p][!u]) // 如果当前层有对应的不相同的数
{ // p 就指向相应的位置
p = son[p][!u];
res = res * 2 + 1;
}else{
p = son[p][u];
res = res * 2 + 0;
}
}
return res;
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n;
for(int i = 0;i < n;i++)
{
cin >> a[i];
insert(a[i]);
}
int res = 0;
for(int i = 0;i < n;i++)
{
res = max(res,search(a[i]));
}
cout << res << endl;
return 0;
}
并查集
快速地处理
- 将两个集合合并
- 询问两个元素是否在一个集合当中
比如 belong[x] 存储的是 x 属于哪一个集合,比如 belong[x] = a 代表 元素 x 属于 集合 a
那么我们就可以用 O(1) 来判断 元素 x 和 y 是否在同一个集合中
if(belong[x] == belong[y])
但是如果要合并一个 1000个元素的集合 和 2000个元素的集合,那么我们至少要做 1000 次操作
但是并查集可以在近乎 O(1) 的时间内完成这两个操作
基本原理
用树的形式来维护每一个集合
每一个集合的根节点元素 (root) 就是她的代表元素,根节点的编号就是我当前集合的编号
对于每一个点,我们都存储她的父节点是谁,p[x] 表示他的父节点是谁,p[p4] = p1
当我们要求某一个点属于哪一个集合的时候,比方说 找 p4 属于哪一个集合,我们可以根据他的 father 依次往上找,直到根节点为止
//问题1 如何判断树根
if(p[x] == x)
//问题2 如何求 x 的集合编号
while(p[x]!=x)
{
x = p[x];
}
//问题3 如何合并两个集合,加一条边:px 是 x 的集合编号,py 是 y 的集合编号
p[x] = y;
压缩路径优化
当我们查询一个元素 pp4 在哪个集合中的时候,我们将他这个路径上的所有点,全部存到根节点下
模板
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
练习
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b
,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 a 和 b 的两个数是否在同一个集合中;输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为
M a b
或Q a b
中的一种。输出格式
对于每个询问指令
Q a b
,都要输出一个结果,如果 a 和 b 在同一集合内,则输出Yes
,否则输出No
。每个结果占一行。
数据范围
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 n,m;
int p[N];
int find(int x) //返回 x 的祖宗结点
{
if(p[x]!=x)
{
p[x] = find(p[x]);
}
return p[x];
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1;i <= n ;i++)
{
p[i] = i;
}
while(m--)
{
char op;
int a,b;
cin >> op >> a >> b;
if(op == 'M')
{
p[find(a)] = find(b);
}else if(op == 'Q'){
if(find(a) == find(b))
{
cout << "Yes" << endl;
}else{
cout << "No" << endl;
}
}
}
return 0;
}
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b
,在点 a 和点 b 之间连一条边,a 和 b 可能相等;Q1 a b
,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;Q2 a
,询问点 a 所在连通块中点的数量;输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为
C a b
,Q1 a b
或Q2 a
中的一种。输出格式
对于每个询问指令
Q1 a b
,如果 aa 和 bb 在同一个连通块中,则输出Yes
,否则输出No
。对于每个询问指令
Q2 a
,输出一个整数表示点 a 所在连通块中点的数量每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
5 5 C 1 2 Q1 1 2 Q2 1 C 2 5 Q2 5
输出样例:
Yes 2 3
连通块:如果 从 点 a 可以走到 b,从 b 也可以走到 a,那么说明 a 和 b 属于同一连通块
我们可以用集合来维护连通块,当我们在两个连通块之间连一条边的时候,其实就是将两个集合进行合并
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int p[N];
int Size[N];
int find(int x) //返回 x 的祖宗结点
{
if(p[x]!=x)
{
p[x] = find(p[x]);
}
return p[x];
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1;i <= n ;i++)
{
p[i] = i;
Size[i] = 1;
}
while(m--)
{
char op[5];
int a,b;
cin >> op;
if(op[0] == 'C')
{
cin >> a >> b;
if(find(a) == find(b))
{
continue;
}
Size[find(b)] += Size[find(a)];
p[find(a)] = find(b);
}else if(op[1] == '1'){
cin >> a >> b;
if(find(a) == find(b))
{
cout << "Yes" << endl;
}else{
cout << "No" << endl;
}
}else{
cin >> a;
cout << Size[find(a)] << endl;
}
}
return 0;
}
动物王国中有三类动物 A,B,C这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N 编号。
每个动物都是 A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是
1 X Y
,表示 X 和 Y 是同类。第二种说法是
2 X Y
,表示 X 吃 Y。此人对 NN 个动物,用上述两种说法,一句接一句地说出 KK 句话,这 KK 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X 或 Y 比 N 大,就是假话;
- 当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入格式
第一行是两个整数 N 和 K,以一个空格分隔。
以下 K 行每行是三个正整数 D,X,Y两数之间用一个空格隔开,其中 D 表示说法的种类。
若 D=1,则表示 X 和 Y 是同类。
若 D=2,则表示 X 吃 Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000
0≤K≤100000输入样例:
100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5
输出样例:
3
我们通过维护并查集的 子节点到 root 结点的距离来实现 食物链
比如 1 到 root 结点的距离是 1,说明 1 被 root 吃,4 到 root 的距离是 1,说明 4 被 root 吃
2 到 root 的距离是 2,说明 2 被 到 root 距离为 1 的结点吃
3 到 root 的距离是 3,说明 3 被 到 root 距离为 2 的结点吃,同时可以吃到 root 距离为 1的结点
#include <iostream>
using namespace std;
const int N = 50010;
int n, m;
int p[N], d[N];
int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
int res = 0;
while (m -- )
{
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if (x > n || y > n) res ++ ;
else
{
int px = find(x), py = find(y);
if (t == 1)
{
if (px == py && (d[x] - d[y]) % 3) res ++ ;
else if (px != py)
{
p[px] = py;
d[px] = d[y] - d[x];
}
}
else
{
if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
else if (px != py)
{
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
}
printf("%d\n", res);
return 0;
}
堆
如何手写一个堆?
- 插入一个数 heap[++size] = x;up(x);
- 求集合当中的最小值 heap[1]
- 删除最小值 heap[1] = heap[size];size–;down(1);
- 删除任意一个元素 heap[k] = heap[size];size–;down(k) or up(k)
- 修改任意一个元素 heap[k] = x;down(k);up(k);
基本结构
堆的本质是一个完全二叉树,也就是除了最后一层结点,剩余层结点都是满的,最后一层从左到右排列
以小根堆为例,每一个点都是小于等于左右儿子的
我们用一个一维数组来存 堆
其中一号点是根节点
x 的左儿子是 2x,右儿子是 2x+1
如果修改的根节点,我们就将他与他的左右儿子比较,将小的那个移到根节点,直到根节点符合逻辑
如果修改的是叶子结点,我们就将他与他的兄弟和父结点比较,同样将小的移到根节点,递归地调用直到符合逻辑
如果用 size 来维护整个堆的大小
我们在做插入操作的时候,就是在 heap[++size] = x,也就是堆的左下方加入新元素,然后在对这个元素进行 up 操作
我们可以很容易地删除 最后一个点 size–
但是我们很难删除第一个点,我们可以用这样的方法来操作
让第一个元素 = 最后一个元素,然后 size–,再对第一个元素 进行一遍 down 操作
同理如果我们要删除第 k 个点的话,我们只需要让 第 k 个元素 = 最后一个元素,size–,如果 heap[k]的值是变大了,那我应该down(k),如果变小了就 up(k)
模板
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
// ph[k] = j;hp[j] = k;
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
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 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && 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] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
练习
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围
1≤m≤n≤105,
1≤数列中元素≤109输入样例:
5 3 4 5 1 3 2
输出样例:
1 2 3
#include<iostream>
#include<algorithm>
using namespace std;
const int N =100010;
int n,m;
int h[N],Size;
void down(int u)
{
int t = u;
if(u*2 <= Size && h[u*2] < h[t])
{
t = u*2;
}
if(u*2+1 <= Size && h[u*2+1] < h[t])
{
t = u*2+1;
}
if(u!=t)
{
swap(h[u],h[t]);
down(t);
}
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1;i <= n;i++)
{
cin >> h[i];
}
Size = n;
for(int i = n/2;i;i--)
{
down(i);
}
while(m--)
{
cout << h[1] << " ";
h[1] = h[Size];
Size--;
down(1);
}
return 0;
}
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 x;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 k 个插入的数;C k x
,修改第 k 个插入的数,将其变为 x;现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
输入格式
第一行包含整数 N。
接下来 N 行,每行包含一个操作指令,操作指令为
I x
,PM
,DM
,D k
或C k x
中的一种。输出格式
对于每个输出指令
PM
,输出一个结果,表示当前集合中的最小值。每个结果占一行。
数据范围
1≤N≤105
−109≤x≤109
数据保证合法。输入样例:
8 I -10 PM I -10 D 1 C 2 8 I 6 PM DM
输出样例:
-10 6
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N =100010;
int ph[N],hp[N];
int h[N],Size;
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 <= Size && h[u*2] < h[t])
{
t = u*2;
}
if(u*2+1 <= Size && 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,u/2);
u/=2;
}
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
int n,m=0;
cin >> n;
while(n--)
{
char op[10];
int k,x;
cin >> op;
if(!strcmp(op, "I"))
{
cin >> x;
m++;
Size++;
ph[m] = Size;
hp[Size] = m;
h[Size] = x;
up(Size);
}else if(!strcmp(op,"PM"))
{
cout << h[1] << endl;
}else if(!strcmp(op,"DM"))
{
heap_swap(1,Size);
Size--;
down(1);
}else if(!strcmp(op,"D"))
{
cin >> k;
k = ph[k];
heap_swap(k,Size);
Size--;
down(k);
up(k);
}else{
cin >> k >> x;
k = ph[k];
h[k] = x;
down(k);
up(k);
}
}
return 0;
}
哈希表
存储结构
- 开放寻址法
- 拉链法
作用
将一个 -109~109的数 通过 x mod 105 映射到 [0,105]
mod 的数一般要取成一个质数
如果发生冲突,(将两个不一样的数,映射成了同样的数)
例子
维护一个集合,支持如下几种操作:
I x
,插入一个数 x;Q x
,询问数 x 是否在集合中出现过;现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数 N,表示操作数量。
接下来 N 行,每行包含一个操作指令,操作指令为
I x
,Q x
中的一种。输出格式
对于每个询问指令
Q x
,输出一个询问结果,如果 xx 在集合中出现过,则输出Yes
,否则输出No
。每个结果占一行。
数据范围
1≤N≤105
−109≤x≤109输入样例:
5 I 1 I 2 I 3 Q 2 Q 5
输出样例:
Yes No
拉链法
开一个 一维数组 存储所有的 哈希值
比如我们第一次 将 h(11) 映射到了 3,我们就在 一维数组的 3 下面拉一条链,把 11 存下来,如果我们 又将 h(23)映射到了 3,我们就在 11 的后面存一个 23
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100003;
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;
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
int n;
cin >> n;
//清空槽
memset(h,-1,sizeof h);
while(n--)
{
char op[2];
int x;
cin >> op >> x;
if(op[0] == 'I')
{
insert(x);
}else{
if(find(x))
{
cout << "Yes" << endl;
}else{
cout << "No" << endl;
}
}
}
return 0;
}
开放寻址法
假如我们求出来 h(x) = k,如果 k 已经有人了,那么就去下一个坑位,直到我们找到一个没有数据的坑位,就把 x 放进去
那么我们在查找的时候同理,按照 哈希函数 计算出 x 的位置为 k,那么我们找到对应 k 位置的数,如果这个数 != x,那么我们就找 k+1 的数,直到找到 这个数 == x,或者是个空位就说明这个数 不存在
删除的时候,我们一般不会把 x 删掉,而是在数组中打一个标记,证明这个数不存在
#include<iostream>
#include<cstring>
using namespace std;
const int N = 200003,null = 0x3f3f3f3f;
int h[N];
int find(int x)
{
//如果 x 存在,返回的是 x 存在的位置
//如果 x 不存在,返回的是 x 应该存在的位置
int k = (x % N + N ) % N;
while(h[k] != null && h[k] != x)
{
k++;
if(k == N)
{
k = 0;
}
}
return k;
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
int n;
cin >> n;
//清空槽
memset(h,0x3f,sizeof h);
while(n--)
{
char op[2];
int x;
cin >> op >> x;
if(op[0] == 'I')
{
int k = find(x);
h[k] = x;
}else{
int k = find(x);
if(h[k] == null)
{
cout << "No" << endl;
}else{
cout << "Yes" << endl;
}
}
}
return 0;
}
字符串前缀哈希法
常用于比较两个字符串是否相等
比如我们有一个字符串 str = “ABCABCDEFXCACWING”
先预处理出来所有前缀的哈希
h[0] = 0
h[1] = “A” 的哈希值
h[2] = “AB” 的哈希值
h[3] = “ABC” 的哈希值
h[4] = “ABCA” 的哈希值
…
我们将每一个字符串 看作 p进制的数,每一位上的字母,就代表着 P 进制的每一位数字
以 h[3] 为例,他一共有三个字母那就看成有 三位,第一位上的数字是 A,第二位上的数字是 B,第三位上的数字是 C
把 A 当成 1,B 当成 2,C 当成 3,那么 “ABC” 就可以看成 p进制的 123 = 1 x p2+2 x p1+ 3 x p0,这个数可能很大所以最后我们对他 mod Q
那么最后我们就可以把这个数 映射到 0~Q-1 的位置了
一般不能映射成 0 —> 因为会把不同的字符串映射成 同一个数
一般来说 p = 131 或 13331 Q = 264 冲突会比较少
| |
L R
我们已知 h[L-1] 和 h[R]的哈希值,如何求 [L,R] 的哈希值
左边是高位,右边是低位
在 h[R] 里 R 就是第 0 位,1 就是 R-1位 pR-1~p0
h[L-1] 里 L-1 就是第 0 位,1 就是 L-2位 pL-2~p0
我们第一步需要将 h[L-1] 这一段往左移,移到和我们的 h[R] 对齐为止
例如 123 和 12345,123左移后 == 12300 和 12345 对齐
h[L-1] x pR-L+1
第二步就是h[R] - h[L-1] x pR-L+1
这样就可以求出来 [L,R] 这一段的哈希值了
小技巧:我们用 unsighed long long 来存所有的 h
所以:h[i] = h[i-1] x p + str[i]
模板
(1) 拉链法
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) 开放寻址法
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的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
*/
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
练习
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2请你判断 [l1,r1][l1,r1] 和 [l2,r2][l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 n 和 m,表示字符串长度和询问次数。
第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。
接下来 m 行,每行包含四个整数 l1,r1,l2,r2表示一次询问所涉及的两个区间。
注意,字符串的位置从 1 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出
Yes
,否则输出No
。每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
8 3 aabbaabb 1 3 5 7 1 3 6 8 1 2 1 2
输出样例:
Yes No Yes
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010,P = 131;
int n,m;
char str[N];
ULL h[N],p[N];
ULL get(int l,int r)
{
return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m >> str+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];
}
while(m--)
{
int l1,r1,l2,r2;
cin >> l1 >> r1 >> l2 >> r2;
if(get(l1,r1) == get(l2,r2))
{
cout << "Yes" << endl;
}else{
cout << "No" << endl;
}
}
return 0;
}
STL
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector, greater> q;stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素deque, 双端队列
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)set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,–bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[] count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0 set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
#include<stack>
#include<deque>
#include<set>
#include<map>
#include<unordered_map>
#include<unordered_set>
#include <bitset>
using namespace std;
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
//vector
cout << "vector" << endl;
vector<int> a;
vector<int> b(10);
vector<int> c(10,3); //有10个数,每一个数都是3
vector<int> d[10]; //定义 10 个 vector
cout << b.size() << endl; //元素的个数
cout << b.empty() << endl; //是不是空的
b.clear(); //清空
cout << b.size() << endl;
a.push_back(1); //向最后插入一个数
a.push_back(2);
cout << a.front() << endl; //第一个数
cout << a.back() << endl;
// a.begin()/a.end() 迭代器,第一个位置和最后一个位置,可以看成指针
for(auto i = a.begin();i != a.end();i++)
{
cout << *i << endl;
}
for(auto x:a)
{
cout << x << endl;
}
a.pop_back(); //删掉最后一个数
cout << *(a.begin()) << endl;
//支持比较运算,根据字典序来比较
vector<int> aa(4,3),bb(3,3);
if(aa > bb)
{
cout << "Yes" << endl;
}
cout << endl;
//pair
cout << "pair:" << endl;
pair<int,string> p;
p={20,"abc"};
cout << p.first << " " << p.second << endl;
pair<int,pair<int,int>> pp;
pp={30,{15,45}};
cout << pp.first << " " << pp.second.first << " " << pp.second.second << endl;
cout << endl;
//string
cout << "string:" << endl;
string str = "yxc";
str += 'c';
cout << str << endl;
str += "ccc";
cout << str << endl;
string str1 = str.substr(0,10); //超出就全部
string str2 = str.substr(1); //从 1 开始的 字串
cout << str2 << endl;
cout << str1 << endl;
cout << str.substr(0,3) << endl; //从 0 开始 3 个 字符
cout << str.c_str() << endl;
cout << endl;
//queue
cout << "queue:" << endl;
queue<int> q;
q.push(1); //向队尾插入元素
q.push(2);
cout << q.front() << endl; //返回队头元素
cout << q.back() << endl; //返回队尾元素
q.pop(); //弹出队头元素
cout << q.front() << endl;
q = queue<int>(); //重新构造一个q
cout << endl;
//priority_queue
cout << "priority_queue:" << endl;
priority_queue<int> heap; //默认大根堆
priority_queue<int,vector<int>,greater<int>>heap1; //小根堆
heap.push(2); //插入一个元素
cout << heap.top() << endl; //返回堆顶元素
heap.push(3);
cout << heap.top() << endl;
heap.push(1);
cout << heap.top() << endl;
heap.pop(); // 弹出堆顶元素
cout << heap.top() << endl;
cout << endl;
//stack
cout << "stack:" << endl;
stack<int> stk;
stk.push(1); //栈顶插入一个元素
stk.push(2);
cout << stk.top() << endl; //返回栈顶元素
stk.pop(); //弹出栈顶元素
cout << stk.top() << endl;
cout << endl;
//deque
cout << "deque:" << endl;
deque<int> dq;
dq.push_back(1);
dq.push_front(2);
cout << dq[1] << endl;
cout << dq.front() << endl;
cout << dq.back() << endl;
dq.pop_back();
cout << dq.back() << endl;
dq.pop_front();
cout << dq.front() << endl;
cout << endl;
//set&multiset
cout << "set&multiset:" << endl;
set<int> s; //不可重复
multiset<int> ms; //可以重复
s.insert(1);
s.insert(1);
cout << s.size() << endl;
ms.insert(1);
ms.insert(1);
cout << ms.size() << endl;
cout << s.count(1) << endl; //返回某一个数的数量
cout << ms.count(1) << endl;
s.erase(1);
cout << s.count(1) << endl;
s.insert(1);
s.insert(3);
s.insert(2);
set<int>::iterator it;
it = s.lower_bound(3); //返回大于等于 x 的最小的数的迭代器
cout << *it << endl;
it = s.upper_bound(3); //返回大于 x 的最小的数的迭代器
cout << *it << endl;
cout << endl;
//map&multimap
cout << "map&multimap:" << endl;
map<int,int> map;
multimap<int,int> mlmap; //可重复
map[1] = 1;
map.insert({2,2});
map[3] = 3;
cout << map[1] << endl;
cout << map[2] << endl;
cout << map.size() << endl;
auto mapit = map.begin();
map.erase(mapit);
cout << map.size() << endl;
cout << endl;
//unordered_set&unordered_map&unordered_multimap&unordered_multiset
//不支持 lower_bound & upper_bound,其余同上
// crud 复杂度是O(1)
//bitset
// 假设我们需要开一个 1024个bool 的数组,那么我们需要 1k的内存
// 如果我们把他压到每一位的话,我们只需要 128B 的内存
cout << "bitset:" << endl;
// 存 10000x10000的bool矩阵
bitset<10000> bit;
cout << bit.any() << endl; //判断是否至少有一个 1
bit.set(); //把所有位变成 1
//~bit; 取反
cout << bit.any() << endl; //判断是否至少有一个 1
cout << bit.none() << endl;
bit.reset(); //把所有位变成 0
cout << bit.none() << endl; //判断是否全为 0
bit.set(0,1); //把第 0 位变成 1
bit.flip(); //等价于 ~
bit.flip(0); //把第 0 位取反
return 0;
}