通过数据结构 -- acwing

第二讲 数据结构

单链表

为什么要用数组模拟链表?—快!

用的最多是 邻接表(实际上是 n 个链表)–> 用来存储 图 和 树

单链表初始状态:

head-> 空结点

插入数据后:

head -> node1 -> node2 -> node3 -> null

每个 node 都有一个 value 和 一个 next 指针

举例

例如我有如下链表

value 3 5 7

head -> node1 -> node2 -> node3 -> null

下标 0 1 2

那么我 e[ 0 ] = 3 ne[ 0 ] = 1, e[ 1 ] = 5 ne[ 1 ] = 2, e[ 2 ] = 7 ne[ 2 ] = -1;

此处空结点的坐标用 -1 来表示

假使我现在有一个结点 a ,想插入 头结点 和 第一个结点之间

  1. 让 a 的next 指向 第一个结点
  2. 让 head 的next 指向 a

基本操作

int head,e[N],ne[N],n,idx = 0;

//  初始化
void init()
{
    head = -1;
    idx = 0;
}

//  将 x 插入头结点
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx ++;
}

//  将 x 插入下标是 k 的点的后面
void add(int x,int k)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

//  将 k 后面那个点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

练习

826. 单链表 - AcWing题库

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
#include<iostream>

using namespace std;

const int N = 100010;

//  head 表示头结点的下标
//  e[i] 表示节点 i 的值
//  ne[i]表示节点 i 的 next 指针是多少
//  idx 存储当前用到了哪个点

int head,e[N],ne[N],n,idx = 0;

//  初始化
void init()
{
    head = -1;
    idx = 0;
}

//  将 x 插入头结点
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx ++;
}

//  将 x 插入下标是 k 的点的后面
void add(int x,int k)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

//  将 k 后面那个点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    cin >> n;
    init();
    for(int i = 0;i < n;i++)
    {
        int k,x;
        char op;
        
        cin >> op;

        if(op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }else if(op == 'D')
        {
            cin >> k;
            if(!k)
            {
                head = ne[head];
            }else{
                remove(k-1);
            }
        }else if(op == 'I')
        {
            cin >> k >> x;
            add(x,k-1);
        }
        
    }
    
    for(int i = head;i != -1;i = ne[i])
    {
        cout << e[i] << ' ';
    }
    
    return 0;
}

双链表

原理

用来优化某些问题

有两个指针,一个指向前,一个指向后

一个 e[N] 数组存储值 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];
}

练习

827. 双链表 - AcWing题库

实现一个双链表,双链表初始为空,支持 5 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式

共一行,将整个链表从左到右输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
#include<iostream>
using namespace std;

const int N = 100010;
int m;
int e[N],l[N],r[N],idx;

void init()
{
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

void insert(int k,int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx++;
}

void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main()
{
    int k,x;
    init();
    cin >> m;
    while(m--)
    {
        string op;
        cin >> op;
        if(op == "L")
        {
            cin >> x;
            insert(0,x);
        }else if(op == "R")
        {
            cin >> x;
            insert(l[1],x);
        }else if(op == "D")
        {
            cin >> k;
            remove(k+1);
        }else if(op == "IL")
        {
            cin >> k >> x;
            insert(l[k+1],x);
        }else if(op == "IR")
        {
            cin >> k >> x;
            insert(k+1,x);
        }
    }
    
    for(int i = r[0];i != 1;i = r[i])
    {
        cout << e[i] << " ";
    }
    return 0;
}

连结表

其实就是 n 个单链表

原理

先进后出,可以理解成一个单口的罐子,每次只有两个操作,放进去和拿出来最上面的一个

模板

// tt表示栈顶
int stk[N], tt = 0;

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

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

// 栈顶的值
stk[tt];

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

}

练习

828. 模拟栈 - AcWing题库

实现一个栈,栈初始为空,支持四种操作:

  1. push x – 向栈顶插入一个数 x;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤100000,
1≤x≤109
所有操作保证合法。

输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
#include<iostream>
using namespace std;

const int N = 100010;
int stk[N],tt,m;
//  tt 表示栈顶下标
//  插入:   stk[++tt] = x;
//  弹出:  tt--;
//  判断栈空:  if(tt>0) --> 不空
//  栈顶元素:   stk[tt];

void init()
{
    tt = 0;
}

bool isempty()
{
    if(tt==0)
    {
        return true;
    }
    return false;
}

void push(int x)
{
    stk[++tt] = x;
}

void pop()
{
    if(!isempty())
    {
        tt--;
    }
}

int query()
{
    cout << stk[tt] << endl;
}

int main()
{
    cin >> m;
    init();
    while(m--)
    {
        int x;
        string op;
        cin >> op;
        if(op == "push")
        {
            cin >> x;
            push(x);
        }else if(op == "pop")
        {
            pop();
        }else if(op == "empty")
        {
            if(isempty())
            {
                cout << "YES" << endl;
            }else{
                cout << "NO" << endl;
            }
        }else if(op == "query")
        {
            query();
        }
    }
    return 0;
}

3302. 表达式求值 - AcWing题库

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过 231-1。
  • 题目中的整除是指向 00 取整,也就是说对于大于 00 的结果向下取整,例如 5/3=15/3=1,对于小于 00 的结果向上取整,例如 5/(1−4)=−15/(1−4)=−1。
  • C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 105

输入样例:
(2+2)*(1+1)
输出样例:
8
#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();
    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;
            num.push(x);
        }
        else if (c == '(') op.push(c);
        else if (c == ')')
        {
            while (op.top() != '(') eval();
            op.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;
}

队列

原理

先进先出,可以理解为一个双头的筒,每次也只有两个操作,从后面塞入一个东西和从前面拿出一个东西

模板

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

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

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

// 队头的值
q[hh];

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

//2. 循环队列
// 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)
{
	//不是空的
}

练习

829. 模拟队列 - AcWing题库

实现一个队列,队列初始为空,支持四种操作:

  1. push x – 向队尾插入一个数 x;
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。

现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤100000,
1≤x≤109
所有操作保证合法。

输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
#include<iostream>
using namespace std;

const int N = 1000010;
int q[N],hh,tt;
int m;
//  队尾插入,队头弹出

void init()
{
    hh = 0;
    tt=-1;
}

void push(int x)
{
    q[++tt] = x;
}

void pop()
{
    hh++;
}

bool isempty()
{
    if(hh <= tt)
    {
        cout << "NO" << endl;
        return false;
    }
    cout << "YES" << endl;
    return true;
}

int query()
{
    return q[hh];
}

int last()
{
    return q[tt];
}

int main()
{
    init();
    cin >> m;
    while(m--)
    {
        string op;
        cin >> op;
        if(op == "push")
        {
            int x;
            cin >> x;
            push(x);
        }else if(op == "pop")
        {
            pop();
        }else if(op == "empty")
        {
            isempty();
        }else if(op == "query")
        {
            cout << query() << endl;
        }
    }
    return 0;
}

单调栈

常用于:给定一个序列,求每一个数,左(右)边离他最近的且比他小(大)的数

举例

有序列:3 4 2 7 5

找到每一个数左边第一个比他小的数,没有就返回 -1

那么 3 的左边没有数所有返回 -1

4 的左边第一个是 3

2 的左边没有比他小的 返回 -1

7 的左边比他小的最近数为 2

5 的左边比他小的最近数为 2

所有最后返回 [-1,3,-1,2,2]

先考虑暴力做法:两重循环

第一重循环 i从 0-n

第二重从 i-1 开始枚举,一直往左走,知道找到第一个比他小的数停止

如果我们的栈中存在如下关系:
a x > = a y ∣ ∣ x < y 那么 a x 就会被删掉 a_x >= a_y ||x <y\\ 那么 a_x就会被删掉 ax>=ay∣∣x<y那么ax就会被删掉
所以最后剩下的序列,一定是一个单调序列

那么此时如果进来一个 i,要找到左边第一个 比 i 小的数

if stk[tt] >= i

把 stk[tt] 删了,一直删

直到找到一个 stk[tt] < i

这个 stk[tt] 就是我们要找的值,再把 i 插入单调栈

模板

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

高效输入输出

cin.tie(0);
ios::sync_with_stdio(false);

练习

830. 单调栈 - AcWing题库

给定一个长度为 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<iostream>

using namespace std;

const int N = 100010;

int n;
int stk[N],tt;

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n;
    
    for(int i = 0;i < n;i++)
    {
        int x;
        cin >> x;
        while(tt && stk[tt] >= x)
        {
            tt--;
        }
        if(tt)
        {
            cout << stk[tt] << " ";
        }else{
            cout << -1 << " ";
        }
        
        stk[++tt] = x;
    }
    
    return 0;
}

单调队列

经典应用:滑动窗口里的最大值/最小值

举例

假设有序列:

1 3 -1 -3 5 3 6 7

第一次滑动窗口是 【1 3 -1】最小值是 -1

第二次滑动窗口是 【3 -1 -3】最小值是 -3

以此类推最后一次滑动窗口是 【3 6 7】 最小值是 3

我们用队列来维护这个窗口,保证队列中存储的时时刻刻都是我们窗口中的元素

每一次求极值的时候,暴力做法是遍历窗口中的元素,时间复杂度是O(k)

单调队列定律:当一个选手比你小还比你强的时候,你永远无法超越他

那么只要我们的队列中存在如下情况:

前面有一个数,比我后面的数大,那么前面的点一定没有用

因此,我们就可以把大的点删了,最后会变成一个严格单调上升的队列

模板

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

练习

154. 滑动窗口 - AcWing题库

给定一个大小为 n≤106 的数组。

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

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

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

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],kk 为 33。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

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

输入格式

输入包含两行。

第一行包含两个整数 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 = 1000010;
int n,k;
int a[N],q[N];

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    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(hh <= tt && i-k+1>q[hh])
        {
            hh++;
        }
        
        while(hh <= tt && a[q[tt]] >= a[i])
        {
            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++)
    {
        //  判断队头是否滑出窗口
        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)
        {
            cout << a[q[hh]] << " ";
        }
        
    }
    
    return 0;
}

KMP

原理

失败了退一步,再尝试

假设我们有一个字符串

暴力枚举法

假设 S[N] 是原串,P[M] 是模式串

for(int i = 1;i <= n;i++)
{
	bool flag = true;
	for(int j = 1;j <= m;j++)
	{
		if(s[i]!=p[j])
		{
			flag = false;
			break;
		}
	}
}

在暴力做法中,若出现在匹配不合适的情况,只会将 匹配的起点往后移动一位

那么在我失败后,我新的模板串往后移动多少位可以开始匹配?

也就是我新的模板串往后移动多少位使得 我新的模板串从 起点到我上一次失败的点的串 和 原串相等

引入 next 数组

而 next[ i ] 的意义就是 ,以 i 为终点的后缀 和 从 1 开始的前缀相等,而且后缀的长度最长

假设 next[ i ] = j 那么其含义就是p[1,j] = p[i-j+1,i]

假设我们匹配出错的点是 i,对于模式串来说出错的点是 j+1,也就是 s[i] != p[j+1],那么这个时候我们需要移动我们的模式串,也就是调用我们的next数组,找到以这个点为终点的后缀 和 从 1 开始的前缀,最长的相等,也就是 next[ j ]

模板

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

举例

假设有数组

S = " abababc"

P = “abababab”

那么我们有

next[1] = 0 //我不能等于自己

next[2] = 0 //ab 的前缀是 a,后缀是 b,不相等

next[3] = 1 //aba 长度是 1 的时候 前缀是 a,后缀是 a;长度是 2 的时候 前缀是 ab ,后缀是 ba,不匹配,所以 值为 1

next[4] = 2 //abab 长度为 2 的时候 前缀是 ab,后缀是 ab,长度为 3 的时候 前缀是 aba,后缀是 bab,不匹配,所以值为 2

依次类推

next[5] = 3

next[6] = 4

next[7] = 5

next[8] = 6

当我们进行 kmp 匹配的时候,当我们下标是 7 的时候不相等了【也就是 {s[7] = c} != {p[7]=a},i = 7,j = 6】

那么我找到 j = ne[j] == ne[6] = 4

为什么?

因为这个时候 是长度为6的字符串,他的前缀和后缀相等的最大 也就是 我们 next[6]的值也就是 4

练习

831. KMP字符串 - AcWing题库

给定一个字符串 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<iostream>
using namespace std;

const int N = 1e6+10;
int ne[N];
char s[N],p[N];
int n,m;

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    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++)
    {
        // j 没有退回起点 并且 s[i] 不能和 p[j+1] 匹配
        while(j && s[i]!=p[j+1])
        {
            j = ne[j];
        }
        //  已经匹配
        if(s[i]==p[j+1])
        {
            j++;
        }
        if(j == n)
        {
            //匹配成功
            cout << i-n << " ";
            j = ne[j];
        }
    }
    
    return 0;
}

Trie

基本用法&作用

快速地存储和查找字符串集合的数据结构

我们在使用 trie 的过程中,我们使用的字符串一定是 都是 大写/小写,并且长度不长

比分说我们有字符串:

abcdef

abdef

aced

bcdf

cdaa

bcdc

bcff

首先 trie 树有一个根节点 root

我们现在将第一个字符串存进来,我们在存的时候是从前往后遍历每一个字符

首先遍历到的是 字符 a,检查有没有 a 作为子节点,没有的话就把 a 结点创建下来

再遍历到 b,检查 a 下有没有 b 作为子节点,没有就把 b 结点创建下来

ROOT
a
b
c
d
e
f

再接下来 存储 第二个字符串 abdef

同样我们检查 root 下有没有 a 作为子结点,如果没有就创立一个 a 子结点,那么显然是有的,那就不用操作继续往下走,直到走到 d,发现 b 下没有 d 作为子结点,依次类推检查 d 下有没有 e 作为子结点

ROOT
a
b
c
d
e
f
d_1
e_1
f_1

最后我们构造的树如下:

ROOT
a
b
c
d
e
f
d_1
e_1
f_1
c_2
e_2
d_2
b_3
c_3
d_3
f_3
c_4
d_4
a_4
a_44
c_5
f_6
f_66

当然我们要在每个单词的结尾做一个标记,不然假设我们现在 进来一个 abc ,我们就无法检测出来这个字符串

此时模拟查找的过程

  1. 查找 bcdf,首先在 root的子节点中查找 b,再在 b 的子节点中查询 c,依次类推查找到 f ,检查有无终止标记
  2. 查找 abc,确实查到了 c 结点,但是 c 没有终止标记,所以返回查找失败
  3. 查找 bacd,在 b 的子节点中,并没有查找到 a 作为子节点,所以该字符串不存在,返回查找失败

模板

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
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];
}

练习

835. Trie字符串统计 - AcWing题库

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

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

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗104

输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
#include<iostream>
using namespace std;

int n;
const int N = 100010;
int son[N][26],cnt[N],idx;
char str[N];
//  idx 当前用到的是哪个下标,下标是 0 的点,既是根节点,又是空结点


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];
}

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n;
    while(n--)
    {
        char op[2];
        cin >> op >> str;
        if(op[0] == 'I')
        {
            insert(str);
        }else{
            cout << query(str) << endl;
        }
        
    }
    return 0;
}

143. 最大异或对 - AcWing题库

在给定的 NN 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N。

第二行输入 N 个整数 A1~AN。

输出格式

输出一个整数表示答案。

数据范围

1≤N≤105
0≤Ai<231

输入样例:
3
1 2 3
输出样例:
3
#include<iostream>
using namespace std;

const int N=100010,M=31*N;
int n;
int a[N];
int son[M][2],idx = 0;
//  M 代表一个数字串二进制可以有多长

void insert(int x)
{
    int p = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x>>i&1;     //  取出 x 的第i位的二进制数是什么
        if(!son[p][u])
        {
            son[p][u] = ++idx;  //  插入的时候发现没有该子节点,就开辟一个新路
        }
        p = son[p][u];  //  指针指向下一层
    }
}

int search(int x)
{
    int p = 0;int res = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x>>i&1;
        if(son[p][!u])  // 如果当前层有对应的不相同的数
        {   //  p 就指向相应的位置
            
            p = son[p][!u];
            res = res * 2 + 1;
            
        }else{
            p = son[p][u];
            res = res * 2 + 0;
        }
    }
    return res;
}


int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n;
    for(int i = 0;i < n;i++)
    {
        cin >> a[i];
        insert(a[i]);
    }
    int res = 0;
    for(int i = 0;i < n;i++)
    {
        res = max(res,search(a[i]));
    }
    cout << res << endl;
    return 0;
}

并查集

快速地处理

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

比如 belong[x] 存储的是 x 属于哪一个集合,比如 belong[x] = a 代表 元素 x 属于 集合 a

那么我们就可以用 O(1) 来判断 元素 x 和 y 是否在同一个集合中

if(belong[x] == belong[y])

但是如果要合并一个 1000个元素的集合 和 2000个元素的集合,那么我们至少要做 1000 次操作

但是并查集可以在近乎 O(1) 的时间内完成这两个操作

基本原理

用树的形式来维护每一个集合

root1
p1
p4
p2
p5
p3
p6
p7
root2
pp1
pp4
pp2
pp5
pp3
pp6
pp7

每一个集合的根节点元素 (root) 就是她的代表元素,根节点的编号就是我当前集合的编号

对于每一个点,我们都存储她的父节点是谁,p[x] 表示他的父节点是谁,p[p4] = p1

当我们要求某一个点属于哪一个集合的时候,比方说 找 p4 属于哪一个集合,我们可以根据他的 father 依次往上找,直到根节点为止

//问题1	如何判断树根
if(p[x] == x)
//问题2	如何求 x 的集合编号
while(p[x]!=x)
{
    x = p[x];
}
//问题3	如何合并两个集合,加一条边:px 是 x 的集合编号,py 是 y 的集合编号
p[x] = y;

root1
p1
p4
p2
p5
p3
p6
p7
root2
pp1
pp4
pp2
pp5
pp3
pp6
pp7

压缩路径优化

当我们查询一个元素 pp4 在哪个集合中的时候,我们将他这个路径上的所有点,全部存到根节点下

root1
p1
p4
p2
p5
p3
p6
p7
root2
pp1
pp2
pp5
pp3
pp6
pp7
pp4

模板

(1)朴素并查集:

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

    // 返回x的祖宗节点
    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);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    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所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);


(3)维护到祖宗节点距离的并查集:

    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)的偏移量

练习

836. 合并集合 - AcWing题库

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
#include<iostream>

using namespace std;

const int N = 100010;
int n,m;
int p[N];

int find(int x) //返回 x 的祖宗结点
{
    if(p[x]!=x)
    {
        p[x] = find(p[x]);
    }
    return p[x];
}     

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n >> m;
    
    for(int i = 1;i <= n ;i++)
    {
        p[i] = i;
    }
    
    while(m--)
    {
        char op;
        int a,b;
        cin >> op >> a >> b;
        if(op == 'M')
        {
            p[find(a)] = find(b);
        }else if(op == 'Q'){
            if(find(a) == find(b))
            {
                cout << "Yes" << endl;
            }else{
                cout << "No" << endl;
            }
        }
    }
    return 0;
}

837. 连通块中点的数量 - AcWing题库

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  1. C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  2. Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 aa 和 bb 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3

连通块:如果 从 点 a 可以走到 b,从 b 也可以走到 a,那么说明 a 和 b 属于同一连通块

我们可以用集合来维护连通块,当我们在两个连通块之间连一条边的时候,其实就是将两个集合进行合并

#include<iostream>

using namespace std;

const int N = 100010;
int n,m;
int p[N];
int Size[N];

int find(int x) //返回 x 的祖宗结点
{
    if(p[x]!=x)
    {
        p[x] = find(p[x]);
    }
    return p[x];
}     

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n >> m;
    
    for(int i = 1;i <= n ;i++)
    {
        p[i] = i;
        Size[i] = 1;
    }
    
    while(m--)
    {
        char op[5];
        int a,b;
        cin >> op;
        if(op[0] == 'C')
        {
            cin >> a >> b;
            if(find(a) == find(b))
            {
                continue;
            }
            Size[find(b)] += Size[find(a)];
            p[find(a)] = find(b);
        }else if(op[1] == '1'){
            cin >> a >> b;
            if(find(a) == find(b))
            {
                cout << "Yes" << endl;
            }else{
                cout << "No" << endl;
            }
        }else{
            cin >> a;
            cout << Size[find(a)] << endl;
        }
    }
    return 0;
}

240. 食物链 - AcWing题库

动物王国中有三类动物 A,B,C这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 NN 个动物,用上述两种说法,一句接一句地说出 KK 句话,这 KK 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X 或 Y 比 N 大,就是假话;
  3. 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000
0≤K≤100000

输入样例:
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
输出样例:
3
1
2
3
4
5
6
root

我们通过维护并查集的 子节点到 root 结点的距离来实现 食物链

比如 1 到 root 结点的距离是 1,说明 1 被 root 吃,4 到 root 的距离是 1,说明 4 被 root 吃

2 到 root 的距离是 2,说明 2 被 到 root 距离为 1 的结点吃

3 到 root 的距离是 3,说明 3 被 到 root 距离为 2 的结点吃,同时可以吃到 root 距离为 1的结点

#include <iostream>

using namespace std;

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);

        if (x > n || y > n) res ++ ;
        else
        {
            int px = find(x), py = find(y);
            if (t == 1)
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] - d[x];
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }

    printf("%d\n", res);

    return 0;
}

如何手写一个堆?

  1. 插入一个数 heap[++size] = x;up(x);
  2. 求集合当中的最小值 heap[1]
  3. 删除最小值 heap[1] = heap[size];size–;down(1);
  4. 删除任意一个元素 heap[k] = heap[size];size–;down(k) or up(k)
  5. 修改任意一个元素 heap[k] = x;down(k);up(k);

基本结构

堆的本质是一个完全二叉树,也就是除了最后一层结点,剩余层结点都是满的,最后一层从左到右排列

以小根堆为例,每一个点都是小于等于左右儿子的

我们用一个一维数组来存 堆

其中一号点是根节点

x 的左儿子是 2x,右儿子是 2x+1

如果修改的根节点,我们就将他与他的左右儿子比较,将小的那个移到根节点,直到根节点符合逻辑

如果修改的是叶子结点,我们就将他与他的兄弟和父结点比较,同样将小的移到根节点,递归地调用直到符合逻辑

如果用 size 来维护整个堆的大小

我们在做插入操作的时候,就是在 heap[++size] = x,也就是堆的左下方加入新元素,然后在对这个元素进行 up 操作

我们可以很容易地删除 最后一个点 size–

但是我们很难删除第一个点,我们可以用这样的方法来操作

让第一个元素 = 最后一个元素,然后 size–,再对第一个元素 进行一遍 down 操作

同理如果我们要删除第 k 个点的话,我们只需要让 第 k 个元素 = 最后一个元素,size–,如果 heap[k]的值是变大了,那我应该down(k),如果变小了就 up(k)

模板

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

// 交换两个点,及其映射关系
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 <= 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);
    }
}

void up(int u)
{
    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);

练习

838. 堆排序 - AcWing题库

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式

第一行包含整数 n 和 m。

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

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1≤m≤n≤105
1≤数列中元素≤109

输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
#include<iostream>
#include<algorithm>

using namespace std;

const int N =100010;
int n,m;
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()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n >> m;
    
    for(int i = 1;i <= n;i++)
    {
        cin >> h[i];
    }
    
    Size = n;
    
    for(int i = n/2;i;i--)
    {
        down(i);
    }
    
    while(m--)
    {
        cout << h[1] << " ";
        h[1] = h[Size];
        Size--;
        down(1);
    }
    
    return 0;
}

839. 模拟堆 - AcWing题库

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

  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≤N≤105
−109≤x≤109
数据保证合法。

输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
#include<iostream>
#include<algorithm>
#include<string.h>

using namespace std;

const int N =100010;
int ph[N],hp[N];
int h[N],Size;

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 <= 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);
    }
}

void up(int u)
{
    while(u/2 && h[u/2] > h[u])
    {
        heap_swap(u,u/2);
        u/=2;
    }
}


int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    int n,m=0;
    cin >> n;
    
    while(n--)
    {
        char op[10];
        int k,x;
        cin >> op;
        if(!strcmp(op, "I"))
        {
            cin >> x;
            m++;
            Size++;
            ph[m] = Size;
            hp[Size] = m;
            h[Size] = x;
            up(Size);
        }else if(!strcmp(op,"PM"))
        {
            cout << h[1] << endl;
        }else if(!strcmp(op,"DM"))
        {
            heap_swap(1,Size);
            Size--;
            down(1);
        }else if(!strcmp(op,"D"))
        {
            cin >> k;
            k = ph[k];
            heap_swap(k,Size);
            Size--;
            down(k);
            up(k);
        }else{
            cin >> k >> x;
            k = ph[k];
            h[k] = x;
            down(k);
            up(k);
        }
    }
    
    
    return 0;
}

哈希表

存储结构

  1. 开放寻址法
  2. 拉链法

作用

将一个 -109~109的数 通过 x mod 105 映射到 [0,105]

mod 的数一般要取成一个质数

如果发生冲突,(将两个不一样的数,映射成了同样的数)

例子

840. 模拟散列表 - AcWing题库

维护一个集合,支持如下几种操作:

  1. I x,插入一个数 x;
  2. Q x,询问数 x 是否在集合中出现过;

现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

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

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 xx 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109

输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No

拉链法

开一个 一维数组 存储所有的 哈希值

比如我们第一次 将 h(11) 映射到了 3,我们就在 一维数组的 3 下面拉一条链,把 11 存下来,如果我们 又将 h(23)映射到了 3,我们就在 11 的后面存一个 23

#include<iostream>
#include<cstring>
using namespace std;

const int N = 100003;
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;
}
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    int n;
    cin >> n;
    //清空槽
    memset(h,-1,sizeof h);
    
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        if(op[0] == 'I')
        {
            insert(x);   
        }else{
            if(find(x))
            {
                cout << "Yes" << endl;
            }else{
                cout << "No" << endl;
            }
        }
    }
    
    return 0;
}

开放寻址法

假如我们求出来 h(x) = k,如果 k 已经有人了,那么就去下一个坑位,直到我们找到一个没有数据的坑位,就把 x 放进去

那么我们在查找的时候同理,按照 哈希函数 计算出 x 的位置为 k,那么我们找到对应 k 位置的数,如果这个数 != x,那么我们就找 k+1 的数,直到找到 这个数 == x,或者是个空位就说明这个数 不存在

删除的时候,我们一般不会把 x 删掉,而是在数组中打一个标记,证明这个数不存在

#include<iostream>
#include<cstring>
using namespace std;

const int N = 200003,null = 0x3f3f3f3f;
int h[N];

int find(int x)
{
    //如果 x 存在,返回的是 x 存在的位置
    //如果 x 不存在,返回的是 x 应该存在的位置 
    int k = (x % N + N ) % N;
    while(h[k] != null && h[k] != x)
    {
        k++;
        if(k == N)
        {
            k = 0;
        }
        
    }   
    return k;
}
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    int n;
    cin >> n;
    //清空槽
    memset(h,0x3f,sizeof h);
    
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        if(op[0] == 'I')
        {
            int k = find(x); 
            h[k] = x;
        }else{
            int k = find(x);
            if(h[k] == null)
            {
                cout << "No" << endl;
            }else{
                cout << "Yes" << endl;
            }
        }
    }
    
    return 0;
}

字符串前缀哈希法

常用于比较两个字符串是否相等

比如我们有一个字符串 str = “ABCABCDEFXCACWING”

先预处理出来所有前缀的哈希

h[0] = 0

h[1] = “A” 的哈希值

h[2] = “AB” 的哈希值

h[3] = “ABC” 的哈希值

h[4] = “ABCA” 的哈希值

我们将每一个字符串 看作 p进制的数,每一位上的字母,就代表着 P 进制的每一位数字

以 h[3] 为例,他一共有三个字母那就看成有 三位,第一位上的数字是 A,第二位上的数字是 B,第三位上的数字是 C

把 A 当成 1,B 当成 2,C 当成 3,那么 “ABC” 就可以看成 p进制的 123 = 1 x p2+2 x p1+ 3 x p0,这个数可能很大所以最后我们对他 mod Q

那么最后我们就可以把这个数 映射到 0~Q-1 的位置了

一般不能映射成 0 —> 因为会把不同的字符串映射成 同一个数

一般来说 p = 131 或 13331 Q = 264 冲突会比较少

| |


​ L R

我们已知 h[L-1] 和 h[R]的哈希值,如何求 [L,R] 的哈希值

左边是高位,右边是低位

在 h[R] 里 R 就是第 0 位,1 就是 R-1位 pR-1~p0

h[L-1] 里 L-1 就是第 0 位,1 就是 L-2位 pL-2~p0

我们第一步需要将 h[L-1] 这一段往左移,移到和我们的 h[R] 对齐为止

例如 123 和 12345,123左移后 == 12300 和 12345 对齐

h[L-1] x pR-L+1

第二步就是h[R] - h[L-1] x pR-L+1

这样就可以求出来 [L,R] 这一段的哈希值了

小技巧:我们用 unsighed long long 来存所有的 h

所以:h[i] = h[i-1] x p + str[i]

模板

(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;
    }

(2) 开放寻址法
    int h[N];

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


/*
字符串哈希
核心思想:将字符串看成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];
}

练习

841. 字符串哈希 - AcWing题库

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

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

输入格式

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

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

接下来 m 行,每行包含四个整数 l1,r1,l2,r2表示一次询问所涉及的两个区间。

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

输出格式

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

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
#include<iostream>
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];

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

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    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;
}

STL

vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序

pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)

string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址

queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素

priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector, greater> q;

stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素

deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, – 返回前驱和后继,时间复杂度 O(logn)

set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器

map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,–

bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]

​ count() 返回有多少个1

​ any() 判断是否至少有一个1
​ none() 判断是否全为0

​ set() 把所有位置成1
​ set(k, v) 将第k位变成v
​ reset() 把所有位变成0
​ flip() 等价于~
​ flip(k) 把第k位取反

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>

#include<vector>
#include<queue>
#include<stack>
#include<deque>
#include<set>
#include<map>
#include<unordered_map>
#include<unordered_set>
#include <bitset>


using namespace std;

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    //vector
    cout << "vector" << endl;
    vector<int> a;
    vector<int> b(10);
    vector<int> c(10,3);    //有10个数,每一个数都是3
    vector<int> d[10];  //定义 10 个 vector
     
    cout << b.size() << endl;   //元素的个数
    cout << b.empty() << endl;  //是不是空的
    b.clear();  //清空
    cout << b.size() << endl;
    a.push_back(1);     //向最后插入一个数
    a.push_back(2);
    cout << a.front() << endl;  //第一个数
    cout << a.back() << endl;
    //  a.begin()/a.end()   迭代器,第一个位置和最后一个位置,可以看成指针
    for(auto i = a.begin();i != a.end();i++)
    {
        cout << *i << endl;
    }
    
    for(auto x:a)
    {
        cout << x << endl;
    }
    a.pop_back();    //删掉最后一个数
    cout << *(a.begin()) << endl;
    //支持比较运算,根据字典序来比较
    vector<int> aa(4,3),bb(3,3);
    if(aa > bb)
    {
        cout << "Yes" << endl;
    }
    cout << endl;
    
    //pair
    cout << "pair:" << endl;
    pair<int,string> p;
    p={20,"abc"};
    cout << p.first << " " << p.second << endl;
    pair<int,pair<int,int>> pp;
    pp={30,{15,45}};
    cout << pp.first << " " << pp.second.first << " " << pp.second.second << endl;
    cout << endl;
    
    //string
    cout << "string:" << endl;
    string str = "yxc";
    str += 'c';
    cout << str << endl;
    str += "ccc";
    cout << str << endl;
    string str1 = str.substr(0,10);     //超出就全部
    string str2 = str.substr(1);    //从 1 开始的 字串
    cout << str2 << endl;
    cout << str1 << endl;
    cout << str.substr(0,3) << endl;    //从 0 开始 3 个 字符
    cout << str.c_str() << endl;
    cout << endl;
    
    //queue
    cout << "queue:" << endl;
    queue<int> q;
    q.push(1);  //向队尾插入元素
    q.push(2);
    cout << q.front() << endl;  //返回队头元素
    cout << q.back() << endl;   //返回队尾元素
    q.pop();    //弹出队头元素
    cout << q.front() << endl;
    q = queue<int>();   //重新构造一个q
    cout << endl;
    
    //priority_queue
    cout << "priority_queue:" << endl;
    priority_queue<int> heap;   //默认大根堆
    priority_queue<int,vector<int>,greater<int>>heap1;  //小根堆
    heap.push(2);   //插入一个元素
    cout << heap.top() << endl;     //返回堆顶元素
    heap.push(3);
    cout << heap.top() << endl;
    heap.push(1);
    cout << heap.top() << endl;
    heap.pop();     // 弹出堆顶元素
    cout << heap.top() << endl;
    cout << endl;
    
    //stack
    cout << "stack:" << endl;
    stack<int> stk;
    stk.push(1);    //栈顶插入一个元素
    stk.push(2);
    cout << stk.top() << endl;  //返回栈顶元素
    stk.pop();  //弹出栈顶元素
    cout << stk.top() << endl;
    cout << endl;
    
    //deque
    cout << "deque:" << endl;
    deque<int> dq;
    dq.push_back(1);
    dq.push_front(2);
    cout << dq[1] << endl;
    cout << dq.front() << endl;
    cout << dq.back() << endl;
    dq.pop_back();
    cout << dq.back() << endl;
    dq.pop_front();
    cout << dq.front() << endl;
    cout << endl;
    
    //set&multiset
    cout << "set&multiset:" << endl;
    set<int> s;     //不可重复
    multiset<int> ms;   //可以重复
    s.insert(1);
    s.insert(1);
    cout << s.size() << endl;
    ms.insert(1);
    ms.insert(1);
    cout << ms.size() << endl;
    cout << s.count(1) << endl;     //返回某一个数的数量
    cout << ms.count(1) << endl;
    s.erase(1);
    cout << s.count(1) << endl;
    s.insert(1);
    s.insert(3);
    s.insert(2);
    set<int>::iterator it;
    it = s.lower_bound(3);      //返回大于等于 x 的最小的数的迭代器
    cout << *it << endl;
    it = s.upper_bound(3);      //返回大于 x 的最小的数的迭代器
    cout << *it << endl;
    cout << endl;
    
    //map&multimap
    cout << "map&multimap:" << endl;
    map<int,int> map;
    multimap<int,int> mlmap;    //可重复
    map[1] = 1;
    map.insert({2,2});
    map[3] = 3;
    cout << map[1] << endl;
    cout << map[2] << endl;
    cout << map.size() << endl;
    auto mapit = map.begin();
    map.erase(mapit);
    cout << map.size() << endl;
    cout << endl;
    
    //unordered_set&unordered_map&unordered_multimap&unordered_multiset
    //不支持 lower_bound & upper_bound,其余同上
    // crud 复杂度是O(1)
    
    //bitset
    //  假设我们需要开一个 1024个bool 的数组,那么我们需要 1k的内存
    //  如果我们把他压到每一位的话,我们只需要 128B 的内存
    cout << "bitset:" << endl;
    //  存 10000x10000的bool矩阵
    bitset<10000> bit;
    cout << bit.any() << endl;  //判断是否至少有一个 1
    bit.set();  //把所有位变成 1
    //~bit; 取反
    cout << bit.any() << endl;  //判断是否至少有一个 1
    cout << bit.none() << endl;
    bit.reset();    //把所有位变成 0
    cout << bit.none() << endl; //判断是否全为 0
    bit.set(0,1);    //把第 0 位变成 1
    bit.flip(); //等价于 ~
    bit.flip(0); //把第 0 位取反
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ACwing是一个在线的程序设计竞赛训练平台,提供了丰富的算法题目和解题思路。在ACwing上,数据结构是其一个重要的学习内容。根据引用内容,我们可以得出以下观点。 首先,数据结构在解决问题时起到了至关重要的作用。STL(Standard Template Library,标准模板库)是一种常用的C++库,其包含了各种数据结构算法。然而,STL不一定能够满足所有的需求,有些问题可能需要使用组来解决。因此,学习组的方法对于实现各种数据结构是非常重要的。 其次,使用结构体和指针来创建数据结构节点时,每次创建一个新节点都需要执行new Node()操作,这个操作可能会比较慢。特别是在处理大量据的情况下,频繁的new操作可能导致运行时间超时。因此,在设计数据结构时需要考虑到运行效率的问题。 最后,在比赛,通常没有进行O2优化。在这种情况下,纯STL可能会比组模拟的数据结构稍慢一些。这是因为STL包含了更多的细节和抽象,而组模拟的数据结构更为直接和简单。然而,在实际应用,选择使用哪种数据结构还是要根据具体问题的需求和性能要求来决定。 综上所述,ACwing数据结构包含了使用STL和组等不同的方法来实现各种数据结构。根据具体问题的需求和性能要求,选择合适的数据结构方法是很重要的。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【AcWing 算法基础课】 2、数据结构 笔记](https://blog.csdn.net/qq_40915831/article/details/124761243)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值