数据结构
单链表
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx = 1;
//e[]为链表的元素
//ne[]为链表的序号(第几次插入的)
//idx为操作次数,(第一次默认为头结点为0了)
void insert(int k,int x)
{
e[idx] = x, ne[idx] = ne[k],ne[k] = idx ++;
}
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int n;
cin >> n;
int k, x;
char op;
while(n --)
{
cin >> op;
//在表头插入
if(op == 'H'){
cin >> x;
insert(0,x);
}else if(op == 'I'){//在第k个插入的数后面插入一个数
cin >> k >> x;
insert(k,x);//第k个,就是k-1
}else if(op == 'D'){//删除第k个插入的数后面的数 (k=0时删除头结点)
cin >> k;
//if(!k) head = ne[head];//删除头结点
remove(k);//第k个,就是k-1
}
}
for(int i = ne[0]; i; i = ne[i]){
cout << e[i] << ' ';
}
cout << endl;
return 0;
}
双链表
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int e[N],l[N],r[N],idx;
//e[]为链表的节点
//l[i]为链表的左序下标数组 <--- i节点的左元素
//r[i]为链表的右序下标数组 ---> i节点的右元素
//idx为操作次数
void init()
{
r[0] = 1, l[1] = 0;//第一个点的右边是 1 第二个点的左边是 0
idx = 2;//idx 此时已经用掉两个点
// 此时的链表: 0 1,
//若干次操作后 0 ......... 1
}
void insert(int k,int x)
{
e[idx] = x;//创建新结点
//把新元素伸向左右的创建好
r[idx] = r[k];//idx的右元素是没插入时k的右元素(下标)
l[idx] = k;//左元素的下标是k
//把左右元素指向自己
l[r[k]] = idx;//让右侧指向新元素
r[k] = idx ++;//左侧指向新元素
}
//删除k节点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
init();
while(n --)
{
string op;
cin >> op;
int k, x;
if(op == "L"){//在链表的最左端插入x
cin >> x;
insert(0,x);
}else if(op == "R"){//在链表的最右端插入x
cin >> x;
insert(l[1],x);
}else if(op == "D"){//将第k个插入的数后面的数删除
cin >> k;
remove(k + 1);//初始值就是2了
}else if(op == "IL"){//表示在第k个插入的数左侧插入一个数
cin >> k >> x;
insert(l[k + 1], x);
}else if(op == "IR"){//表示在第k个插入的数右侧插入一个数
cin >> k >> x;
insert(k + 1, x);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
模拟栈
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
//ios::sync_with_stdio(0); cin.tie(0);
int n,idx = 0;
cin >> n;
while(n --)
{
string op;
cin >> op;
if(op == "push"){
int x;
cin >> x;
a[++ idx] = x;//此处的++idx 比 idx ++ 好用
}else if(op == "pop"){
idx --;
}else if(op == "empty"){
if(!idx) cout << "YES" << endl;
else cout << "NO" << endl;
}else if(op == "query"){
cout << a[idx] << endl;
}
//cout << "idx = " << idx << endl;
}
return 0;
}
模拟队列
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
int i = 0, j = 0, n;
cin >> n;
string op;
while(n --)
{
cin >> op;
if(op == "push"){
cin >> a[++ j] ;
}else if(op == "pop"){
++ i;
}else if(op == "empty"){
if(i == j) cout << "YES" << endl;
else cout << "NO" << endl;
}else if(op == "query"){
cout << a[i + 1] << endl;
}
}
return 0;
}
单调栈
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
//ios::sync_with_stdio(0); cin.tie(0);
int n;
cin >> n;
int i = 0;
while(n --)
{
int x;
cin >> x;
while(x <= a[i] && i > 0) i --;
if(!i) cout << "-1 ";
else cout << a[i] << ' ';
a[++ i] = x;
}
return 0;
}
滑动窗口
思路: 最大值和最小值分开做,两次代码几乎相同
-
解决队首已经出窗口的问题;
-
解决队尾与当前元素a[i]不满足单调性的问题;
-
将当前元素下标加入队尾;
-
如果满足条件则输出结果;
需要注意的细节:
- 上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
- 队列中存的是原数组的下标,取值时要再套一层,a[q[]];
- 算最大值前注意将hh和tt重置;
- hh从0开始,数组下标也要从0开始。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int a[N],q[N];//a是存储原数组,q存储窗口中的元素的 下标
int n, k;
int main()
{
ios::sync_with_stdio(0); cin.tie(0);
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(i - k + 1 > q[hh] ) ++ hh;//若队首滑出窗口,hh++
while(hh <= tt && a[i] <= a[q[tt]]) -- tt;//若队尾不单调,tt--
q[++ tt] = i;//下标加到队尾
if(i + 1 >= k) cout << a[q[hh]] << ' ';//满足窗口内有k个元素了,就该输出了
}
cout << endl;
hh = 0, tt = -1;
for(int i = 0; i < n; i ++){
if(i - k + 1 > q[hh]) ++ hh;
while(hh <= tt && a[i] >= a[q[tt]]) -- tt;
q[++ tt] = i;
if(i + 1 >= k) cout << a[q[hh]] << ' ';
}
return 0;
}
Trie字符串统计
Trie:高效的存储和查找字符串集合的数据结构
编号为 u
的节点指向了编号为 v
的节点,边对应了字母 c
,反映到 son 数组中就是:
son[u][c] = v;
变量解释:
//cnt[]:以当前节点为结尾的字符串有多少个
//son[i][j] = a: 在i后面的字符是a
//idx:idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标
#include<iostream>
using namespace std;
const int N = 100010;
int son[N][26],cnt[N],idx;
string str;
void insert(string 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(string 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()
{
int n;
cin >> n;
while(n --)
{
string op,str;
cin >> op >> str;
if(op == "I"){
insert(str);
}else if(op == "Q"){
int a = query(str);
cout << a << endl;
}
}
return 0;
}
并查集 - 合并集合
并查集的用处:
- 合并集合
- 查询两个元素是否在同一集合当中
基本原理:每个集合用一棵树来表示。树的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点
操作:
- 判断树根 if(p[x] = x)
- 求x的集合编号 while(p[x] != x) x = p[x]
- 合并两个集合,这两将x的根节点嫁接到y的根节点, px为x的根节点, py为y的根节点,嫁接p[px] = py
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int find(int k) //返回k的祖宗节点 + 路径压缩
{
if(k != p[k]){ //如果这个元素不是祖宗节点
p[k] = find(p[k]);
}
return p[k];
}
int main()
{
scanf("%d%d", &n, &m);
//从1开始!
for(int i = 1; i <= n; i ++) p[i] = i;//祖宗节点是它本身
while(m --)
{
char op[2];
int a,b;
scanf("%s%d%d",op, &a, &b);
//合并
if(op[0] == 'M')
{
p[find(a)] = find(b);
}
else if(op[0] == 'Q')
{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
模拟堆
堆是一个完全二叉树,
小根堆
:根节点
是整个堆中的最小值
; 大根堆
:根节点
是整个堆中的最大值
主要操作是简单的down和up,复杂点的是有修改操作的(涉及到第几次插入,需要单独开辟数组存储)
主要变量注释:
- h[x] 表示树中位置 x 的元素
- ph[k] = x 表示第 k 个插入的元素在树中存放的位置 x
- 此时如果要交换 ph 中的两个元素需要知道树中位置 x 是第几个被插入的, 于是便引入了数组 hp
- hp[x] = k 表示树中位置 x 存放的为第 k 个插入的元素
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int h[N],ph[N],hp[N],idx;
//h:维护的堆
//ph: 从下标找到堆中元素
//hp: 从堆中的元素找到其下标 这俩主要是查找第几个插入的数
//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]);
}
void down(int u)
{
int t = u;
if(u * 2 <= idx && h[u * 2] < h[t]) t = u * 2;
if(u * 2 + 1 <= idx && 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()
{
//ios::sync_with_stdio(0); cin.tie(0);
int n, m = 0;
cin >> n;
while(n --)
{
string op;
int k, x;
cin >> op;
if(op == "I")
{
cin >> x;
m ++;
idx ++;
ph[m] = idx, hp[idx] = m;
h[idx] = x;
down(idx), up(idx);
}
else if(op == "PM") cout << h[1] << endl;
else if(op == "DM")
{
heap_swap(1,idx);
idx --;
down(1);
}else if(op == "D")
{
cin >> k;
k = ph[k];//从ph中找到第k次插入的数在堆中的下标
heap_swap(k,idx);
idx --;
down(k), up(k);
}else {
cin >> k >> x;
k = ph[k];//得到第k个数的位置
h[k] = x;//修改在堆中的元素
down(k), up(k);
}
}
return 0;
}
模拟散列表
哈希方法: k = (x % N + N) % N
,N为常数,先模再加是为了防止爆int
-
开放寻址法:先找到一个坑,如果坑里没人就进这个坑;如果有人就继续向后找坑
一个长度为题目要求
2~3倍
的一维数组,通过循环找到哈希值k
的位置,(如果哈希值冲突的情况位置也会冲突,那么k继续向后找位置)while(e[k] != null && e[k] != x){ //不为空 && 不是k的位置存的不是x k ++;//继续向后找位置 if(k == N) k = 0;//到末尾从头再来 }
-
拉链法:找到一个坑.不管有人没人都进去,有人就去坑里排队
开辟一个一维数组,并且每个一维数组都开出两个数组(共同组成链表)
#include<bits/stdc++.h>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
int find(int x)
{
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x){
if(k == N) k = 0;
k ++;
}
return k;
}
int main()
{
//ios::sync_with_stdio(0); cin.tie(0);
memset(h,0x3f,sizeof h);//初始化
int n;
cin >> n;
while(n --)
{
char op;
int x;
cin >> op >> x;
if(op == 'I')
{
int t = find(x);
h[t] = x;
}else{
if(h[find(x)] == x) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
字符串哈希
把所有的字符都变成p进制数字(哈希值),实现不同的字符串映射到不同的数字
前缀和公式: h[i] = h[i - 1] * P + s[i]
h为前缀和数组,P为进制,s为字符串数组
区间和公式:h[l,r] = h[r] - h[l - 1] * P^(r - l + 1)
可以p数组存储P的几次方
注意:
- p[0] = 1,不能为0,否则假设
hash(A) = 0
,则hash(AA)==0
也是成立的,无法区分 - P 一般为
131
or13331
,经验值
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;// or p = 13331 经验值
int n,m;
char s[N];
ULL h[N],p[N];
ULL query(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
cin >> n >> m;
p[0] = 1;//一定要注意,hash值不能为0,否则A AA AAA 的hash值都是0
h[0] = 0;
for(int i = 1; i <= n; i ++) cin >> s[i];
for(int i = 1; i <= n; i ++)
{
p[i] = p[i - 1] * P;//存储p的几次方,便于计算区间和
h[i] = h[i - 1] * P + s[i];//计算前缀和
}
while(m --)
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if(query(l1,r1) == query(l2,r2)) cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}