二 数据结构

二 数据结构

单链表

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

(1)向链表头插入一个数

(2)删除第k个插入的数后面的数

(3)在第k个插入的数后插入一个数

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

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

输入格式:

第一行M,代表操作次数

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

输入样例:

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;

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

//初始化
void init()
{
	head = -1;  //空默认下标为-1
	idx = 0;
}

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

//将x插到下标是k的点后面
void add(int k,int x)
{
	e[idx] = x, ne[idx] = ne[k], ne[k] = idx, 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];
			remove(k - 1); 
 //第一次插入的数下标为0, 第二次对应为1,所以第k次对应下标为k-1
		}
		else
		{
			cin >> k >> x;
			add(k - 1, x);
		}
	}
	int i;
	for ( i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
	cout << endl;
	return 0;
}

双链表

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

(1)在最左侧插入一个数

(2)在最右侧插入一个数

(3)将第k个插入的数删除

(4)在第k个插入的数左侧插入一个数

(5)在第k个插入的数右侧插入一个数

输入格式:

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

(1)“L x”,表示在链表最左端插入数x

(2)“R x”,表示在链表最右端插入数x

(3)“D x”,表示将第k个插入的数删除

(4)“IL x”,表示在第k个插入的数左侧插入一个数

(5)“IR 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()
{
    // 0表示左端点,1表示右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在下标是k的点的右边,插入x
void add(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
    idx++;
}
//在下标是k的点的左边插入x,可以直接写add(l[k],x); 
//下标k-1的数就是l[k],相当于在下标是k-1的点右边插入x

// 删除第k个点
void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main()
{
    int m;
    cin >> m;
    init();
    while (m--)
    {
        string op;            //字符串op表示输入的操作
        int k, x;
        cin >> op;
        if (op == "L") 
        {
            cin >> x;
            add(0, x);
        }
        else if (op == "R") 
        {
            cin >> x;
            add(l[1], x);  //1是链表结束,l[1]是最后一个数
            //add(l[1], x)表示在最后一个数后面再插入一个数
        }
        else if (op == "D")
        {      //这里要注意,输入的k是下标
            cin >> k;               //但是要删除第k个数,实际上是删除下标为k+1的数,因为idx从2开始,第一个数下标为2。。。。第k个数下标为k+1
            remove(k + 1);          //下面IL和IR同理,第k个数的下标都是k+1
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            add(l[k + 1], x);
        }
        else if (op == "IR") 
        {
            cin >> k >> x;
            add(k + 1, x);
        }
    }
    for (int i = r[0]; i != 1; i = r[i]) {     //链表第一个数是0的右指针指向的节点
        cout << e[i] << ' ';
    }
    cout << endl;
    return 0;
}

栈和队列

栈:先进后出(桶)

队列:先进先出(没底的)

#include <iostream>
using namespace std;
const int N = 100010;

// **************** 栈
int stk[N], tt; //tt栈点
//插入
skt[ ++ tt] = x;
//弹出
tt --;
//判断栈是否为空
if (tt > 0) not empty;
else empty;
//栈顶
skt[tt];



// **************** 队列
//在队尾插入元素,在队头弹出元素
int q[N],hh,tt=-1;
//hh左  tt右
//插入
q[++ tt]=x;
//弹出
hh ++;
//判断队列是否为空
if(hh<=tt) not empty
else empty
//取出队头,队尾元素
q[hh]  q[tt]

思想:

下面两道题的思想就是构造一个严格单调递增的序列

一个用栈顶比较下一个数,满足就替换,输出栈顶

一个用队尾比较下一个数,满足就替换,输出队头


单调栈(少)

返回数组中某一位数前面离它最近且比他小的数

有则输出其值

无则输出 -1

输入用例:

5
3 4 2 7 5

输出:

-1 3 -1 2 2

代码:

这里的 scanf printf 比 cin cout 快很多

#include <iostream>
using namespace std;
const int N = 100010;

int n;
int stk[N], tt; 

int main()
{
	scanf_s("%d", &n);   //比cin快很多

	int i;
	for (i = 0; i < n; i++)
	{
		int x;
		scanf_s("%d", &x);
		while (tt && stk[tt] >= x) tt--;
		if (tt) printf("%d ", stk[tt]);
		else printf("-1 ");
		stk[++tt] = x; //保持了栈的单调递减性质
	}
	return 0;
}

单调队列

例题:滑动窗口

给定一个大小为 n<=10^6 ^的数组

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

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

需要确定并输出滑动窗口谓语每个位置时,窗口中的最大值和最小值。

输入:

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];
//q数组存的是下标
int main()
{
	scanf_s("%d %d", &n, &k);
	int i;
	for (i = 0; i < n; i++) scanf_s("%d", &a[i]);
	int hh = 0, tt = -1;
	for (i = 0; i < n; i++)
	{
		//判断队头是否已经滑出窗口
		if (hh <= tt && i - k + 1 > q[hh]) hh++;
        //滑动窗口只往后移动一位,所以不用while
		while (hh <= tt && a[q[tt]] >= a[i]) tt--;
		q[++tt] = i; 
		if (i >= k - 1) printf("%d ", a[q[hh]]);
        //以防滑动窗口里的数不足k
        //整一个a[N]最后是单调递增,求最小值每次弹出队头
	}
	puts("");

	hh = 0, tt = -1;
	for (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;
}

KMP字符串

给定一个模式串S,以及一个模版串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模版串P在模式串S中多次作为子串出现。

求出模版串P在模式串S中所有出现位置的起始下标。

输入格式

第一行N,表示P的长度
第二行字符串P
第三行M,表示S长度
第四行字符串S
数据范围
1<=N<=104
1<=M<=105

输入样例:

3
aba
5
ababa

输出样例:

0 2

代码:

#include <iostream>
using namespace std;
const int N = 10010, M = 100010;

int n, m;
char p[N], s[M];
int ne[N]; //next数组

int main()
{
	cin >> n >> p + 1 >> m >> s + 1;
    //注意从1开始存放
	int i,j;
	//求next的过程
	for (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 (i = 1, j = 0; i <= m; i++)
	{
		while (j && s[i] != p[j + 1]) j = ne[j];
		if (s[i] == p[j + 1]) j++;
        //匹配成功则输出
		if (j == n)
		{
			printf("%d ", i - n);
			j = ne[j];
		}
	}
	return 0;
}

Trie字符串

Trie:高效的存储和查找字符串集合的数据结构
在这里插入图片描述

题目:

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

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

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

对于每个询问指令 “ Q x”,都要输出一个整数作为结果

1<=N<=2*104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1

代码:

#include <iostream>
using namespace std;
const int N = 100010;
char str[N];
int son[N][26], cnt[N], idx;
//son[N][26]存的是每一个单词每一次出现的字母,相当于一个分支点
//cnt[N]存放单词出现次数,cnt[p]++代表一个单词insert一次
//idx代表每一个分支点,每一个单词的每一个字母都有其的idx
int i;

void insert(char str[])
{
    int p = 0;
    for (i = 0; str[i]; i++) //str[i]!='\0'
    {
        int u = str[i] - 'a'; //a-z映射到0-25
        if (!son[p][u]) son[p][u] = ++idx; //idx=0是总起点不动
        p = son[p][u];
        //p最后代表单词最后一个字母的位置
    }
    cnt[p]++;
}

int query(char str[])
{
    int p = 0;
    for (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()
{
    int n;
    scanf_s("%d", &n);
    while (n--)
    {
        char op[2];
        scanf_s("%s%s", op, 2,str,N);   //固定格式,不这样写报错
        if (op[0] == 'I') insert(str);
        else printf("%d\n", query(str));
    }
    return 0;
}

并查集

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

近乎O(1)

基本原理:

每个集合用一棵树来表示。
树根的编号就是整个集合的编号。
每个节点存储它的父节点,p[x]表示x的父节点

问题1:如何判断树根:if(p[x]==x)

问题2:如何求x的集合编号:while(p[x]!=x) x=p[x];

问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x]=y

优化—路径压缩:查到祖先之后就把px存成祖先(集合中每个元素直接指向根节点

int find(int x)  //返回x的祖宗节点+路径压缩
{
	if (p[x] != x) p[x] = find(p[x]);
	return p[x];
}

合并集合

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

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

  1. “ M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略此操作。
  2. “ Q a b”,询问编号为a和b的两个数是否在同一个集合。

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 i, j;
int n, m;

int p[N];  //p[x]表示x的父节点

int find(int x)  //返回x的祖宗节点+路径压缩
{
	if (p[x] != x) p[x] = find(p[x]);
	return p[x];
}

int main()
{
	cin >> n >> m;
	for (i = 1; i <= n; i++) p[i] = i; 
    //刚开始一个数就是一个集合所以树根就是自己
	while (m--)
	{
		char op[2];int a, b;
		cin >> op >> a >> b;
		if (op[0] == 'M') p[find(a)] = find(b); //把一个集合直接插入到另一个集合,然后同一个根节点
		else {
			if (find(a) == find(b)) puts("Yes");
			else puts("No");
		}
	}
	return 0;
}

连通块中点的数量

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

现在进行m个操作:

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

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

对于每个询问指令“ Q2 a”,输出点a所在连通块中点的数量

输入样例:

5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例:

Yes
2
3

把连接块看作集合

#include <iostream>
using namespace std;
const int N = 100010;
int i, j;
int n, m;

int p[N], num[N];
//num[N]统计集合的点的数量

int find(int x)  //返回x的祖宗节点+路径压缩
{
	if (p[x] != x) p[x] = find(p[x]);
	return p[x];
}

int main()
{
	cin >> n >> m;
	for (i = 1; i <= n; i++)
	{
		p[i] = i;
		num[i] = 1;
	}
	while (m--)
	{
		string op;
		int a, b;
		cin >> op;
		if (op=="C")
		{
			cin >> a >> b;
			if (find(a) == find(b)) continue; 
			num[find(b)] += num[find(a)]; 
            //连接块变大,更新包含点的个数
			p[find(a)] = find(b);
		}
		else if (op=="Q1")
		{
			cin >> a >> b;
			if (find(a) == find(b)) puts("Yes");
			else puts("No");
		}
		else
		{
			cin >> a;
			printf("%d\n", num[find(a)]);
		}
	}
	return 0;
}

完全二叉树

存储:用一维数组存储

  1. x的左儿子:2x
  2. x的右儿子:2x+1
根节点(下标为1)23456789
xx的左儿子x的右儿子x左儿子的左儿子x左儿子的右儿子x右儿子的左儿子x右儿子的右儿子

在这里插入图片描述

函数

down(x)、up(x)两个函数

确保从上至下是从小到大

void up(int u)
{
	while (u / 2 && h[u / 2] > h[u])
	{
		swap(h[u / 2], h[u]);
		u /= 2;
	}
}
void down(int u)
{
    //t是最小值下标
	int t = u;
    //左右儿子是否存在
	if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2; 
	if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
	if (u != t)
	{
		swap(h[u], h[t]);
		down(t);
	}
}
如何手写一个堆:具体操作
1.插入一个数heap[++num]=x ; up(num);
2.求集合当中最小值heap[1];
3.删除最小值heap[1] = heap[num]; num- -; down(1);
4.删除任意一个元素heap[k] = heap[num]; num- -; down(k); up(k);
5.修改任意一个元素heap[k] = x; down(k); up(k);

堆排序

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

1<=m<=n<=105
1<=数列中元素<=109

输出:包含m个整数,表示数列中前m个小的数

输入样例:

4 3
4 1 3 2

输出样例:

1 2 3

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int i, j;
int n, m;

int h[N],num;

void down(int u)
{
	int t = u;
    //左右儿子是否存在
	if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2; 
	if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
	if (u != t)
	{
		swap(h[u], h[t]);
		down(t);
	}
}

int main()
{
	cin >> n >> m;
	for (i = 1; i <= n; i++) scanf_s("%d", &h[i]);
	num = n;

	for (i = n / 2; i; i--) down(i); //排序 近似O(n) 
	//最后一个非叶子节点的索引是 n/2
	while (m--)
	{
		printf("%d ", h[1]);
		h[1] = h[num]; 
        //在每次取出堆顶元素后都会减少,以确保下一次操作不会越界
		num--;
		down(1);
	}
	return 0;
}

模拟堆

维护一个集合,初始时集合为空

  1. “ I x ”,插入一个数x
  2. “PM”,输出当前集合最小值
  3. “DM”,删除当前集合中的最小值
  4. “D K”,删除第k个插入的数
  5. “C k x”,修改第k个插入的数,将其变为x

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

输入样例:

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

输出样例:

-10
6

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 100010;
int i, j;
int n, m;

int h[N], ph[N], hp[N], num;
//h[]存的是元素
//ph[i] 表示第 i 个插入的数在堆中的下标。
//hp[j] 表示堆中下标为 j 的元素是第几个插入的数。
//num是堆的大小

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 up(int u)
{
	while (u / 2 && h[u / 2] > h[u])
	{
		heap_swap(u / 2, u);
		u /= 2;
	}
}

void down(int u)
{
	int t = u;
	if (u * 2 <= num && h[u * 2] < h[t]) t = u * 2; //如果左儿子存在且小于就交换
	if (u * 2 + 1 <= num && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
	if (u != t)
	{
		heap_swap(u, t);
		down(t);
	}
}

int main()
{
	cin >> n;
	while (n--)
	{
		string op;
		int k, x;
		cin >> op;
		if (op=="I")
		{
			cin >> x;
			num++;
			m++;
			ph[m] = num, hp[num] = m;
			//num是整个堆的元素数量,m是插入数个数,映射关系
			h[num] = x;
			up(num);
		}
		else if (op == "PM") printf("%d\n", h[1]);
		else if (op == "DM")
		{
			heap_swap(1, num);
			num--;
			down(1);
		}
		else if (op == "D")
		{
			cin >> k;
			k = ph[k];
			heap_swap(k, num);
			num--;
			down(k), up(k);
		}
		else
		{
			cin >> k >> x;
			k = ph[k];  //将k更新为该数位于堆的下标
			h[k] = x;
			down(k), up(k);
		}
	}
	return 0;
}

哈希表

通过将键(key)映射到表中的某个位置来实现高效的数据检索。

数据范围-109~109 ,映射0到105~106

  1. **哈希函数(Hash Function):**它接受一个键作为输入,并输出一个固定长度的哈希值。
  2. 哈希表存储结构: 哈希表通常是一个数组,每个数组元素称为桶(Bucket)。每个桶可以存储一个键值对,或者采用链表、树等数据结构存储多个键值对,以处理哈希冲突。
  3. 哈希冲突(Hash Collision): 当两个不同的键经过哈希函数映射到相同的索引时,就发生了哈希冲突。解决冲突的常见方法有:
    • 链地址法(Separate Chaining):每个桶使用一个链表来存储具有相同哈希值的键值对。
    • 开放寻址法(Open Addressing):在发生冲突时,尝试在其他桶中寻找空位
  4. 哈希表提供了快速的平均时间复杂度(O(1))用于查找、插入和删除操作。

删除:添加一个bool数组,标记一下

存储结构

题目:模拟散列表

维护一个集合

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

数据范围x:-109~109

1<=N<=105

输入范例:

5
I 1
I 2
I 3
Q 2
Q 5

输出范例:

Yes
No

开放寻址法

一维数组,范围最好是规定数据范围的2~3倍的素数

#include <iostream>
#include <cstring> 
//提供了一系列处理字符串和内存操作的函数声明
//memset函数需要<cstring> 

using namespace std;
const int N = 200003,null=0x3f3f3f3f;  
//开一维数组,范围最好是规定数据范围的2~3倍的素数
//0x3f3f3f3f远大于1e5,不存任何数
int n, m;
int i, j;

int h[N];

int find(int x)
{
	int k = (x % N + N) % N;
	//%N:首先,x 被 N 取模,这将确保 x 的值在取模后范围在 -N+1 到 N-1 之间,包括负数和正数。
	//+N:然后,取模后的结果被加上 N,这样就确保结果始终为正数。
	//%N:最后,再次对结果进行 N 取模,这将确保结果在 0 到 N-1 的闭区间内。
	
	while (h[k] != null && h[k] != x) //坑上有数往后看
	{
		k++;
		if (k == N) k = 0; //最后一个坑位看完了,循环看第一个坑位
	}
	return k;
}
//在插入操作中,若x在哈希表中,k就是下标
//若不在,就是k应该存储的位置

int main()
{
	cin >> n;
	memset(h, 0x3f, sizeof h); 
	//初始化,memset是按照字节,0x3f就代表0x3f3f3f3f
	//memset函数用于设置一块内存区域的值,一般中间写-1或者0
	while (n--)
	{
		string op;
		int x;
		cin >> op >> x;
		int k = find(x);
		if (op == "I") h[k] = x;
		else
		{
			if (h[k]!=null) puts("Yes");
			else puts("No");
		}
	}
	return 0;
}

拉链法

一维数组,数组每个元素都单开一个单链表

#include <iostream>
#include <cstring> //提供了一系列处理字符串和内存操作的函数声明

using namespace std;
const int N = 100003; 
//哈希表数组范围取大于数据范围且最接近的质数,确保冲突最小

int n, m;
int i, j;

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

void insert(int x)
{
	int k = (x % N + N) % N; //使余数是正数
	//可能存的数是负数,那x % N就是负数
	// 但映射到0-1e5,所以要(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 (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); 
    //初始化,单链表指向-1,数组所有的值都初始为-1
	//memset函数用于设置一块内存区域的值,一般中间写-1或者0
	
	while (n--)
	{
		string op;
		int x;
		cin >> op >> x;
		if (op == "I") insert(x);
		else
		{
			if (find(x)) puts("Yes");
			else puts("No");
		}
	}
	return 0;
}

字符串哈希

思想:

  1. 把字符串str每个位置前缀的哈希值存到数组里
  2. 用p进制存,p 存储了 P 的幂次

在这里插入图片描述
在这里插入图片描述


题目:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,

请你判断 [ l1 , r1 ], [ l2 , r2 ]两个区间的字符串子串是否完全相同。

相同输出 Yes,否则No

输入样例:

8 3
aabbaab
1 3 5 7
1 3 6 8
1 2 1 2

输出样例:

Yes
No
Yes

#include <iostream>
using namespace std;
const int N = 100010,P=131;
//P取131或13331 大概率不发生冲突
typedef unsigned long long ULL;

int i, j;
int n, m;
char str[N];
ULL h[N], p[N];
//h存储了每个位置的前缀哈希值,而 p 存储了 P 的幂次
ULL get(int l, int r)
{
	return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
	cin >> n >> m >> str + 1;
	p[0] = 1;
	for (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)) puts("Yes");
		else puts("No");
	}
	return 0;
}

std

vector,变长数组,倍增的思想
string,字符串,substr( ) , c-str( )
quene,队列,push(),front(),pop()
priority_quene,优先队列,push(),top(),pop()
stack,栈,push(),top(),pop()
deque,双端队列
set,map,multiset,multimap,基于平衡二叉树(红黑树),动态维护有序序列
unordered_set,unordered_map,unordered_multiset,unordered_multimap,哈希表
bitset,压位

思想:

  1. 把字符串str每个位置前缀的哈希值存到数组里
  2. 用p进制存,p 存储了 P 的幂次

[外链图片转存中…(img-HtGIDHvC-1710417460150)]

[外链图片转存中…(img-ukNEZsB6-1710417460150)]


题目:给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,

请你判断 [ l1 , r1 ], [ l2 , r2 ]两个区间的字符串子串是否完全相同。

相同输出 Yes,否则No

输入样例:

8 3
aabbaab
1 3 5 7
1 3 6 8
1 2 1 2

输出样例:

Yes
No
Yes

#include <iostream>
using namespace std;
const int N = 100010,P=131;
//P取131或13331 大概率不发生冲突
typedef unsigned long long ULL;

int i, j;
int n, m;
char str[N];
ULL h[N], p[N];
//h存储了每个位置的前缀哈希值,而 p 存储了 P 的幂次
ULL get(int l, int r)
{
	return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
	cin >> n >> m >> str + 1;
	p[0] = 1;
	for (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)) puts("Yes");
		else puts("No");
	}
	return 0;
}

std

vector,变长数组,倍增的思想
string,字符串,substr( ) , c-str( )
quene,队列,push(),front(),pop()
priority_quene,优先队列,push(),top(),pop()
stack,栈,push(),top(),pop()
deque,双端队列
set,map,multiset,multimap,基于平衡二叉树(红黑树),动态维护有序序列
unordered_set,unordered_map,unordered_multiset,unordered_multimap,哈希表
bitset,压位
  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JaneHan_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值