数据结构
单链表
const int N = 100010;
int head;//头结点下标
int e[N];//e[i]表示结点i的值
int ne[N];//结点i的下一个指针
int 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 ++;
}
//删除头结点
void remove_head()
{
head = ne[head];
}
//删除下标为k后的元素
void remove(int k)
{
ne[k] = ne[ne[k]];
}
双链表
//e[]表示值,r[]表示右结点,l[]表示左结点,idx表示用到了哪个结点
const int N = 100010;
int e[N], r[N], l[N], idx;
//初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1;
l[1] = 0;
idx = 2;//0和1用了两个位置
}
//在k右边插入x
void add(int k, int x)//k左边插入为 add(l[k], x);
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx ++;
}
//删除k
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
栈
const int N = 100010;
//tt表示栈顶
int stk[N], tt = 0;
//入栈
stk[++ tt];
//出栈
tt --;
//栈顶值
stk[tt];
//判断栈是否为空
if(tt > 0) not empty;
else empty;
队列
const int N = 100010;
//tt代表队尾,hh代表队首
int q[N], tt = -1, hh = 0;
//x入队
q[++ tt] = x;
//出队
hh ++;
//队头值
q[hh];
//队尾值
q[tt];
//判断是否为空
if(hh <= tt) not empty;
else empty;
单调栈
//找出每个数左边离它最近的比它小/大的数
const int N = 100010;
int stk[N], tt = 0;
for(int i = 0; i < n; i ++)
{
while(tt && stk[tt] >= x)//栈不为空且栈顶大于等于x 大/小 改符号<
t -- //就出栈
if(tt) cout << stk[tt];
stk[ ++ tt] = x;
}
单调队列
思路:
最小值和最大值分开来做,两个for循环完全类似,都做以下四步:
1.解决队首已经出窗口的问题;
2.解决队尾与当前元素a[i]不满足单调性的问题;
3.将当前元素下标加入队尾;
4.如果满足条件则输出结果;
需要注意的细节:
上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
1.队列中存的是原数组的下标,取值时要再套一层,a[q[]];
2. 算最大值前注意将hh和tt重置;
3.此题用cout会超时,只能用printf;
4.hh从0开始,数组下标也要从0开始。
#include<iostream>
using namespace std;
const int N = 1000010;
int a[N], q[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, k, hh = 0, tt = -1;
cin >> n >> k;
for(int i = 0; i < n; i ++)
{
cin >> a[i];
if(hh <= tt && i - k + 1 > q[hh]) hh ++;//若队首出窗口 hh+1
while(hh <= tt && a[i] <= a[q[tt]]) 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 ++)
{
cin >> a[i];
if(hh <= tt && i - k + 1 > q[hh]) hh ++;
while(hh <= tt && a[i] >= a[q[tt]]) tt --;
q[++ tt] = i;
if(i >= k - 1) cout << a[q[hh]] << " ";
}
return 0;
}
KMP算法匹配字符串
//s[]为长串,p[]为匹配串,n为p[]长度,m为p[]长度
char s[M], p[N];
int n, m;
int ne[N];
int main()
{
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 ++)
{
while(j && s[i] != p[j + 1]) j = ne[j];
if(s[i] == p[j + 1]) j ++;
if(j == n)
{
//匹配成功
//具体逻辑
j = ne[j];
}
}
return 0;
}
Trie树(字典树)
高效的存储和查找字符串集合的数据结构
const int N = 100010;
int son[N][26];//存储树中的每一个节点的子节点
int cnt[N];//存储以每个节点结尾的单词数量
int idx;
char str[N];
//插入
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];
}
并查集
并查集:
1.将两个集合合并
2.询问两个元素是否在一个集合当中
时间复杂度近乎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;
-
朴素并查集
int p[N];//存储每个节点的祖宗节点 //返回祖宗节点 + 路径压缩 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);
-
维护Size的并查集
int p[N],Size[N];//Size[]只有祖宗节点才有意义,表示此祖宗节点所在集合有多少节点 //返回祖宗节点 + 压缩路径 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集合合并 将a树插到b根上 Size[find(b)] += Size[find(a)];//顺序不能变,先加后合并 p[find(a)] = find(b);
-
维护
维护到祖宗节点距离的并查集: 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)的偏移量
堆
const int N = 100010;
int h[N], Size;
void down(int u)
{
int t = u;
if(u * 2 <= Size && h[u * 2] < h[t]) t = u * 2;//左儿子存在且左儿子小于父亲,t= 左儿子
if(u * 2 + 1 <= Size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//右儿子存在且右儿子小于父亲,t= 右儿子
if(u != t)//如果根节点不是最小
{
swap(h[u], h[t]);//交换
down(t);//递归处理
}
}
void up(int u)
{
while(u / 2 && h[u / 2] < h[u])//有父节点且父节点大于子节点
{
swap(h[u / 2], h[u]);
u /= 2;
}
}
//初始化预处理堆O(n)
for(int i = n / 2; i >= 0; i ++)
down(i);
//堆排序
#include<iostream>
using namespace std;
const int N = 100010;
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()
{
int n, m;
cin >> n >> m;
Size = n;
for(int i = 1; i <= n; i ++)
cin >> h[i];
for(int i = n / 2; i ; i --)
down(i);
while(m --)
{
cout << h[1] << " ";
h[1] = h[Size];
Size --;
down(1);
}
return 0;
}
哈希表
在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:
0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。
//**拉链法**
const int N = 100010;
int e[N], ne[N], h[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;
}
#include<cstring>
memset(h, -1, sizeof h);//初始哈希表全赋值-1
//**开放寻址法
const int N = 200010, NULL = 0x3f3f3f3f;//N开2~3倍
int h[N];
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;
}
memset(h, 0x3f, sizeof h);
字符串哈希(懵的批爆)
核心思想:将字符串看成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];
}
作用:查找两端之间内的字符串是否相等
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
ULL h[N], p[N];
char str[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
int n, m;
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;
}