由于动态链表new一个新空间的时候耗时较长,因此写算法题时,尽量用数组模拟链表,即静态链表,不需要new,更快速,可以节约很多时间;
const int N=1e8;
int e[N]; //存储节点的值
int ne[N]; //存储节点的next指针
int idx; //表示当前用到了哪个节点
int head; //head存储链表头
void init(void) //初始化
{
idx=0;
head=-1;
}
void insert(int x) //在链表头插入数x(即在链表的最开头插入x)
{
e[idx] = x;
ne[idx] = head;
head = idx++;
}
void add(int k,int x) //在节点后插入数x
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
void del(int k) //删除第k个节点后的结点
{
ne[k] = ne[ne[k]];
}
void remove(void) //将当前头结点删除
{
head = ne[head];
}
模板题
使单个节点具有左右两个next指针;
记得main函数中引入初始化!!!
const int N=1e5+10;
int e[N];
int l[N]; //当前节点的左next指针
int r[N]; //当前节点的右next指针
int idx;
void init(void)
{
l[1] = 0; //初始时使得0号节点为头节点,1号节点为尾节点,都不存数值,只是作为链表的起点和终点形式
r[0] = 1;
idx = 2;
}
void insert(int k,int x) //在k号节点右边插上数x
{
e[idx] = x;
l[idx] = k; //建立插入节点与左右节点关系
r[idx] = r[k];
l[r[k]] = idx; //修改左右节点指向,与下一行顺序不能反,否则l[r[k]]会变化
r[k] = idx++;
}
void remove(int k) //删除k号节点
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
模板题
一种后进先出的数据结构;
数组模拟栈:需要一个数组和一个栈顶top指针;
int st[N];
int top=0; //栈顶指针
st[++top] = x; //入栈
--top; //出栈
if(!top) empty; //判断是否为空
else not_empty;
st[top]; //访问栈顶元素
模板题
栈中元素大小如下图呈现单调增加或单调减少的趋势;
应用时先用栈按照暴力模拟做法解出题目,然后去掉其中无用的元素,再分析其中的单调性,若剩下元素有单调性,则进行优化;优化后取极值或最值即取两边端点值,查找值可用二分;
一般需要用单调栈的问题就是求一个数左边第一个比它小的数,或者右边第一个比它大的数这种类似的问题;
给定一个长度为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
using namespace std;
const int N=1e5+10;
int s[N];
int top;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0); //加速cin,cout读入输出
int n;
cin >> n;
int x;
for(int i=0;i<n;++i){
cin >> x; //每读入一个数,就处理一次
while(top && s[top] >= x){ //如果在s[top]前面有比s[top]更大的数s[y],则s[y]不可能会被作为答案,因为s[top]离x更近,所以s[y]可以被清除
--top; //该循环后得到的结果是栈为空,或者x前面的数都比x小;而最后得到的结果是栈内的数呈现单调递增的趋势
}
if(!top) //如果栈是空的,说明x前面没有比它小的数了
cout << "-1 ";
else
cout << s[top] << " ";
s[++top] = x; //将当前的x入栈
}
cout << endl;
return 0;
}
一种先进先出的数据结构;
数组模拟队列:需要一个数组和两个指针,其中一个是队头指针,另一个是队尾指针;
int q[N];
int head; //队头指针
int tail; //队尾指针
q[tail ++] = x; //入队,队尾插入
++ head; //出队,队头弹出
if(head == tail) empty; //若首尾指针相遇,则队列为空
else not_empty;
q[head]; //队头元素
队列中元素大小规律与单调栈类似;
应用时先用队列按照暴力模拟做法解出题目,然后去掉其中无用的元素,再分析其中的单调性,若剩下元素有单调性,则进行优化;优化后取极值或最值即取两边端点值,查找值可用二分;
一般用单调队列的就是窗口滑动类似的问题;
典型问题:窗口滑动
给定一个大小为 n ≤ 1 0 6 n≤10^6 n≤106的数组。 有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。 您只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。 您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式 输入包含两行。
第一行包含两个整数n和k,分别代表数组长度和滑动窗口的长度。
第二行有n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式 输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
题目链接
参考题目解析:
以样例为例;
我们用q来表示单调队列,p来表示其所对应的在原列表里的序号。
由于此时队中没有一个元素,我们直接令1进队。此时,q={1},p={1}。
现在3面临着抉择。下面基于这样一个思想:假如把3放进去,如果后面2个数都比它大,那么3在其有生之年就有可能成为最小的。此时,q={1,3},p={1,2}
下面出现了-1。队尾元素3比-1大,那么意味着只要-1进队,那么3在其有生之年必定成为不了最小值,原因很明显:因为当下面3被框起来,那么-1也一定被框起来,所以3永远不能当最小值。所以,3从队尾出队。同理,1从队尾出队。最后-1进队,此时q={-1},p={3}
出现-3,同上面分析,-1>-3,-1从队尾出队,-3从队尾进队。q={-3},p={4}。
出现5,因为5>-3,同第二条分析,5在有生之年还是有希望的,所以5进队。此时,q={-3,5},p={4,5}
出现3。3先与队尾的5比较,3<5,按照第3条的分析,5从队尾出队。3再与-3比较,同第二条分析,3进队。此时,q={-3,3},p={4,6}
出现6。6与3比较,因为3<6,所以3不必出队。由于3以前元素都<3,所以不必再比较,6进队。因为-3此时已经在滑动窗口之外,所以-3从队首出队。此时,q={3,6},p={6,7}
出现7。队尾元素6小于7,7进队。此时,q={3,6,7},p={6,7,8}。
那么,我们对单调队列的基本操作已经分析完毕。因为单调队列中元素大小单调递*(增/减/自定义比较),
因此,队首元素必定是最值。按题意输出即可。
作者:Tyouchie
链接:https://www.acwing.com/solution/content/2499/
#include
using namespace std;
const int N=1e6+10;
int q[N]; //单调队列,但储存的并不是数,而是数组下标,即位置,再通过a[q[i]]得到值
int a[N]; //储存读入的数
int main()
{
ios::sync_with_stdio(false); //加速读入
cin.tie(0);
cout.tie(0);
int n,k;
cin >> n >> k;
for(int i = 0; i < n; ++ i) //滑动窗口,单调队列
cin >> a[i];
int head = 0,tail = -1; //head要设为窗口最开始即为0吧,且为了保持统一,不特判,因为后面有元素入队操作q[++tail] = i,所以tail开始置为-1,++之后就到窗口最开始即0了吧
//类比单调栈,那里是top为0为空栈,而在这里是head>tail为空队列,head<=tail则队列中有元素,开始时head为0,那么tail就会被置为-1
for(int i=0; i<n; ++i){ //全部读入后一个个对数组元素进行处理
if(head <= tail && i-q[head]+1 > k) //当队列不为空即head<=tail 且当前队列中的长度(i为当前元素,即滑动窗口的最右端,q[head]为滑动窗口的最左端,窗口长度为i-q[head]+1)大于题目中给出的窗口长度k时
++head; //队头元素(窗口最左端元素)出队
while(head <= tail && a[q[tail]] >= a[i]) //当队列不为空,且当前处理元素a[i]更小时,因为a[i]左边比a[i]更大的值a[max](即a[tail]) 与a[i]在同一个窗口时,(且a[i]在a[tail]右侧,所以a[i]存在时间比a[tail]的更长) a[tail]不可能会被选中,所以去除a[tail],即使得a[tail]出队
-- tail; //队尾元素(窗口最右端元素)出队,特殊情况,非正常队列出队
q[++ tail] = i; //将当前处理元素i(a[i])入队
if(i+1 >= k) //因为队列中的元素是一个个增加,逐个处理的,而很明显只有当滑动窗口内(不算上已清除的)元素个数满足k时,才会输出最小值,比如总共8个元素,k=3,所以会输出8-3+1=6个元素,就是因为刚开始时处理窗口元素个数过少,不会输出元素
cout << a[q[head]] << " "; //由于队列(即滑动窗口)中元素为单调递增的(单调队列+处理的性质),所以head,即窗口最左端元素为最小值
}
cout << endl;
head = 0,tail = -1; //输出最大值,与上述类似
for(int i = 0; i < n; ++i){
if(head <= tail && i-q[head]+1 > k)
++head;
while(head <= tail && a[q[tail]] <= a[i]) //这里改为若队列元素小于等于当前处理元素就出队
--tail;
q[++ tail] = i;
if(i + 1 >= k)
cout << a[q[head]] << endl;
}
cout << endl;
return 0;
}
KMP算法真的难,多思考思考吧;
可求循环节;
关键是模板串p与模板串s匹配的时候,p的移动并不是暴力做法那样一位位移动,而是跳着移动,即利用已经匹配好的部分same,在s后面未匹配的部分中寻找与same相同的部分,再跳到该部分,如p=abc,s=abdabfabdabc,此时p与s中ab是相同的,即相同部分same=ab,所以在s中找ab即可,具体实现是由next数组完成,而next数组是通过p与自身匹配后得出的,实际上next数组的得出与kmp匹配类似,不同的是前者为p与p匹配,后者p与s匹配;匹配过程中s与p中指针j的物理位置是不变的,j= next[j],实际上是整个模板串p的向右移动,移动到具有s中相同部分same处,而这样相对来看就是j向左移动了。且实际上j只能在p上面移动,即j的移动范围是0~n,(初始为0,n为p的长度),且j之前的都是匹配成功的,所以匹配失败j就向前移动,若无法匹配则j=0,匹配成功j就在p上++,即向右移动,若j移动到p末尾n的位置,就说明整个模板串p都成功匹配了;
但是强也是真的强,时间复杂度为O(n+m),即O(n);
给定一个模式串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
using namespace std;
const int N = 1e5+10;
const int M = 1e6+10;
char s[M]; //模式串
char p[N]; //模板串
int ne[N]; //next有时会与头文件冲突,所以用ne更好,next[i]表示子串s[1,2,,,,,,i-1,i]的最长相等前后缀的前缀最后一位下标,或者说是子串的最长相等前后缀的长度,因为我们是从下标1开始的,这也体现出了从1开始的好处
int main()
{
ios::sync_with_stdio(false); //cin,cout加速
cin.tie(0);// **i一直向右**移动,作为**后缀**;而j经常会跳回去,即**j回退**,很容易回退到0即从头开始匹配**前缀**,但如果p前面出现abab···,当p[1~4]与s[1~4]满足匹配,j=4,但p[4+1]不满足匹配时,j回退,但此时不必回退到0,回退到p[2]即可,因为p[1],p[2]分别与p[3],p[4]相等,又因为匹配时是j+1进行匹配(p[j+1]=p[i]),所以p从第2个a开始匹配前缀就可以了,即j=next[j]
cout.tie(0);
int n,m;
int pos;
cin >> n >> p+1 >> m >> s+1; //直接用cin读入字符串p和s,由于p和s都是从1开始的,所以是p+1和s+1
for(int i = 2,j = 0; i <= n; ++i){ //求ne数组,本质是模板串p与自身的匹配,因为ne[1] = 0,所以从2开始,其中p[i]相当于p的后缀,p[j+1]相当于p的前缀,最好是debug一遍就容易理解了,p:abcdeabcgabcd
while(j && p[i] != p[j+1]) //匹配失败,j回退,即将模板串向右移动
j = ne[j];
if(p[i] == p[j+1]) //如果该位匹配成功,j就向前移动一位即使得j指向该位置
++j;
ne[i] = j; //更新next,每次移动i前,将已经匹配成功的长度记录到next中,由于j为模板串中的1~j的最后一位,所以j即为最大相同后缀(或前缀)长度,再把j与下标i对应
}
for(int i = 1,j = 0; i <= m; ++i){ //由于是s[i] == p[j+1],s和p都是从1开始,所以int i = 1,j = 0
while(j && s[i] != p[j+1]) //不匹配则回退,即将模板串p向右移动,可以看成是s和j指针的物理位置都不变化,只有p向右移动j-ne[j]+1位
j = ne[j];
if(s[i] == p[j+1]) //匹配成功则j往前走一步,注意j是在p上面移动的,j从0开始走,直到走到模板串p的末尾,即匹配成功,j移动的位置即为成功匹配的位置
++j;
if(j == n){ //因为j在p上移动的位置就是p与s成功匹配的位置,所以当j移动到n时,即整个模板串p都已经成功匹配了
pos = i - n; //i为当前p匹配成功时,s的对应元素位置,又因为模板串p长度为n,且答案要求数组s是从0开始计算匹配位置,所以起点为(i-n+1)-1=i-n
cout << pos << " "; //输出成功匹配后s对应元素的起点位置
j = ne[j]; //继续移动j或者说模板串p,看之后的s中还有没有与p完全匹配的
}
}
cout << endl;
return 0;
}
模板题链接
解释一下下标从1开始的好处,首先KMP算法的核心就是要理解next数组的含义。
当下标从0开始时,next[i]表示子串s[0,i]的最长相等前后缀的前缀最后一位的下标,如果我们要求出这个子串的最长相等前后缀的长度时,需要next[i]+1;
当下标从1开始时,next[i]依然表示子串s[1,i]的最长相等前后缀的前缀最后一位的下标,而且next[i]就是这个子串的最长相等前后缀的长度(这也是next[i]数组的另一层含义),不需要我们再去人为加1了。
从另一个角度来讲,下标从0开始时,next[i]=-1,表示我们找不到相等的前后缀。如果下标从1开始,next[i]=0,表示最长相等前后缀的长度为0,也就是说没有相等的前后缀,显然后者更符合我们的一般思路。
所以推荐大家最好从下标为1开始输入。
参考文献 胡凡-算法笔记
时间复杂度分析:
首先,在每次i++的过程中,j最多只会增加一次,因此j总共最多增加M次,而while循环中j总共的回退次数不可能超过它增加的次数,因此while循环中j最多回退M次,所以 KMP 匹配过程时间复杂度为 O(M)。Next[ ]求解时间复杂度为 O(N)。
因此,整个算法的总时间复杂度为 O(M+N)
参考题解1
参考题解2
参考题解3
Trie是一种高效地存储和查找字符串集合的数据结构(一颗树);
模板题链接
#include
using namespace std;
const int N = 1e5 + 10;
int cnt[N]; //记录字符串出现的次数
int son[N][26]; //表示当前节点的儿子,且一个节点最多有26个子节点,son[1][0]=2表示1号节点的值为a的子节点为2号节点,son[x][0]为x的第一个儿子,son[x][1]为x的第二个儿子
int idx; //现在是第几个节点,idx=0表示根节点或空节点
char str[N];
void insert(char str[])
{
int p = 0; //初始化,开始是根节点
for(int i = 0; str[i] ; ++i){
int u = str[i] - 'a'; //将a~z映射成0~25
if(!son[p][u]) //如果当前无该字母,即对应的son[p][u]=0,则把该字母安照编号次序加进去
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()
{
// ios::sync_with_stdio(false); //加了个这东西反而错了???????
// cin.tie(0);
// cout.tie(0);
char op;
int n;
cin >> n;
while(n--){
cin >> op >> str;
if(op == 'I')
insert(str);
else
cout << query(str) << endl;
}
return 0;
}
数值型Trie数题
#include
#include
using namespace std;
const int N = 1e5 + 10 , M = 31 * N;
int a[N];
int idx; //节点次序
int son[M][2]; //整数个数不大于1e5,每个整数占据31个点,所以总共的点数需要31*N个
void insert(int a)
{
int p = 0; //开始时p指针指向根节点
int k; //每个节点最多有0或1两个子节点而一条路径上恰好有31个节点,即根节点沿着一条路径出发到叶节点,其中有31个节点
for(int i = 30; i >= 0; --i){ //由于是从高位向低位(位数越高优先级越高,因为求得的值会最大)依次取31位上对应的0或1,所以开始时i = 30
k = a >> i & 1; //取二进制0或1操作
if(!son[p][k]) //如果当前无该二进制数分支
son[p][k] = ++idx; //则新建一个子节点
p = son[p][k]; //p指针指向下一个节点
}
}
int query(int a)
{
int p = 0;
int k;
int res = 0;
for(int i = 30; i >= 0; --i){
k = a >> i & 1;
if(son[p][!k]){ //如果存在于a[i]这一位不同的,就沿着这往下走,这样异或一定是较大的
p = son[p][!k]; //p指针下移
res = (res << 1) + 1;//答案res此时左移一位,再加上该第i位对应的1,;用<<或>>的时候打括号,不然会发生优先级的错误
}
else {
p = son[p][k];
res = (res << 1) + 0;//没有!a[i]也要左移一位
}
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for(int i = 0; i < n; ++i){ //先将所有数 分解为二进制形式后存入Trie树中
cin >> a[i];
insert(a[i]);
}
int res = 0;
for(int i = 0; i < n; ++i){
res = max(res , query(a[i])); //依次开始查询
}
cout << res << endl;
return 0;
}
作用:
1.合并两个集合;
2.询问两个元素是否在同一个集合当中;基本原理:
1.用一棵树来表示一个集合;
2.树根的编号就是整个集合的编号;
3.每个节点存储它的父节点,p[x]就是x的父节点,根节点的父节点就是它本身;
实现细节:
if(p[x] == x) True; //1.判断树根
int find(int x)//2.求x的集合编号,即通过递归结合路径压缩求对应根节点
{
if(p[x] != x)
p[x] = find(p[x]);
return p[x];
}
p[y] = x;//3.合并两个集合,如让集合y成为集合x的子树
并查集优化方法: 路径压缩
还可通过添加其他变量来维护集合某些性质,如添加一个数组记录集合中节点数量可以维护集合内部节点数;
模板题
#include
using namespace std;
const int N = 1e5 + 10;
int cnt[N]; //动态统计连通块中的节点的数量,开始用size[N]报错???可能和next一样
int p[N]; //父节点
int find(int x) //寻找根节点
{
if(p[x] != x) //根节点特点是p[x] == x
p[x] = find(p[x]); //关键步骤,寻找根节点+路径压缩,递归实现,若没找到则一直令p[x] = find(p[x])即一直向上找
return p[x];//直到找到根节点,且由于p为全局数组,当第一遍递归实现后,一个集合中所有子节点都直接与根节点相连(即路径压缩),这样的话之后就不用再递归寻找根节点了,可以以O(1)的时间找到根节点
}
int main()
{
int n,m;
char op[2];
int a,b;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; ++i) {//初始化p和cnt,开始时每个节点都为根节点,每个集合里只有一个元素,1到n
p[i] = i;
cnt[i] = 1;
}
while(m--){
scanf("%s",op);
if(op[0] == 'C'){
scanf("%d%d",&a,&b);
a = find(a),b = find(b);//寻找a,b各自根节点
if(a != b) { //如果a,b不在同一个集合中
p[a] = b; //合并a,b
cnt[b] += cnt[a]; //将a的数量添加到b中
}
}
else if(op[1] == '1'){
scanf("%d%d",&a,&b);
if(find(a) == find(b)) //根据所属集合编号即所属根节点来判断是否在同一个集合中
puts("Yes");
else
puts("No");
}
else{
scanf("%d",&a);
printf("%d\n",cnt[find(a)]);
}
}
return 0;
}
较复杂模板题链接
功能:
1.插入一个数;
2.求集合当中的最值(大根堆,小根堆);
3.删除最值;
4.删除任意一个元素;//4,5在STL中只能间接实现,不能直接实现
5.修改任意一个元素;
简单无映射堆:
#include
#include
using namespace std;
const int N = 1e5 + 10; //堆的话下标从1开始,方便处理
int heap[N]; //堆数组
int n,m;
int size; //堆大小
void down(int u)
{
int t = u;
if(2*u <= size && heap[2*u] < heap[t]) //当左儿子存在且更小
t = 2*u; //更新当前t
if(2*u+1 <= size && heap[2*u+1] < heap[t]) //当右儿子存在且更小
t =2*u+1;
if(t != u){ //若出现儿子值更小的情况
swap(heap[t],heap[u]); //更新交换两节点
down(t); //继续看下一层儿子节点是不是比当层节点更小,一直递归
}
}
void up(int u)
{
while(u/2 && heap[u/2] > heap[u]){ //看父节点是否存在且更小
swap(heap[u],heap[u/2]); //更新
u /= 2;
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
size = n;
for(int i = 1; i <= n; ++i)
cin >> heap[i];
for(int i = n/2; i >=1; --i){ //从n/2即最后一个有子节点的节点开始建堆,向左向上依次down
down(i);
}
while(m--){//堆排序
cout << heap[1] << " "; //根节点即为最小值
heap[1] = heap[size--]; //直接用最后一个节点的值覆盖根节点的,再size--
down(1); //重新down,调整堆
}
cout << endl;
return 0;
}
较复杂有映射堆
模板题链接
若全局数组值作为函数参数,可能会在函数中把本来不该改变的全局数组值给修改掉,因此不能直接传全局数组值,因此提前设置一个变量,使其代替该全局数组值;
#include
#include //在有映射的堆中,调整节点位置都是通过swap_heap完成的,因为要考虑到hp和ph的映射关系
using namespace std;
const int N = 1e5 + 10; //堆的话下标从1开始,方便处理
int heap[N]; //堆数组
int n;
int cnt; //记录插入次序
int hp[N]; //heap point 由堆中节点对应的插入第k次次序,由于题目涉及第k次插入的数的求解,所以需要用两个数组记录第k次插入次序以及对应的在堆中节点的下标
int ph[N]; //point heap 第k次插入对应在堆中的节点下标,ph与hp互为反函数,即ph[k] = x,hp[x] = k
int length; //堆大小
void swap_heap(int a,int b)//给的a,b是堆中节点下标,hp,ph都为堆节点下标与次序的映射
{//不仅要交换值,也要调整映射关系,否则会出现混乱
swap(ph[hp[a]],ph[hp[b]]); //由于ph中参数为次序,所以先要把下标a,b转换成hp[a],hp[b]
swap(hp[a],hp[b]); //双向都要交换,上面交换下标,这里交换次序
swap(heap[a],heap[b]);//交换值
}
void down(int u) //up和down参数对象都是堆中元素下标
{
int t = u;
if(2*u <= length && heap[2*u] < heap[t]) //当左儿子存在且更小
t = 2*u; //更新当前t
if(2*u+1 <= length && heap[2*u+1] < heap[t]) //当右儿子存在且更小
t =2*u+1;
if(t != u){ //若出现儿子值更小的情况
swap_heap(t,u); //更新交换两节点
down(t); //继续看下一层儿子节点是不是比当层节点更小,一直递归
}
}
void up(int u)
{
int t = u;
while(t/2 && heap[t/2] > heap[t]){ //看父节点是否存在且更小
swap_heap(t,t/2); //更新
t >>= 1;
}
}
int main()
{
scanf("%d",&n);
string op;
int k,x;
while(n--){
cin >> op;
if(op == "I"){//插入一个数
scanf("%d",&x);
++cnt,++length; //插入次序和下标都++
heap[length] = x;
ph[cnt] = length; //修改双向的映射关系
hp[length] = cnt;
up(length); //插入的话是插到最后面,所以是up
}
else if(op == "PM"){//输出最小值
printf("%d\n",heap[1]);
}
else if(op == "DM"){//删除最小值
swap_heap(1,length--);
down(1);
}
else if(op == "D"){//删除第k个插入的值
scanf("%d",&k);
k = ph[k]; //若全局数组值作为函数参数,可能会在函数中把本来不该改变的全局数组值给修改掉,因此不能直接传全局数组值,因此提前设置一个变量,使其代替该全局数组值
swap_heap(k,length--); //交换两个节点或者说一个节点覆盖另一个节点的操作由swap_heap函数来完成
up(k); //在down和op中最多只会执行一个操作
down(k);//为了方便up和down都写一遍
}
else{//修改第k个插入的值
scanf("%d%d",&k,&x);
k = ph[k];
heap[k] = x;
up(k);
down(k);
}
}
return 0;
}
有两种创建散列的方法:
一是开放寻址法,即如果插入元素与表内元素发生位置上的冲突,插入元素就会移动到空位置再插入;
二是拉链法,用个数组表示每个hash后的值,然后数组的每个槽上连一条单链表;
散列表的操作一般只有插入和查找两种操作,删除的话并不是直接删除元素,而是在对应位置做个bool标记;
散列表模板题
(1)开放寻址法
#include //开放寻址法模拟散列表
#include
using namespace std;
const int N = 2e5 + 3; //N一般是大于数据量两倍的一个质数
int h[N]; //散列表数组
const int null = 0x3f3f3f3f; //null为大于题目中最大数据1e9的数,类似于inf,若数组位置内为这个数,则说明该位置为空,int为0x3f3f3f3f,long long为0x3f3f3f3f3f3f3f3f
int find(int x)//返回情况有两种,一种是找到空位置即返回null,一种是找到了x
{
int k = (x % N + N) % N; //hash操作
while(h[k] != null && h[k] != x){//如果既非空又不等于x就一直向右寻找
++k;
if(k == N) //如果到了表的尽头就从头开始
k = 0;
}
return k;
}
int main()
{
memset(h,0x3f,sizeof(h)); //把h都初始化成null(inf),memset按字节初始化,int一个字节8位,即0x3f
string op;
int n;
int x;
scanf("%d",&n);
while(n--){
cin >> op;
scanf("%d",&x);
int t = find(x);
if(op == "I")
h[t] = x; //不管是空还是x,都可以直接插入
else{
if(h[t] == null)
puts("No");
else
puts("Yes");
}
}
return 0;
}
(2)拉链法
#include //拉链法模拟散列表
#include
using namespace std;
const int N = 1e5 + 3;//N为大于1e5的质数
int ne[N]; //存储节点指向的下一个位置
int e[N]; //存储节点值
int h[N]; //槽,即数组每个位置对应的单链表的第一个位置
int idx; //标号,其中标号是所有单链表共用的,如idx=1可能是第一条单链表里的,而idx=2可能是最后一条单链表里的
void insert(int x)
{
int k = (x % N + N) % N; //hash函数,这样取防止有负数,得到的结果一定为0~N的正数
e[idx] = x; //和链表一样,先存值
ne[idx] = h[k]; //类似于插到头结点,即插到链表的第一个位置
h[k] = idx++; //更新头结点
}
int find(int x)
{
int k = (x % N + N) % N;
for(int i = h[k]; i != -1; i = ne[i]){ //从h[k]开始寻找,按链表向下寻找,直到遇到-1
if(e[i] == x)
return true;
}
return false;
}
int main()
{
int n;
int x;
memset(h,-1,sizeof(h)); //开始时数组槽由于没有链表,全部初始化成-1
string op;
scanf("%d",&n);
while(n--){
cin >> op;
scanf("%d",&x);
if(op == "I")
insert(x);
else {
if(find(x))
printf("Yes\n");
else
printf("No\n");
}
}
return 0;
}
字符串前缀哈希法
h[n] = 前n个字符的hash值(数值)
hash过程:
(1)将字符串str看成p进制的数,位数为str.size;
(2)计算值,转化为10进制数,如str = “abc”,则 n u m = a ∗ p 2 + a ∗ p 1 + c num = a*p^2+a*p^1+c num=a∗p2+a∗p1+c;
(3)将值取模q,即将hash值映射到0~q-1,num = num % q;
这样可以将任意字符串映射到0~q-1;
注意事项:
(1)不能将某个字符映射成0,如h(a) = 0,则h(aa)= 0也成立,这样就会有冲突;
(2)p =131或13331,且 q = 2 64 q = 2^{64} q=264,这样几乎不会发生hash冲突;
若已知h[R]和h[L-1](R>L,h[R]是前R个字符的hash值)
则 h [ R − L ] = h [ R ] − h [ L − 1 ] ∗ p R − L + 1 h[R-L] = h[R] - h[L-1]*p^{R-L+1} h[R−L]=h[R]−h[L−1]∗pR−L+1;(先让h[L-1]乘上p的R-L+1次方,使得其与h[R]对齐,然后相减即得结果)
预处理:(类似前缀和)
h [ i ] = h [ i − 1 ] + s t r [ i ] h[i] = h[i-1] + str[i] h[i]=h[i−1]+str[i]
字符串hash模板题
#include
using namespace std;
const int N = 1e5 + 10,P = 131;
typedef unsigned long long ull; //一般用unsigned long long ,这样溢出的话就相当于取模了
ull p[N]; //存p的n次方,因为是p进制数,所以是乘p
ull h[N]; //前n个字符的hash值
char str[N];
ull get(int l,int r) //计算l~r的hash值,类似前缀和的公式
{
return h[r] - h[l-1] * p[r-l+1];
}
int main()
{
p[0] = 1; //初始化幂方为1
int n, m;
int l1, r1, l2, r2;
scanf("%d%d%s", &n, &m, str+1); //类似前缀和,下标从1开始
for(int i = 1; i <= n; ++i){ //初始化,类似于求前缀和
p[i] = p[i - 1] * P; //顺便计算p的n次方
h[i] = h[i - 1] * P + str[i]; //h[i]是前i个字符的hash值
}
while(m--){
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if(get(l1, r1) == get(l2, r2))
puts("Yes");
else
puts("No");
}
return 0;
}