一、链表
1.单链表
//head 表示头结点下标
//e[i] 表示节点i的值
//ne[i] 表示节点i的next指针是多少
//idx 存储当前已经用到了哪个节点
int head,e[N],ne[N],idx;
//初始化
void init(){
head=-1;
idx=0;
}
//将x插入到头结点
void add_to_head(int x){
e[idx]=x;
ne[idx]=head;
head=idx++;
}
//将x插到下标为k的节点后面
void add(int k,int x){
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx++;
}
//将下标为k的点后面的点删掉
void remove(int k){
ne[k]=ne[ne[k]];
}
2.双链表
//l[i],r[i] 表示i节点左,右指针
int e[N],l[N],r[N],idx;
//初始化
void init(){
r[0]=1;
l[1]=0;
idx=2;
}
//在下标为k的点右面插入一个点
void add(int k,int x){
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;
r[k]=idx++;
}
//删除第k个点
void remove(int k){
r[l[k]]=r[k];
l[r[k]]=l[k];
}
二、栈
//用stk数组模拟栈 tt为栈顶指针
int stk[N],tt;
//插入x
stk[++tt]=x;
//弹出
tt--;
//判断是否为空
if(tt>0) not empty
else empty
//获取栈顶元素
int top=stk[tt];
三、队列
1.普通队列
//hh表示队头,tt表示队尾
int q[N],hh,tt;
//插入x
q[++tt]=x;
//弹出
hh++;
//判断是否为空
if(tt>=hh) not empty
else empty
//取出队头元素
int head=q[hh];
//取出队尾元素
int tail=q[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;
// 队头的值
int head = q[hh];
// 判断队列是否为空
if (hh != tt) not empty
else empty
三、KMP
主要思想:寻找模式串中以下标 i 为结尾的最长相等子前后缀
//s[]是长文本,p[]是模式串,ne[i]存储以p[i]结尾的模式串中最长相等子前后缀的长度
//n,m为s[],p[]的长度
char s[N],p[M],ne[M];
int n,m;
//求模式串的ne数组:
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];
...//根据题目发挥
}
}
四、Trie树
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[p][u]存储树中每个节点的子节点
// cnt[p]存储以每个节点结尾的单词数量
// 插入一个字符串
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];
}
五、并查集
AcWing 836. 合并集合
AcWing 837. 连通块中点的数量
AcWing 240. 食物链
基本原理:
把每个集合用一棵树表示,树的根节点就是集合的编号,且每个节点都存储它的父节点,用 p[x] 表示 x 节点的父节点(定义根节点的父节点是它本身)。
Q&A:
Q1:如何判断树根?
A1:if(p[x]==x)
Q2:如何求x的集合编号?
A2:while(p[x]!=x) x=p[x];
Q3:如何合并两个集合
A3:将一棵树的根节点连在另一棵树的任意一个节点上
p[x]=y;//x,y是两棵树的根节点
优化:路径压缩:
当遍历一棵树时,将其所有节点的父节点都赋值为这棵树的根节点,即可将时间复杂度近似降为
O
(
1
)
O(1)
O(1)。
(1)朴素并查集:
//存储每个点的根节点
int p[N];
//返回x的根节点+路径压缩
int find(int x){
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
//合并a和b所在的两个集合
p[find(a)] = find(b);
//判断是否在同一集合中
if(find(a) == find(b))
(2)维护集合size的并查集:
//p[]存储每个点的根节点, size[]只有根节点的有意义,表示根节点所在集合中的点的数量
int p[N], size[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;
size[i] = 1;
}
//合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
(3)维护到根节点距离的并查集:
//p[]存储每个点的根节点, d[x]存储x到p[x]的距离
int p[N], d[N];
//返回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)的偏移量
六、堆
AcWing 838. 堆排序
AcWing 839. 模拟堆
利用数组存储一个完全二叉树,其中下标为x的节点左儿子为 2 x 2x 2x,右儿子为 2 x + 1 2x+1 2x+1
小根堆:每个节点存储的数小于等于其两个子节点(根节点存储最小的数)
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
// 交换两个点,及其映射关系
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 /= 2;
}
}
//插入一个数
h[++size]=x;
up(x);
//求数组中最小值
h[1];
//删除最小值
h[1]=h[size--];
down(1);
//删除下标为k的数
h[k]=h[size--];
down(k),up(k);
//修改下标为k的数
h[k]=x;
down(k),up(k);
七、哈希
将一组范围很大的数据映射到另一个小范围里
一般操作:对一个质数取模
1.一般哈希
(1)拉链法
基本思想:插入时存在冲突用链表存储
const int N = 100003;
int h[N], e[N], ne[N], idx;
//向哈希表中插入一个数
void insert(int x){
//负数取模还是负数,+N后%N变为正数
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)开放寻址法
基本思想:非空就找下一位
当定义一个无穷大(
n
>
1
0
9
n>10^9
n>109)时,经验是定义为0x3f3f3f3f,可以用memset(h,0x3f,sizeof(h))
赋初始值
//N开到元素个数的2~3倍,null开到数据范围之外
const int N=200003,null=0x3f3f3f3f;
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;
}
int main(){
memset(h,0x3f,sizeof(h));
}
2.字符串哈希
AcWing 841. 字符串哈希
基本思想:将字符串看成P进制数,P的经验值是131或13331,冲突概率低
小技巧:取模的数用
2
64
2^{64}
264,这样直接用unsigned long long
存储,溢出后的结果就是取模后的结果,(不过这里假定RP MAX无冲突)
const int N=100003;
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];
}