基础数据结构

纲要:用数组与函数模拟这些结构(结构体数组写起来慢,new慢)

链表

单链表-->邻接表存储图与数、双链表-->优化问题

单链表

用数组模拟动态链表,new node太慢了。

单链表 模板

用数组模拟链表(快,new太慢)

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
// 尾插就再加一个tail变量记录即可
// 理解不了就画图
int head, e[N], ne[N], idx;

//常用
// 初始化
void init()//别忘了在主函数调用
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ,idx++ ;//存头插的数据,将head指向第二个数据变为e[iex]这个数据的nex指针,head指向idx所在位置,idx加一移动至下一个空数组元素
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

//其他的函数
//下标为k的点后面插入
void add(int k,int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
//下标为k的点后面删掉,删掉下标为0与删掉头节点可不一样,头节点是相对位置。
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

//遍历
for(int i=head;i!=-1;i=ne[i])

双链表 模板

双链表存有两个指针,一左一右,用\(l[N],r[N]\)来存

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()//别忘了在主函数调用
{
    //!!0是左端点,1是右端点,左右端点是空的,他们绝对位置是最左和最右,操作不会改变他们性质
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除下标为a的节点
void remove(int a)
{
    l[r[a]] = l[a];//我右边的左边指向我的左边
    r[l[a]] = r[a];//我左边的右边指向我的右边
}

例.AcWing827 双链表

栈、队列

栈:先进后出,队列:先进先出

栈 模板

// tt表示栈顶,也表示现在栈内物的数量,数字从1开始标号(栈不能按下标访问,这里只是为了方便我们数组模拟操作)
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if (tt > 0) 不空
else 空

栈实现表达式求值

原理:不含括号的前提下,当前操作符优先级<=栈顶操作符优先级,就不断取栈顶符号计算。括号就单独考虑即可。

表达式求值模板

此模板可根据需要随意添加新运算符

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>

using namespace std;

stack<int> num;
stack<char> op;

void eval()
{
    auto b = num.top(); num.pop();//注意b、a的顺序,越靠近栈顶说明后放入,说明在表达式右边,这意味着从栈先拿出来的数字是加减乘除数,【后拿出来】的是【被】加减乘除数。
    auto a = num.top(); num.pop();
    auto c = op.top(); op.pop();
    int x;
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    num.push(x);
}

int main()
{
    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    string str;
    cin >> str;
    for (int i = 0; i < str.size(); i ++ )
    {
        auto c = str[i];
        if (isdigit(c))
        {
            int x = 0, j = i;
            while (j < str.size() && isdigit(str[j]))
                x = x * 10 + str[j ++ ] - '0';
            i = j - 1;//读取一定的长度的数字后,调整i,由于马上i++,故i=j-1;
            num.push(x);
        }
        else if (c == '(') op.push(c);
        else if (c == ')')
        {
            while (op.top() != '(') eval();
            op.pop();//从右往左处理,因为可以不止一个括号,括号嵌套括号,最后pop左括号
        }
        else
        {
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
            op.push(c);
        }
    }
    while (op.size()) eval();
    cout << num.top() << endl;
    return 0;
}
//代码可扩展性很强,可以加次方运算等,只要改动eval()以及运算符优先级即可,main函数的主体,没有牵扯到具体运算。

队列 模板

// hh 表示队头,tt表示队尾,规定数组从hh点往右(包括hh)依次为队列内容
//即hh 第二个数 第三个数 ... 第n个数 tt;共计n+1即tt-hh+1个数
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];
//队尾
q[tt];

// 判断队列是否为空
if (hh <= tt) 不空
else 空

循环队列 模板

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt) 不空
else 空

单调栈 模板

题目很典型

//常见模型:找出每个数左边离它最近的比它大/小的数
//思路,假设说一个数列里求一个数左侧第一个比它小的数,那在存入这个数组的时候,如果存了一个数字,这个数字可以取代这个数字左侧第一个比它小的数中间所有的数,因为这些而数字没有用,若能被输出则一定能被现在这个数字输出,把这些数字全都pop掉,或者说tt--,如果tt=0了就代表全pop掉了,没有比新数小的数。!别忘了最后一步无论如何新数都要存一下(所以你也能看到最后输出的结果是不增的,不看-1的话)
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    //输出
    stk[ ++ tt] = i;
}

例.AcWing830 单调栈

给定一个长度为 $N$ 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 $-1$。

输入格式

第一行包含整数 $N$,表示数列长度。

第二行包含 $N$ 个整数,表示整数数列。

输出格式

共一行,包含 $N$ 个整数,其中第 $i$ 个数表示第 $i$ 个数的左边第一个比它小的数,如果不存在则输出 $-1$。

数据范围

$1 \le N \le 10^5$
$1 \le 数列中元素 \le 10^9$

输入样例

5
3 4 2 7 5

输出样例

-1 3 -1 2 2

题解

#include <iostream>

using namespace std;

const int N = 100010;

int tt;
int q[N];

int main()
{
    tt=0;
    int n;
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        while(tt&&q[tt]>=x) tt--;
        if(tt) cout<<q[tt]<<' ';
        else cout<<"-1 ";
        q[++tt]=x;
    }
    
    return 0;
}

单调队列 模板

题目很典型

//常见模型:找出滑动窗口中的最大值/最小值
//q[]存的是下标,这样可以通过队列读取出窗口内数字的范围
#include <iostream>

using namespace std;

const int N = 1000010;

int a[N], q[N];

int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);

    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;//整个队列内是递增的,在新加入的数字左边且比新数小的数字都没有用,永无出头之日,为了保证全部剔除,应从大的数字开while循环,也就是从队尾(新数字进入的方向)开始。
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");

    hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");

    return 0;
}

例.AcWing154 滑动窗口

给定一个大小为 $n \le 10^6$ 的数组。

有一个大小为 $k$ 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 $k$ 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],$k$ 为 $3$。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 $n$ 和 $k$,分别代表数组长度和滑动窗口的长度。

第二行有 $n$ 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例

8 3
1 3 -1 -3 5 3 6 7

输出样例

-1 -3 -3 -3 3 3
3 3 5 5 6 7

题解

#include <iostream>

using namespace std;


const int N = 1e6 + 10;
int a[N],q[N],hh=0,tt=-1;//q[i]存的是a的下标
int n,k;

int main()
{
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    
    for(int i=0;i<n;i++)
    {
        if(hh<=tt && i-k+1>q[hh]) hh++;
        while(hh<=tt&&a[q[tt]]>a[i]) tt--;
        q[++tt]=i;
        if(i>=k-1) printf("%d ",a[q[hh]]);
    }
    printf("\n");
    hh=0,tt=-1;
    for(int i=0;i<n;i++)
    {
        if(hh<=tt && i-k+1>q[hh]) hh++;
        while(hh<=tt&&a[q[tt]]<a[i]) tt--;
        q[++tt]=i;
        if(i>=k-1) printf("%d ",a[q[hh]]);
    }
    return 0;
}

KMP字符串

快速查找子串,时间复杂度\(O(m+n)\)

KMP 模板

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度,一般下标从1开始
//求模式串的Next数组(预处理):(next[]的性质是next[i]=j,而对于p来说,这意味着p[1,j]=p[i-j+1,i],i是第一个不匹配的点,这样可以知道字符串可以直接往后滑多少长度,而降低时间复杂度。
//“A中以i结尾的非前缀字串”与“A的前缀”能够匹配的最长长度
//https://www.youtube.com/watch?v=dgPabAsTFa8

int ne[],p[],s[];
//读取p,s建议下标从1开始
//ne从1开始,且显然ne[1]=0;
//ne数组叫next可能会报错,关键词占用
//i用来指短串,表示现在生成ne数组到第几个了
for (int i = 2, j = 0; i <= n; i ++ )//等号别漏了
{
    while (j && p[i] != p[j + 1]) j = ne[j];//利用已有的ne看看下一位,也就是第i位是否和j后一位j+1是否相等,不行就再ne
    if (p[i] == p[j + 1]) j ++ ;//若能匹配的上就继续看能不能多滑一位
    ne[i] = j;
}

// 匹配
//i是长串指针,j是短串指针
for (int i = 1, j = 0; i <= m; i ++ )//等号别漏了
{
    while (j && s[i] != p[j + 1]) j = ne[j];//使用ne数组将字符串平移,注意对于长字符串的指针i并未移动,但同时对短字符串也不必从头检索,这时候循环继续i++比从j=0(被j=ne[j]替代)开始比对要快。
    if (s[i] == p[j + 1]) j ++ ;//相等就在执行这行
    if (j == n)
    {
        j = ne[j];//KMP在匹配完一个字符串之后仍然适用,现在继续找下一个
        // 匹配成功后的逻辑
    }
}
//这个时间复杂度是O(M+N),你可以看出来我们还是要完整的走一遍长度为N的长字符串,与M的短字符串,KMP并不会使在长字符串上的指针变化,只是让我们没匹配成功/匹配完成时,让短字符串滑一定长度并让匹配从当前i开始而不是从字符串头为i。

Trie树(字典树)

Trie树:高效的存储和查找字符串集合的数据结构:一般字母类型不多,比如全小写/大写/数字

Trie树 模板

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// idx表示用了多少个节点(第多少个操作),p是节点下标,数组里存的也是节点下标,假设只会出现小写字母,那么每个节点只会有26个分支,故[26];cnt[]用于记录每个节点p是终点的个数,比如abcde与abc同时存储,c需要记录一下是某个字符串终点,不然根据Trie会被abcde完全掩盖,同时这个cnt也能告诉我们重复的字符串数量。
// 插入一个字符串

//N的大小是题目 【数据数量✖每个数据的分支量】  或者 【数据总长度,比如所有字符串总长度】,对于每个数据的分支量,比如我以二进制分数据,每个数据的大小是小于2的31次方,那么这个数据就有30个分支(log以2为底2的30次方),详见acwing143题的N,或者极限思考,当数据量为1的时候,看这个时候N取多少,比如一个大小为2的30次方的数字,在son[N][2]下,显然N=30即可寻到其末尾,这个时候的30即我的【每个数据的分支量】。
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';//将小写字母映射到0~25
        if (!son[p][u]) son[p][u] = ++ idx;//没有就用一个新节点
        p = son[p][u];//son[p][u]存的要么是刚刚给的新节点,要么之前存过。
        //给son[p][u]分配一个idx,并且用此idx作为唯一地址,记录以此为终点的字符串个数,即循环结束后cnt[idx]++;写出来就是cnt[p]++;
    }
    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];
}
//太多字母类型可以直接映射为二进制存,那么就是son[N][2],一般情况下是将单词数字分解为一个字母一个数字

并查集(常用)

作用:将两个集合合并,询问两个元素是否在一个集合中。

原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点都存储他的父节点,\(p[x]\)表示x的父节点。最终是在维护集合。

问题一:如何判断树根?我们令p[x]=x,有这么性质的则为树根

问题二:如何求x的编号 ?while(p[x]!=x) x=p[x];

问题三:如何合并两个集合?px是x的集合编号,py是y的集合编号,直接令p[x]=y,即直接将父节点x接到y树父节点后,使其成为子节点。也可以p[y]=x,这个谁父谁子无所谓

问题四:如何优化更快?【路径压缩】每次子节点找父节点的时候,找到父节点时直接让自己的p指向父节点,同时把一路上所有经过的节点都直接指向父节点,这样只要走一遍,以后再找父节点就直接找到。

//(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);//find()就是返回父节点,将父节点的值直接赋值给一路上所有经过的子节点。注意为了有迭代过程是find(p[x])而不是find(x);即同时进行找父节点与路径压缩两个过程。
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);//让a的父节点 变为 b的父节点 的子节点

//(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, 【size[]只有祖宗节点的有意义】,表示祖宗节点所在集合中的点的数量
	//size在C++17中被使用了,要么改名,要么不用using namespace std;,否则编译会报错。

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);//这里用if而不是while,本身就是递归不需要while
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;//初始化时多了一步所有父节点拥有1的size
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];//合并时多了一步把size加到新的父节点上即可,这步要先做
	//做上面这步之前先考虑find(a)==find(b),如果相等说明已在同一个集合中,没有必要再合并计算size[]
    p[find(a)] = find(b);

	//节点x所在的树的节点总数
	size[find(x)];
//(3)维护到祖宗节点距离的并查集:
//路径压缩的时候同时储存+=到祖宗的距离,对距离取模可以形成层次,以此来判断每个节点之间的关系(与根的关系)
    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
	//d[x]一般可以理解为目前节点与父节点距离,而不是与根节点距离
	//但是由于find函数能改变其父节点为根节点,所以要注意这个过程的节点变化

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];//先递归,从里往外,从树的上往下更新,也就是先让最里层递归d[p[x]]更新成与根节点距离,再一层一层往外,否则加上的只是这个节点的父节点到父父节点的距离,而父节点的父节点不一定是根节点。
            p[x] = u;
            /*注释是错误写法
        	d[x] += d[p[x]];
        	p[x] = find(p[x]);
        	*/
        }
        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)的偏移量
	//这里的d[]一般按题目要求列方程式得出,比如
/*acwing240 食物链
    有
    d[px]=d[y]-d[x];
    因为d[x]到px距离+px到根py距离=y到根距离,这样即可满足x与y到根节点距离之差	模3等于0,即xy同类,即d[px]的表达式来源于方程d[px]+d[x]=d[y]
*/

提示

scanf读字符还是“必须”当字符串读(比如对op[2],%s)这样可以过滤空格与回车,减少麻烦,与此同时我们直接取op[0]就是本来要读的字符了。

堆(手写)

堆属于完全二叉树

要做的事

1.插入一个数:在堆末尾后新加一个数,然后up操作

2.求集合当中最小值:h[1]

3.删除最小值:把h[1]用堆最后一个元素替代后size--,然后对新的h[1]进行down(1)操作

4.删除任意一个元素:如堆第k个元素,与3同理,先用末尾替换后size--,但是up还是down操作具体看数字变小还是变大,但其实不用判断也行,直接up(k),down(k)连用,无论数字变大变小不变,至多会有一个函数起作用,而同时正好也是需要起作用的函数

5.修改任意一个元素:如堆第k个元素,与4同理,直接h[k]=x,然后up(k),down(k)

基本性质:以小根堆为例,每个节点都小于等于左右两个儿子

存储:用一维数组,下标从1开始(方便),下标为x的节点(第x个节点)左儿子下标是2x,右儿子2x+1

堆 模板

注意函数调用的时候传入的参数是什么,三个大函数up,down,heap_swap都是传入数组下标,或者说堆的第几个元素,所以凡是h[i],hp[i]都是错误调用,调用函数只能用i或者ph[i]作为参数(本模板模拟堆较为全面,因为考虑到dijkstra算法会用到,一般的堆模拟无需heap_swap以及hp,ph两个数组)

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

//自己的话:
//ph存第k个插入的点的下标,p[k]=j,p[j]=k;
//堆里某个点是第几个插入的点,这个数组是为了帮助堆变化的时候修正ph

/*
有时int m=0;用m来记录是第几个操作数
*/

// 交换两个点,及其映射关系
void heap_swap(int a, int b)//参数是点的下标
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}//由于所有的交换都是heap_swap完成,故我们就不必再考虑down,up或者其他操作对俩个附加数组hp,ph的影响了

//对于小根堆:形象化理解,越大的数越往下沉down,越小越往上飘up;
void down(int u)//节点数字变大就down操作,传入的参数为该节点的下标/现在所需要维护的点的坐标
{
    int t = u;//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);//递归,继续down,直到满足小根堆对此数字的要求
    }
}

void up(int u)//节点数字变小就up操作,传入的参数为该节点的下标
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);//对已有的数组原地建堆,注意这个时候数据已经读取到h里了,只是还没有建堆。

//若手动添加数字建堆,注意维护hp与ph
scanf("%d",&x);
h[++Size]=x;
ph[++m] = Size;
hp[Size] = m;//m表示第几个操作数,需要在逐个添加数字前额外声明int m=0;
up(Size);

例.AcWing839 模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

  1. I x,插入一个数 $x$;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 $k$ 个插入的数;
  5. C k x,修改第 $k$ 个插入的数,将其变为 $x$;

现在要进行 $N$ 次操作,对于所有第 $2$ 个操作,输出当前集合的最小值。

输入格式

第一行包含整数 $N$。

接下来 $N$ 行,每行包含一个操作指令,操作指令为 I xPMDMD kC k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

$1 \le N \le 10^5$
$-10^9 \le x \le 10^9$
数据保证合法。

输入样例

8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM

输出样例

-10
6
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 100010;

int h[N],hp[N],ph[N],sizee,m;

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<=sizee&&h[u*2]<h[t]) t=u*2;
    if(u*2+1<=sizee&&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/2,u);
        u>>=1;
    }
}

int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        char op[3];
        int a,b;
        cin>>op;
        if(!strcmp(op,"I"))
        {
            cin>>a;
            h[++sizee]=a;
            ph[++m]=sizee;
            hp[sizee]=m;
            up(sizee);
        }
        else if(!strcmp(op,"PM")) cout<<h[1]<<endl;
        else if(!strcmp(op,"DM"))
        {
            heap_swap(1,sizee);
            sizee--;
            down(1);
        }
        else if(!strcmp(op,"D"))
        {
            cin>>a;
            /*错误写法,因为ph[a]在heap_swap之后就不再指向后来需要down,up操作的位置了,即原h[sizee]的新位置
            heap_swap(ph[a],sizee);
            sizee--;
            down(ph[a]);
            up(ph[a]);
            */
            a = ph[a];
            heap_swap(a,sizee);
            sizee--;
            down(a);
            up(a);
        }
        else
        {
            cin>>a>>b;
            h[ph[a]]=b;
            down(ph[a]);
            up(ph[a]);
        }
    }
    
    return 0;
}

哈希表(一般只有添加和查找,若要删除则新开一个bool数组标记)

存储结构

哈希函数若是取模法,取得模尽量是质数并且离2的整次幂越远哈希结果冲突的概率越小(现场写个小东西算一下模,比如我这题N取二十万左右,就算一下从二十万开始的质数是多少,然后用它)

按照处理冲突的方式(两种方式都常用)分为

拉链法:不同的值哈希值相同时,在哈希值上拉一条链存这些值(用单链表存)

开放寻址法:

哈希表 模板

int k = (x % N + N) % N; //正负数同时存在的取模法
//(1) 拉链法
    int h[N], e[N], ne[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;
    }

	//主函数
	memset(h,-1,sizeof h);//一开始h[k]全都是-1,随着新的哈希值加入,h[k]存的是最后一个哈希值为k的数组下标,原值即为e[k],往回溯上一个值下表为ne[k],故整体流程是:存值e[idx]=x -> 存上个同哈希值被分配的下标ne[idx]=h[k] -> 存新同哈希值被分配的下标 h[k]=idx;【h[k]存的是头,-1是尾】
//(2) 开放寻址法(厕所寻找坑位):添加,在第k个位置添加,如果被添加过了就找下一个位置 查找,从第k个位置找起,一直找到没有被用过的位置为止,如果都没有说明不存在。

    int h[N];//一般情况下N取值要达到题目数据个数的2~3倍左右。

	//主函数
	memset(h,0x3f,sizeof h);//具体题目具体分析,一般0x3f3f3f3f比10的九次方大意味着比题目所有的值都大,那么如果这个数组的值是null=0x3f3f3f3f,那这就意味这这个数组没被用过,空的。不同题用-1也行。

    // 如果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;
    }

字符串哈希:字符串前缀哈希法。有的题KMP会很麻烦,就字符串哈希,可以快速判断两个字符串是否相等

1、先算出前缀哈希表,比如字符串”ABCDE“,那么h[0]=0,h[1]="A"的哈希值,h[2]="AB"的哈希值,h[3]="ABC"的哈希值,h[4]=....

2、字符串哈希值算法:把字母转化成p进制下的的数字,一般不会映射成0,变为0就计算的时候见不到它起作用。人品足够好的时候不会有冲突...,经验上进制一般取131或13331,模取2的64次方,出现冲突概率很小。(这种方法不容忍冲突)

3、通过1,2得到任意一段字串的哈希值,那么[l,r]这段字符串的哈希值为,写式子,好推

\[h[R]-h[L-1]*p^{(R-l+1)} \]

h[i]也可以递归计算

\[h[i]=h[i-1]^p+str[i]此字符的哈希值,可以直接以ascii码当哈希值 \]

例.AcWing841 字符串哈希

给定一个长度为 $n$ 的字符串,再给定 $m$ 个询问,每个询问包含四个整数 $l_1, r_1, l_2, r_2$,请你判断 $[l_1, r_1]$ 和 $[l_2, r_2]$ 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数 $n$ 和 $m$,表示字符串长度和询问次数。

第二行包含一个长度为 $n$ 的字符串,字符串中只包含大小写英文字母和数字。

接下来 $m$ 行,每行包含四个整数 $l_1, r_1, l_2, r_2$,表示一次询问所涉及的两个区间。

注意,字符串的位置从 $1$ 开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No

每个结果占一行。

数据范围

$1 \le n, m \le 10^5$

输入样例

8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2

输出样例

Yes
No
Yes
#include <iostream>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;

int n, m;
char str[N];
ULL h[N], p[N];//h[]为前缀哈希值,p[i]则是p的i次方

ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    scanf("%d%d", &n, &m);
    scanf("%s", str + 1);//从1开始而不是从0

    p[0] = 1;//p的0次方等于1
    for (int i = 1; i <= n; i ++ )
    {
        h[i] = h[i - 1] * P + str[i];
        p[i] = p[i - 1] * P;
    }

    while (m -- )
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);

        if (get(l1, r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }

    return 0;
}

注:经过测试,判断次数很多时,字符串哈希快于substr()

posted @ 2022-01-02 20:50  泝涉江潭  阅读(104)  评论(0)    收藏  举报