Template
Single Link List
【Data Structure】
【Pattern】
#include <iostream>
using namespace std;
const int N = 100010;
/*每个节点按顺序都有编号,存储在相应数组的下标中
每个节点i都有对应的值和下一个节点的位置,分别存储在e[i]和ne[i]中
idx是当前需要分配的节点
*/
int head, e[N], ne[N], idx;
//初始化,-1代表为空节点,0代表从下标0开始分配节点
void init(){
head = -1;
idx = 0;
}
//在头节点后插入值为x的节点
void add_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx;
idx ++;
}
//在k节点后插入值为x的节点
void add(int k, int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx ++;
}
//删除k节点后面的节点
void del(int k){
ne[k] = ne[ne[k]];
}
//打印所有节点
void print(int head){
for(int i = head; i != -1; i = ne[i])
cout << e[i] << ' ';
}
Double Link List
【Data Structure】
【Pattern】
#include<iostream>
#include<string>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N], idx;
/*
这里为了方便,把0设置为第一个节点,把1设置为最后一个节点(这里的0和1不放任何节点)
idx 从2开始出发,这里的物理地址并不代表逻辑地址
r[i]代表i右边的节点,l[i]代表i左边的节点
*/
void init(){
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个节点
void del(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
//打印所有的节点
void print(int r[]){
for(int i = r[0]; i != 1; i = r[i])
cout << e[i] << ' ';
}
Stack
【Pattern】
#include<iostream>
#include<string>
using namespace std;
const int N = 100010;
//栈顶指针为tt,静态地生成数组stk[N]比动态地new数组更加节省时间
int stk[N], tt;
//可以加个初始化stk的操作
void init(){
tt = 0;
}
//向栈顶插入一个元素x
void push(int x){
stk[++ tt] = x;
}
//从栈顶弹出一个元素
void pop(){
tt --;
}
//判断栈是否为空
bool empty(){
if(tt > 0)
return true;
else
return false;
}
//查询栈顶元素
int query(){
return stk[tt];
}
Queue
【Pattern】
#include<iostream>
#include<string>
using namespace std;
const int N = 100010;
//hh是队头,tt是队尾
int q[N], hh, tt;
//hh和tt相等的时候不是空的,是有一个元素的
void init(){
hh = 0;
tt = -1;
}
void push(int x){
q[++ tt] = x;
}
void pop(){
hh ++;
}
bool empty(){
if(hh > tt)
return true;
else
return false;
}
int query(){
return q[hh];
}
Monotonic_Stack | Monotonic_Queue(Stack和Queue中的元素是单调的)
【Pattern】
#include<iostream>
using namespace std;
//数组长度一般定义为常量
const int N = 1000010;
//队列q放的是窗口有用元素的下标,这样是为了随着窗口移动方便弹出有用的窗口元素
//a放的是数组元素
int q[N], a[N], hh, tt;
void init(){
hh = 0;
tt = -1;
}
bool empty(){
if(hh > tt)
return true;
else
return false;
}
int main(){
int n, k;
cin >> n >> k;
//存储输入的元素
for(int i = 1; i<=n; i++) cin >> a[i];
//初始化队列
init();
//随着窗口的移动,窗头元素需剔除,窗尾必须添加一个元素,这都需要在单调队列中更新
for(int i = 1; i <= n; i++){
/*即窗口向右滑,需要将窗口的头元素去除,
此时需要将与窗口头元素相等的队列中的头元素去除掉(更新队列中的有用元素)
*/
if(!empty() && i - k == q[hh])hh ++;
//去除无用的元素
while(!empty() && a[q[tt]] <= a[i])tt --;
//插入有用有用元素
q[++ tt] = i;
//当前窗口是满的,便输出队尾元素
if(i >= k)cout << a[q[hh]] << ' ';
}
return 0;
}
KMP
【Instruction】
1、原理:
2、next数组:
【Pattern】
#include<iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
//p是模式串,s是原始串
char p[N], s[M];
//ne是next数组
int ne[N];
int main(){
//p和s是数组开始的地址,p+1和s+1说明数组从下标1开始输入
cin >> n >> p + 1 >> m >> s + 1;
//ne数组:模式串自身作为原始串查询,应为ne[1]默认为0,因此从i=2开始
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;
}
//i代表的是s的数组下标,j代表的是p的数组下标,需要错开比较,是为了使得ne数组的表示更加方便
for(int i = 1, j = 0; i <= m; i++){
//j不能从0开始并且当前的字符不匹配,j就开始移动到ne[j]位置,循环比较
while(j && s[i] != p[j + 1])j = ne[j];
//要是匹配上,则继续匹配下一个字符
if(s[i] == p[j + 1])j ++;
//要是模式串匹配完成
if(j == n){
//返回的字符串起始值是以0开始的
cout << i - n << ' ';
//原始串中有许多模式串,需要继续比较
j = ne[j];
}
}
return 0;
}
Trie
【Data Structure】
【Pattern】
#include<iostream>
/*其实C++中的标准库中已经包含了scanf和printf函数,
加不加这个头文件都无所谓
*/
#include<cstdio>
using namespace std;
const int N = 100010;
/*N表示的是trie的最大节点数目,
26表示每个字符节点的最大儿子个数,因为是26个字母
cnt[i]表示的是以i节点结尾的字符串的个数,
因为在trie中以i节点结尾只单独代表一个字符串,但是可能有多个重复的存在trie中
idx表示当前分配的节点编号
*/
int son[N][26], cnt[N], idx;
char str[N];
void insert(char s[]){
//从0号节点开始
int p = 0;
//字符串是以0结尾的,直接通过s[i]来判断字符串是否已经读到尾部了
for(int i = 0; s[i]; i ++){
//将字符a-z映射到0-25
int x = s[i] - 'a';
//如果p节点的x儿子的字符不存在,则添加一个新的节点,节点编号为++idx
if(!son[p][x])son[p][x] = ++ idx;
p = son[p][x];
}
//以该字符串结尾的数量加1
cnt[p] ++;
}
int query(char s[]){
int p = 0;
for(int i = 0; s[i]; i++){
int x = s[i] - 'a';
//不存在该字符串,返回0
if(!son[p][x])return 0;
p = son[p][x];
}
return cnt[p];
}
int main(){
int n;
cin >> n;
while(n --){
/*操作数设成长度为2的数组是为了存放多余的空格和换行符
(有些测试样例会多输入,为了避免这种情况,我们只获取真正的操作数即opt[0])
*/
char opt[2];
cin >> opt >> str;
if(opt[0] == 'I')insert(str);
else if(opt[0] == 'Q')cout << query(str) << endl;
}
return 0;
}
Union Find
【Instruction】
Union:将两个集合合并
Find:询问两个元素是否在一个集合当中
原理:每一个集合用树来存储,而树用一维数组来存储。树根的编号就是集合的编号,每个节点存储他的父节点,p[x]表示为x的父节点
问题1:如何判断树根 if(p[x] == x)
问题2:如何求x的集合编号:while(p[x] != x)x = p[x]
问题3:如何合并两个集合:p[x]是x的集合编号,p[y]是y的集合编号。p[x] = y
【Pattern】
#include<iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
//找到编号为x的数所属的集合
int find(int x){
/*当x的父节点不是自己,说明当前节点不是根节点
路径压缩,使得路径上的节点都指向代表集合类别的根节点
*/
if(p[x] != x)p[x] = find(p[x]);//这里使用了递归代替了while
return p[x];
}
int main(){
cin >> n >> m;
//初始化,每个数各自在一个集合里,即每个数都各自作为一个根节点
for(int i = 1; i<=n; i++)
p[i] = i;
while(m --){
int a, b;
char opt[2];
cin >> opt >> a >> b;
if(opt[0] == 'M')p[find(a)] = find(b);
else if(opt[0] == 'Q'){
if(find(a) == find(b))
cout << "Yes";
else
cout << "No";
}
}
return 0;
}
HEAP
【Instruction】以小顶堆为例
- 插入一个数:heap[++ size] = x, up(size);
- 求集合当中的最小值: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);
- 将数组改造成堆:down( n/2 ~ 1);
【Pattern】
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
/*s是堆的大小,在std命名空间下不能使用size这个标识符
ph[i]表示第i个插入的数在堆中的编号,hp[j]表示在堆中编号为j的数是第几个插入的
*/
int h[N], ph[N], hp[N], s;
void heap_swap(int a, int b){
/*首先我们要交换ph,那么ph[]中的下标的含义应该
是标号a、b分别是第几次插入的,因此要用到hp
*/
swap(ph[hp[a]], ph[hp[b]]);
//其次我们要交换hp,hp[]下标的含义就是编号a, b
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u){
int t = u;
//当u的左孩子存在的时候,让t指向u与左孩子的最小值
if(u * 2 <= s && h[u * 2] < h[t])t = u * 2;
//当u的右孩子存在的时候,让t指向u与其左右孩子中的最小值
if(u * 2 + 1 <= s && h[u * 2 + 1] < h[t])t = u * 2 + 1;
//如果最小值不是自己,则需要交换并且递归down下去
if(t != u){
heap_swap(u, t);
down(t);
}
}
void up(int u){
//当u的父节点存在且小于其父节点则交换
while(u / 2 && h[u / 2] > h[u]){
//交换父节点和自己
heap_swap(u / 2, u);
//继续up
u /= 2;
}
}
int main(){
//m表示的是第m个插入的数
int n, m = 0;
cin >> n;
int k, x;
char opt[10];
while(n --){
//输入操作符opt
cin >> opt;
if(!strcmp(opt, "I")){
cin >> x;
m ++;
h[++ s] = x;
//更新ph和hp数组
ph[m] = s;
hp[s] = m;
/***************************************
因为up()中使用过heap_swap,会更改ph和hp数组,
因此up方法必须要在对ph和hp数组操作之后
*/
up(s);
}
else if(!strcmp(opt, "PM"))
cout << h[1] << endl;
else if(!strcmp(opt, "DM")){
//heap_swap顺便把h[1] = h[s]的事情做了
heap_swap(1, s);
s --;
down(1);
}
else if(!strcmp(opt, "D")){
cin >> k;
/*这里一定要用k = ph[k]保存第k个插入点的下标
因为在此处heap_swap操作后ph[k]的值已经发生
如果在up,down操作中仍然使用ph[k]作为参数就会发生错误
*/
k = ph[k];
heap_swap(k, s);
s --;
down(k);
up(k);
}
else if(!strcmp(opt, "C")){
cin >> k >> x;
k = ph[k];
h[k] = x;
down(k);
up(k);
}
}
return 0;
}
Hash(都采用取模的方法确定address)
【拉链法】
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
//h[N]是链表头数组
int h[N], e[N], ne[N], idx;
//插入x
void insert(int x){
//当x是负数的时候,x%N的值是负数,为了将他变成正数,采取下面的做法
int k = (x % N + N) % N;//k是x的地址
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++;
}
//查找x
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(){
int n;
cin >> n;
//一定要初始化链表为空
memset(h, -1, sizeof(h));
while(n --){
int x;
char opt[2];
cin >> opt;
if(opt[0] == 'I'){
cin >> x;
insert(x);
}
else if(opt[0] == 'Q'){
cin >> x;
if(find(x))cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
【散列法】
#include<iostream>
#include<cstring>
using namespace std;
//一般再散列的地址空间开2~3倍就可以了,null设置成无限大即可
const int N = 200020, null = 0x3f3f3f3f;
int h[N];
//这里的find是寻址函数,不是判断x是否在集合中
int find(int x){
int k = (x % N + N) % N;
//当当前地址上的值不是null且也不是x的时候,向下寻址
while(h[k] != null && h[k] != x){
k ++;
//当寻址到最后一个点的时候,说明不存在
if(k == N)k = 0;
}
return k;
}
int main(){
int n;
cin >> n;
//记得初始化
memset(h, 0x3f, sizeof(h));
int x;
char opt[2];
while(n --){
cin >> opt >> x;
//find还可以找到x插入的点在哪
int k = find(x);
if(opt[0] == 'I')h[k] = x;
else if(opt[0] == 'Q'){
if(h[k] != null)cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
【哈希字符串】
#include<iostream>
using namespace std;
typedef unsigned long long ULL;//用来取模的数
const int N = 100010, P = 131;//经证明P取131冲突最小;
int n, m;
char str[N];//用来存储字符串
/*h[i]表示长度为i的字符串前缀的hash值
字符串前缀的hash值是把字符串看作P进制的数,
将其转换成十进制就是它的hash值
每个字符用ASCII码表示
p[i]表示P的i次方
将其定义为ULL类型的值是为了“让溢出表示取模”
*/
ULL h[N], p[N];
//区间在l~r区间的字符串的hash值可以通过h[i]获得
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(int i = 1; i <= n; i ++){
//让p[i]初始化为P的i次方
p[i] = p[i - 1] * P;
//初始化所有的字符串前缀的hash值
h[i] = h[i - 1] * P + str[i];
}
while(m --){
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
//如果这两段字符串的hash值相同,说明这两个字符串相等
if(get(l1, r1) == get(l2, r2))cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}