【算法模板】数据结构——链表、栈、队列、单调栈、单调队列、Trie树、并查集、堆哈希表、字符串哈希

week2 数据结构

链表

算法模板
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
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 ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}
双链表
// 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];
}
例子
example 1 : 单链表

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

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

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

#include <iostream>

using namespace std;

const int N = 100010;


// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], 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 ++ ;
}

// 将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int m;
    cin >> m;

    init();

    while (m -- )
    {
        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
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}
example 2 : 双链表

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

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

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

注意:题目中第 k k k​ 个插入的数并不是指当前链表的第 k k k​ 个数。

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int e[N], l[N], r[N], idx;

// 在节点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];
}

int main()
{
    cin >> m;

    // 0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;

    while (m -- )
    {
        string op;
        cin >> op;
        int k, x;
        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
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
    }

    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

算法思想

先进后出

算法模板
// tt表示栈顶
int stk[N], tt = 0;

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

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

// 栈顶的值
stk[tt];

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

}
例子
example 1 : 模拟栈

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

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

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

#include<iostream>
#include<string.h>

using namespace std;

const int N=100010;

int stk[N],tt;
int n;

int main()
{
    cin>>n;
    while(n--)
    {
        string op;
        int x;
        cin>>op;
        if(op=="push")
        {
            cin>>x;
            stk[++tt]=x;
        }
        else if(op=="query")
        {
            cout<<stk[tt]<<endl;
        }
        else if(op=="pop")
        {
            tt--;
        }
        else if(op=="empty")
        {
            if(tt>0)
                cout<<"NO"<<endl;
            else
                cout<<"YES"<<endl;
        }
    }
}

队列

算法思想

先进先出

算法模板

熟练掌握:能够非常快的默写出来

  1. 普通队列

    // 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)
{

}
例子
example 1 : 模拟队列

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

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

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

#include<iostream>
#include<string.h>

using namespace std;

const int N=100010;

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

int main()
{
    cin>>n;
    while(n--)
    {
        string op;
        int x;
        cin>>op;
        if(op=="push")
        {
            cin>>x;
            q[++tt]=x;,
        }
        else if(op=="query")
        {
            cout<<q[hh]<<endl;
        }
        else if(op=="pop")
        {
            hh++;
        }
        else if(op=="empty")
        {
            if(hh-1==tt)
                cout<<"YES"<<endl;
            else
                cout<<"NO"<<endl;
        }
    }
    
    return 0;
}

单调栈

算法思想
  1. 先想朴素算法

  2. 删掉一些永远也用不到的不必要的值

  3. 看剩下的元素是是否具有单调性

  4. 优化(头、尾、二分········)

    在单调栈中,每一个元素只有一次入栈与出栈,所以最终的复杂度是 O ( n ) O(n) O(n)

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

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

eg : 输入 3 4 2 7 5 输出 -1 3 -1 2 2

把元素一个一个放入栈中,如果后面的元素比前面的元素小,将前面的元素将一直用不到,可以把它从栈中弹出,直到遇到比其小的元素,停止并将其输出,再将后面的元素压入栈中


#include<iostream>

using namespace std;

const int N = 100010;

int stk[N],tt;
int n;
int x;

int main()
{
    cin>>n;
    while(n--)
    {
        cin>>x;
        while(tt && stk[tt]>=x) tt--;//删除不必然的元素
        if(tt==0)
            cout<<"-1"<<" ";
        else//利用单调性输出
            cout<<stk[tt]<<" ";
        stk[++tt]=x;
    }
    
    return 0;
}

单调队列

算法思想

与单调栈相同

  1. 先想朴素算法
  2. 删掉一些永远也用不到的不必要的值
  3. 看剩下的元素是是否具有单调性
  4. 优化(头、尾、二分········)
算法模板
常见模型:找出滑动窗口中的最大值/最小值
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;
}
例子
example 1 : 滑动窗口

给定一个大小为 n ≤ 1 0 6 n≤10^6 n106 的数组。

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

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

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

以下是一个例子:

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

窗口位置最小值最大值
[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

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

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000100;
int n,k;
int a[N],q[N];
int main(){
    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--;
        //把新数放进队列,形成单调递增的队列 
        q[++tt]=i;
        //只要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]]); 
    }
    printf("\n");
    return 0;
}

Trie 树

算法思想

Tire 树又称字典树,高效地存储和查找字符串集合的数据结构

算法模板
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点  -----数组模拟的指针
// cnt[]存储以当前节点结尾的单词数量
// idx 当前使用的下标

// 插入一个字符串
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];
}
例子
example 1 : Tire 字符串统计

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

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

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

#include<iostream>
#include<string.h>

using namespace std;

const int N = 100010;
int son[N][26],cnt[N],idx;
int n;

void insert(string str)
{
    int p=0;
    for(int i=0;str[i];i++)
    {
        int u = str[i]-'a';
        if(son[p][u]==0) 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]==0)
            return 0;
        else 
            p=son[p][u];
    }
    return cnt[p];
}

int main()
{
    cin>>n;
    while(n--)
    {
        char op;
        string str;
        cin>>op;
        if(op=='I')
        {
             cin>>str;
             insert(str);
        }
        else if(op=='Q')
        {
            cin>>str;
            cout<<query(str)<<endl;
        }
    }
    
    return 0;
}

并查集

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

优化后的算法复杂度接近 O ( 1 ) O(1) 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

代码模板

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);
例子
example 1 : 合并集合

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

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

  1. M a b,将编号为 a a a b b b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a a a b b b 的两个数是否在同一个集合中;
#include<iostream>

using namespace std;

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

int find(int x)
{
    if(x!=p[x]) p[x]=find(p[x]);
    return p[x];
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        p[i]=i;
    }
    while(m--)
    {
        char op;
        cin>>op;
        if(op=='M')
        {
            int a,b;
            cin>>a>>b;
            p[find(a)]=find(b);
        }
        else if(op=='Q')
        {
            int a,b;
            cin>>a>>b;
            if(find(a)==find(b))
            {
                cout<<"Yes"<<endl;
            }
            else
            {
                cout<<"No"<<endl;
            }
        }
    }
    
    return 0;
}
example 2 : 连通块中点的个数

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

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

  1. C a b,在点 a a a和点 b b b 之间连一条边, a a a b b b 可能相等;
  2. Q1 a b,询问点 a a a和点 b b b 是否在同一个连通块中, a a a b b b 可能相等;
  3. Q2 a,询问点 a a a 所在连通块中点的数量;
#include<iostream>
#include<string.h>

using namespace std;

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

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

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        p[i] = i;
        s[i] = 1;
    }

    while(m--)
    {
        string op;
        cin>>op;
        int a,b;
        if(op=="C")
        {
            cin>>a>>b;
            if(find(a)==find(b))
                continue;
            s[find(b)] += s[find(a)];
            p[find(a)] = find(b);
        }
        else if(op=="Q1")
        {
            cin>>a>>b;
            if(find(a)==find(b))
                cout<<"Yes"<<endl;
            else
                cout<<"No"<<endl;
        }
        else if(op=="Q2")
        {
            cin>>a;
            cout<<s[find(a)]<<endl;
        }
    }
    
    
    return 0;
}

算法思想

用来维护一个集合,可以快速地取得最大值或最小值

代码模板
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是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);
例子
example 1 : 堆排序

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

#include<iostream>

using namespace std;

const int N = 100010;
int n,m;
int h[N];
int length;

void down(int u)
{
    int t = u;
    if (u * 2 <= length && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= length && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if(t!=u)
    {
        swap(h[t],h[u]);
        down(t);
    }
}

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

void makeheap()
{
    for(int i=n/2;i>0;i--)
    {
        down(i);
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>h[i];
    }
    length=n;
    makeheap();
    while(m--)
    {
        cout<<h[1]<<" ";
        h[1]=h[length];
        down(1);
        length--;
    }
    
    return 0;
}

哈希表(散列表)

算法思想

把一个大空间映射到小空间

代码模板
  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;
}
例子
example 1 : 模拟散列表

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

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

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

1、拉链法

#include<iostream>
#include<cstring>

using namespace std;

const int N=100003;

int h[N];
int e[N],ne[N],idx;
int n;

void insert(int x)
{
    int k=(x%N+N)%N;
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;
}

bool query(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>>n;
    memset(h,-1,sizeof h);
    while(n--)
    {
        char op;
        cin>>op;
        if(op=='I')
        {
            int a;
            cin>>a;
            insert(a);
        }
        else
        {
            int a;
            cin>>a;
            if(query(a)==true)
            {
                cout<<"Yes"<<endl;
            }
            else
            {
                cout<<"No"<<endl;
            }
        }
    }
}

2、开放寻址法

#include<iostream>
#include<cstring>

using namespace std;

const int N=200003,null=0x3f3f3f3f;

int h[N];
int 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;
}


int main()
{
    cin>>n;
    memset(h,null,sizeof h);
    while(n--)
    {
        char op;
        cin>>op;
        if(op=='I')
        {
            int a;
            cin>>a;
            h[find(a)]=a;
        }
        else
        {
            int a;
            cin>>a;
            if (h[find(a)] == null) puts("No");
            else puts("Yes");
        }
    }
}

字符串哈希

算法思想

主要内容为前缀哈希法。把一个字符串看成一个 P P P​进制的数,每一个字符就是该 P P P进制数的某一位

特点:1、不能映射为 0 0 0

​ 2、不存在冲突( P P P 131 131 131, Q Q Q 2 64 2^{64} 264​)

可以利用前缀哈希算法算出所有子串的哈希值,从而可以在 O ( 1 ) O(1) O(1)时间内快速判断两个字符串是否相等。

代码模板
//核心思想:将字符串看成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];
}

例子
example 1 : 字符串哈希

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

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

#include<iostream>
#include<string.h>

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>>n>>m;
    p[0]=1;
    for(int i=1;i<=n;i++)
    {
        cin>>str[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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUAA_Peter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值