寒假训练第二周总结

目录

一、单链表

1、算法思路

 2、算法实现

3、完整代码

二、双链表

1、算法思路

2、算法实现

       (1)初始化

        (2)插入的通用模板

        (3)删除通用模板

三、模拟栈和模拟队列

1、模拟栈

        (1)算法实现

        (2)题目描述

2、模拟队列

        (1)算法实现

        (2)题目描述

四、单调栈和单调队列

1、单调栈

             (1)常见模型

         (2)题目描述

2、单调队列

        (1)常见模型

五、KMP

1、题目描述

2、算法优化       

六、Trie

1、算法实现

2、题目描述

七、并查集

1、算法实现

2、算法核心

3、例题

        例题1

        例题2

八、堆

1、算法实现

2、算法操作

3、例题

        例题1

        例题2

九、Hash表 

1、拉链法

        (1)算法思路

        (2)问题处理

2、开放式寻址发

        (1)算法思路

        (2)算法实现

3、字符串哈希方式

(1)算法思路

(2)算法实现

(3)题目描述


一、单链表

1、算法思路

        用数组来模拟单链表

        链表储存方式:开一个数组 e[i] 来储存当前位置的值,再开一个数组 ne[i] 来储存下一个节点的地址。

        头部定义一个变量 head 来充当整个单链表的头部,idx 作为每次数据插入时指向下一位置的变量


 2、算法实现

链表完成的操作

        在完成数据前要先将变量初始化,头部从-1开始,新的数据插入从0开始

head=-1,idx=0;

1.在头部插入一个新数据

       直接将新数据放到 head 前面成为新的头部数据,将下一节点指向头部,更新头部为新数据的位置。

void add_to_head(int x)
{
	e[idx] = x;
    //把数据x储存
	ne[idx] = head;
    //将当前位置的下一位置指向头部
	head = idx++;
    //更新头部位置
}

2.在第 k 节点个后面插入新数据

        在第 k 个位置则要完成两次连线,先将新数据连到 k 位置指向下一个位置的数据上,再将k位置指向的位置更新到新插入的数据位置。

void add(int k, int x)
{
	e[idx] = x;
    //将数据x储存
	ne[idx] = ne[k];
    //将下一个位置指向位置k的下一个位置
	ne[k] = idx++;
    //更新第k个位置指向插入数据的位置
}

3.删除链表中第 k 个后面的数据

        不用处理第 k 个位置后的数据,直接将第 k 个位置数据指向下一个的下一个数据的位置。

void remove(int k)
{
	ne[k] = ne[ne[k]];
    //第k个位置指向第k个位置的下一个位置的下一个位置
}

3、完整代码

#include<iostream>
using namespace std;

const int N = 100010;

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

void init()
{
	head = -1;
	idx = 0;
}

void add_to_head(int x)
{
	e[idx] = x;
	ne[idx] = head;
	head = idx++;
}

void add(int k, int x)
{
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx++;
}

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

	for (int i = head; i != -1; i = ne[i])
		cout << e[i] << " ";
	cout << endl;
	return 0;
}

二、双链表

用数组模拟实现双链表

1、算法思路

        左边首部为 head=0,右边首部为 tail=1;

        链表储存方式:定义 l[i] 来储存现在位置的左边数据位置,定义 r[i] 来储存现在位置的右边数据的位置,用 e[i] 来储存当前位置的数据;

        新数据从 idx=2 开始读入。


2、算法实现

1.在最左侧插入一个数;

2.在最右侧插入一个数;

3.将第 k 个插入的数删除;

4.在第 k 个插入的数左侧插入一个数;

5.在第 k 个插入的数右侧插入一个数 。


       (1)初始化
void init()
{
	r[0] = 1;
    //指向右边的数组从0开始,右边指向头部位置1
	l[1] = 0;
    //指向左边的数组从1开始,左边指向头部位置0
	idx = 2;
    //插入的位置则从2开始
}

        (2)插入的通用模板

1.在最左侧插入一个数据x,则将位置k指向左边的头节点0;

2.在最右侧插入一个数据x,则将位置k指向左边;

3.在第 k 个插入的数左侧插入一个数x,位置为 l[k+1] 因为idx从2开始插入,则k要加 1 ;

4.在第 k 个插入的数右侧插入一个数x,位置为 k+1 。

void add(int k, int x)//在右边插入一个点
{
	e[idx] = x;
    //将新数据储存
    
    //添加新数据的左右链接
	r[idx] = r[k];
    //新数据的右边指向第k位置指向右边的位置
	l[idx] = k;
    //新数据的左边指向第k个位置

    //更新数据两边与当前节点的链接
	l[r[k]] = idx;
    //先将第k个位置的右边数据的左边指向新节点
	r[k] = idx;
    //再将第k个位置的右边指向新节点
    
    //更新下一个数据的位置
	idx++;
}

        (3)删除通用模板

        直接将第k数据两边的数据互相链接

void remove(int k)
{
	l[r[k]] = l[k];
    //将第k位置的右边的左边指向k位置的左边
	r[l[k]] = r[k];
    //将第k位置的左边的右边指向k位置的右边
}

完整代码

#include<iostream>

using namespace std;

const int N = 1e5+10;

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

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

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

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

int main()
{
	cin >> m;

	init();

	while (m--)
	{
		int x, k;
		string op;
		cin >> op;

		if (op == "L")
		{
			cin >> x;
			add(0, x);
		}
		else if (op == "R")
		{
			cin >> x;
			add(l[1], x);
		}
		else if (op == "D")
		{
			cin >> k;
			remove(k + 1);
		}
		else if (op == "IL")
		{
			cin >> k >> x;
			add(l[k + 1], x);
		}
		else
		{
			cin >> k >> x;
			add(k + 1, x);
		}
	}

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

	return 0;
}

三、模拟栈和模拟队列

1、模拟栈

        (1)算法实现

        栈的储存方式是每次在尾部加入一个数据,删除也只能从尾部删除最后一个数据。

用数组 stk[i] 来储存每个数据,用 tt 来指向尾部数据的位置。

        实现的操作

1.插入新数据:stk[++tt] = x;

2.删除尾部数据:tt--;

3.判断栈中是否为空:tt >= 0 不为空,反之则为空。

4.查询栈顶:stk[tt];


        (2)题目描述

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

  1. push x – 向栈顶插入一个数 xx;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。 现在要对栈进行 MM 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。

Input

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

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

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

Output

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

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

Samples

输入数据 1

10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty

输出数据 1

5
5
YES
4
NO

完整代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int m;
int stk[N], tt = -1;

int main()
{
	cin >> m;

	while (m--)
	{
		int x;
		string op;
		cin >> op;

		if (op == "push")
		{
			cin >> x;
			stk[++tt] = x;
		}
		else if (op == "pop") tt--;
		else if (op == "empty") cout << (tt >= 0 ? "NO" : "YES") << endl;
		else cout << stk[tt] << endl;
	}
	return 0;
}

2、模拟队列

        (1)算法实现

        队列储存为每次从队伍尾部插入新的数据,从头部弹出数据。

用数组 q[i] 作为整个数据的储存,变量 hh 代表头部 tt 代表尾部。

         实现的操作

1.队尾插入新数据:stk[++tt] = x;

2.弹出头部数据:hh++;

3.判断队列中是否为空:hh<=tt 不为空,反之则为空;

4.查询对头元素:q[hh];


        (2)题目描述

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

  1. push x – 向队尾插入一个数 xx;
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。 现在要对栈进行 MM 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。

Input

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

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

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

Output

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

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

Samples

输入数据 1

10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6

输出数据 1

NO
6
YES
4

完整代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int m;
int q[N], hh, tt = -1;

int main()
{
	cin >> m;

	while (m--)
	{
		int x;
		string op;
		cin >> op;

		if (op == "push")
		{
			cin >> x;
			q[++tt] = x;
		}
		else if (op == "pop") hh++;
		else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;
		else cout << q[hh] << endl;
	}

	return 0;
}

四、单调栈和单调队列

1、单调栈

             (1)常见模型

        找出每个数字左边离它最近的比它大/小的数。

模板:

int tt;
for (int i = 1; i <= n; i++)
{
    while (tt && check(q[tt], i)) tt--;
    stk[++tt];
}


         (2)题目描述

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

Input

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

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

1≤N≤1051≤N≤105 1≤数列中元素≤1091≤数列中元素≤109

Output

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

Samples

输入数据 1

5
3 4 2 7 5

输出数据 1

-1 3 -1 2 2

AC代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n;
int stk[N], tt;

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);

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

2、单调队列

        (1)常见模型

        找出滑动窗口中的最大值/最小值。

模板:

int hh, tt = -1;
for (int i = 0; i < n; i++)
{
    while (hh <= tt && check(q[hh])) hh++;
    while (hh <= tt && check(q[tt], i)) tt--;
    q[++tt] = i;
}


        (2)题目描述

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

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

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

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

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

Input

输入包含两行。

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

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

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

Output

输出包含两个。

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

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

Samples

输入数据 1

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

输出数据 1

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

 AC代码

#include<iostream>

using namespace std;

const int N = 1e6 + 10;

int n, k;
int a[N], q[N];

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);

	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

1、题目描述

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

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

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

Input

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S

1≤N≤1051≤N≤105

1≤M≤1061≤M≤106

Output

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

Samples

输入数据 1

3
aba
5
ababa

输出数据 1

0 2

暴力思路

        定义两个数组分别储存两个字符串,通过循环嵌套的方式完成每个数据的匹配,从而找出每个位置,时间复杂度很大。

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[i])
        {
            flag = false;
            break;
        }
}

2、算法优化       

        优化思路:

        相比暴力每次移动一位,优化到去除中间每次移动必定不能成功匹配的位置,即最大移动多少,使得在一个模板字符串的长度里,模板串的前面一部分和要进行匹配的字符串的最后一部分相匹配,即最大的移动。

        算法实现:

        在原来的基础上新增一个数组 next[i] 用来处理模板字符串,使得每次重复的字符找到第一次出现字符的位置。

for (int i = 2, j = 0; i <= n; i++)
{
	while (j && p[i] != p[j + 1])  //判断当前两个位置是否相等
        j = ne[j];   //不相等寻找 next[] 数组中的前位置再进行判断
    //直到找到和当前位置相等的第一次出现的位置的前面位置

	if (p[i] == p[j + 1]) j++;
    //若当前i数据和j+1指向的数据相等

	ne[i] = j;
    //读入当前位置的指向
}

        匹配要处理的字符串

for (int 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);  
        //位置从0开始,输出 i-n 作为整个位置的开始
        
		j = ne[j]; 
        //回到此位置在模式串里第一次出现的位置上
	}
}

AC代码

#include<iostream>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
char p[N], s[M];
int ne[N];

int main()
{
	cin >> n >> p + 1 >> m >> s + 1;

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

	for (int 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

        高效地储存和查找字符串集合的数据结构。

1、算法实现

        储存字符串:用数组完成数据的储存,每个数组的位置储存一个,当读完整个数组后再最后一个位置添加一个标识符来表示这是一个完整的字符串,用 son[i] [i] 来储存所有字符串,用cnt[i] 数组来打上标识符,即在第i位置结束的字符串有几个,用 idx 指向下一次读入的位置。

void insert(char str[])
{
	int p = 0;
    //头部从0开始

	for (int i = 0; str[i]; i++) //处理数据为26个小写字母
	{
		int u = str[i] - 'a'; //用数字来取代字符,充当每一层的位置,找到第一个位置的数据
		if (!son[p][u]) son[p][u] = ++idx;  //如果当前位置没有数据,则添加
		p = son[p][u]; 将层数移动到下一个指向的位置
	}

	cnt[p]++; 
    //尾部加1,表示一个字符串在此结束。
}

        查询操作:遍历一遍每一个位置是否都存在,和最后的位置是否有标识符来判断是否存在查询的字符串和有几个这样的字符串。

int query(char str[])
{
	int p = 0;
    //顶部从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];  //返回当前位置有几个字符串结束
}

2、题目描述

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

  1. I x 向集合中插入一个字符串 xx;
  2. Q x 询问一个字符串在集合中出现了多少次。 共有 NN 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

Input

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

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

1≤N≤2∗1041≤N≤2∗104

Output

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

每个结果占一行。

Samples

输入数据 1

5
I abc
Q abc
Q ab
I ab
Q ab

输出数据 1

1
0
1

AC代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int son[N][26], cnt[N], idx; //下标是0的点,即使根节点,又是空节点

char str[N];

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

	int n;
	cin >> n;

	while (n--)
	{
		char op[2];
		cin >> op >> str;
		if (op[0] == 'I') insert(str);
		else cout << query(str) << endl;
	}

	return 0;
}

七、并查集

         1.将两个集合合并

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

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


2、算法核心

        优化:路径压缩

找到每个节点的头部,即树根位置

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

3、例题

        例题1

Description

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

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

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

Input

第一行输入整数 nn 和 mm。

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

1≤n,m≤1051≤n,m≤105

Output

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

每个结果占一行

Samples

输入数据 1

4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出数据 1

Yes
No
Yes

 AC代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n,m;
int p[N];

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

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (int 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); 
        //将a的根部链接b的根部,实现两个集合的合并
   
		else
		{
			if (find(a) == find(b)) puts("Yes"); 
            //判断a和b的根部是否相等来判读是否是在一个集合里面
			else puts("No");
		}
	}

	return 0;
}

        例题2

Description

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

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

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

Input

第一行输入整数 nn 和 mm。

接下来 mm 行,每行包含一个操作指令,指令为 C a bQ1 a b 或 Q2 a 中的一种。

1≤n,m≤1051≤n,m≤105

Output

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

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

每个结果占一行。

Samples

输入数据 1

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

输出数据 1

Yes
2
3

        相比上一个题目,本题目增加了新的操作,询问点a中联通块的数量,要完成此操作,则要新开一个数组 s[] 来储存每个集合里面的数量,而对于数据处理,每次询问都通过 find() 函数找到根部,所有只需要维护根部位置的 s[] 有意义即可;

        s[] 的处理,在输入每个数据时,将每个位置的 s[] 数组预处理为1,在处理合并集合的时候一个根部的数值加上另一个根部的数值即可完成根部数值的维护,即s[find(b)] += s[find(a)]


AC代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int p[N], 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;
        //处理 s[] 数组
	}

	while (m--)
	{
		int a, b;
		char op[5];
        //读入每次操作

		cin >> op;

		if (op[0] == 'C')
		{
			cin >> a >> b;
			if (find(a) == find(b)) continue;   
            //特判一下两个位置的根部是否一致
            
			s[find(b)] += s[find(a)];
            处理b的根部,使其数值加上a根部的数值
        
			p[find(a)] = find(b);
		}
		else if (op[1] == '1')
		{
			cin >> a >> b;
			if (find(a) == find(b)) puts("Yes");
			else puts("No");
		}
		else
		{
			cin >> a;
			cout << s[find(a)] << endl; 
            //输出位置a的根数的 s[] 数组所存储的值
		}
	}

	return 0;
}

八、堆

        小根堆:数据由父节点和子节点组成,每个父节点都有两个子节点,数据的顶部是整个数据最小的值小根堆,每一个点都小于两边的字节点的值,父节点和子节点的关系为u  u*2  u*+1。 

1、算法实现

        定义 down() 函数实现数据的向下比较,使数据的上移动;

        定义up()来实现函数的向上比较,使数据向上移动。


2、算法操作

        1.插入一个数 

        heap[++size] = x; up(size);   

        在数据的最下面插入一个数,up() 一遍这个数据。

        2.求集合当中的最小值 

       heap[1];   顶部数据就是最小的数据

       3.删除最小值

       heap[1] = heap[size]; size--; down(1) ;

      将最后一个数覆盖第一个数,再把最后一个数删掉,之后down(1)一遍。

       4.删除任意一个元素

      heap[k] = heap[size]; size--; down(k); up(k);

      把最后一个数覆盖那个数,删除最后一个数,再将覆盖的那个数down(k)和up(x)一遍

      5.修改任意一个元素

      heap[k] = x; down(k); up(k);

      先把数据进行更改,再将那个数down(k)和up(k)一遍


3、例题

        例题1

Description

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

Input

第一行包含整数 nn 和 mm。

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

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

Output

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

Samples

输入数据 1

5 3
4 5 1 3 2

输出数据 1

1 2 3

AC代码

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], s;

void down(int u)
{
	int t = u;
	if (u * 2 <= s && h[u * 2] < h[t]) t = u * 2;  
    //判断父节点和左边子节点的大小关系

	if (u * 2 + 1 < s && 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 (int i = 1; i <= n; i++)
		cin >> h[i];

	s = n; 

	for (int i = n / 2; i; i--) down(i);  //处理数据
    //大小为n/2,高度为1

	while (m--)
	{
		cout << h[1] << " ";
        //输出最小值
        
        //更新头部
		h[1] = h[s];
        //将最后一个数覆盖头部

		s--;
        删除最后一个数

		down(1);
        //处理头部数据
	}
	return  0;
}

        例题2

Description

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

  1. I x,插入一个数 xx;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 k 个插入的数;
  5. C k x,修改第 k 个插入的数,将其变为 xx; 现在要进行 N 次操作,对于所有第 22 个操作,输出当前集合的最小值。

Input

第一行包含整数 NN。

接下来 NN 行,每行包含一个操作指令,操作指令为 I xPMDMD k 或 C k x 中的一种。

1≤N≤1051≤N≤105 −109≤x≤109−109≤x≤109 数据保证合法。

Output

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

每个结果占一行。

Samples

输入数据 1

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

输出数据 1

-10
6

        题目分析:

本题目要进行删除和修改第k个数,则要建两个新的数组 ph[k], hp[k] 分别储存第k个插入的点的下表是什么,和第k个点是第几个插入的点,即 ph[j]=k, hp[k]=j 。


AC代码

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

using namespace std;

const int N = 1e5 + 10;

int h[N], hp[N], ph[N], s;

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 <= s && h[u * 2] < h[t]) t = u * 2;
	if (u * 2 + 1 <= s && 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 / 2, u); //交换父节点和子节点的数据,实现数据的上移
		u /= 2; //更新位置再次进行判断
	}
}

int main()
{
	int n, m = 0;
	cin >> n;

	while (n--)
	{
		int k, x;
		char op[10];
		cin >> op;

		if (!strcmp(op, "I"))
		{
			cin >> x;
			s++; 
			m++;
            //更新两个下表
			ph[m] = s, hp[s] = m; 
            //s是堆的最后的位置,m是当前第s个插入位置的ph[]的地址
			h[s] = x;  //插入的值
			up(s); //进行向上操作
		}
		else if (!strcmp(op, "PM")) cout << h[1] << endl; //输出堆顶最小值
		else if (!strcmp(op, "DM"))
		{
			heap_swap(1, s);  //删除堆顶最小值。
			s--;
			down(1);
		}
		else if (!strcmp(op, "D"))
		{
			cin >> k;
			k = ph[k];  //找到第k个插入的数的下表
			heap_swap(k, s);  //交换
			s--; //删除尾元素
			down(k), up(k); //down和up只会进行其中的一个
		}
		else
		{
			cin >> k >> x;
			k = ph[k];  //找到第k个插入的数的下表
			h[k] = x; //修改第k个数据的数值
			down(k), up(k);
		}
	}

	return 0;
}

九、Hash表 

        将一堆复杂的数据映射到 0—N 的数里面

                储存结构:        (1)拉链法   

哈希表:                         (2)开放寻址法

                字符串哈希方式


 题目描述

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

  1. I x,插入一个数 xx;
  2. Q x,询问数 xx 是否在集合中出现过; 现在要进行 NN 次操作,对于每个询问操作输出对应的结果。

Input

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

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

1≤N≤1051≤N≤105 −109≤x≤109−109≤x≤109

Output

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

Samples

输入数据 1

5
I 1
I 2
I 3
Q 2
Q 5

输出数据 1

Yes
No

1、拉链法

        (1)算法思路

        构造一个哈希函数来实现数据的映射 h[x]  将-10^9-10^9的数映射到0-10^5的范围内

哈希函数的构造:x mol 10^5    (mol的数最好取成质数,才能减少冲突的发生)

产生的问题:冲突   (两个数会映射到同一个位置)

        (2)问题处理

        1.寻找边界范围:

        找到第一个大与100000的质数

#include<iostream>

using namespace std;

int main()
{
	for (int i = 100000;; i++)
	{
		bool flag = true;
		for(int j=2;j*j<i;j++)
			if (i % j == 0)
			{
				flag = false;
				break;
			}
		if (flag)
		{
			cout << i << endl;
			break;
		}
	}

	return 0;
}

         2.冲突的处理:

        数据处理:在每个链子上的槽上拉再一个链来储存每个冲突的映射数据

类比单链表的方式,开 e[] 和 ne[] 数组来分别储存当前位置的数据和指向下一位置的数据。

        3.执行的操作:

        添加:先用哈希函数h(x) 查找映射在哪一个槽上,在槽上的链子上添加上数据;
        查询:用哈希函数h(x) 查找在哪一个槽上,循环一遍看看这个链子里面有没有这个数据;
        删除:先查找这个数,之后在其位置上打上一个标识符。(不常用)


AC代码

#include<iostream>
#include<cstring>

using namespace std;

const int N = 1e5 + 3; 
//第一个大于100000的质数

int h[N], e[N], ne[N], idx;
//e[]和ne[]与单链表的一样,分别储存当前点的值,和下一个点的下表

void insert(int x)//插入操作
{
	int k = (x % N + N) % N;  //哈希函数,找到x的位置
    //负数的mol结果为负数,正数的mol为正数
	//后面加上N再 mol N 保证k值为正数

    //单链表相同操作,完成数据的插入
	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()
{
	int n;
	cin >> n;

	memset(h, -1, sizeof h);
	//定义一个int类型的数组h,每个字节为-1,-1是空指针。

	while (n--)
	{
		char op[2];
		int x;
		cin >> op >> x;
		if (op[0] == 'I') insert(x);
		else
		{
			if (find(x)) puts("Yes");
			else puts("No");
		}
	}

	return 0;
}

2、开放式寻址发

        (1)算法思路

        还是先构造哈希函数来映射数据,但数组范围开到原数据的2到3倍,冲突的概率最低。

        (2)算法实现

        整体思路:

        寻找位置,看一下当前位置有没有,要是有则去下一个,直到找到一个每有的位置。

        1.添加:找到第k个坑位,往后找,直到找到一个空的坑位;

        2.查找:找到第k个坑位,如果当前坑位有人但表示x则继续往后找,如果当前坑位有人是x,则找到了,如果当前坑位没人,则x不存在;

        3.删除:先进行查找,找到后在位置上打上一个标识,表示已经被删除了(不常用)。


AC代码

#include<iostream>
#include<cstring>

using namespace std;

const int N = 2e5 + 3, null = 0x3f3f3f3f;
//定义一个不在数据范围的值null 大于1e9

int h[N];

int find(int x)
{
	int k = (x % N + N) % N;

	while (h[k] != null && h[k] != x) //如果当前坑位有人,且不等于x
	{
		k++;
		//移动到下一个位置
		if (k == N) k = 0;
		//如果到达最后一个位置了,就看第一个坑位
	}

	return k;
	//如果k在哈希表当中的话,k就是x的下标;如果k不在哈希表当中的话,k就是x存储的位置
}

int main()
{


	int n;
	cin >> n;

	memset(h, 0x3f, sizeof h);
    //定义一个int类型的h数组,数组的每个字节为03xf

	while (n--)
	{
		char op[2];
		int x;
		cin >> op >> x;

		int k = find(x);
		if (op[0] == 'I') h[k] = x;
		else
		{
			if (h[k] != null) puts("Yes");
			else puts("No");
		}
	}

	return 0;
}

3、字符串哈希方式

(1)算法思路

        给出一个字符串:str="ABCABCDEYXC"

        预处理所有前缀的哈希值:

        h[0]:特殊定义,前0个字符串的哈希
        h[1]="A" 前一个字母的哈希值
        h[2]="AB"前两个字母的哈希值
        h[3]="ABC"前三个字母的哈希值
        h[4]="ABCA"……
        以此类推……


        如何定义哈希值:
将字符串看成p进制的数
        例如:A  B  C  D  
                   1  2   3  4   
转化成10进制数为:1×0p^3+2×p^2+3×p^1+4×p^0
因为转化成10进制的数有的会很大,无法储存起来,则要mol上一个Q,使整个数字映射到0到Q-1的范围里面

    哈希值的转化:(1×0p^3+2×p^2+3×p^1+4×p^0)mol Q


(2)算法实现

          预处理:h[i]=h[i-1]*p+str[i]

        求L到R的哈希值:
        已知 h[R] 1到R的哈希值      h[L-1] 1到L-1的哈希值
        在整个哈希值当中左边是高位,右边是低位。将 h[l-1] 向右移动到和R对齐的位置,让 h[R] 减去 h[L-1]*p^R-L+1,所得值即为L-R的哈希值。

         求区间值公式:h[R]-h[L-1]*p^R-L+1;

        取模:用 unsing long long 来储存所有的h,溢出表示取模。

注意:

        (1)字符串的值不能映射成0  

        (2)Rp足够好,不用处理冲突当

(p=131或者p=13331的时候Q=2^64;此时99.99%都不会发生冲突)


(3)题目描述

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

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

Input

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

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

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

注意,字符串的位置从 11 开始编号。 1≤n,m≤1051≤n,m≤105

Output

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

每个结果占一行。

Samples

输入数据 1

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

输出数据 1

Yes
No
Yes

AC代码

#include<iostream>

using namespace std;

typedef unsigned long long ULL; 
//简便定义

const int N=1e5+10,P=131; //P为P进制
//也可以取P=13331

int n,m;
char str[N];
//原字符串

ULL h[N],p[N]; 
//第一个数组h[]储存表前缀的哈希值
//第二个数组p[]储存p的几次方

ULL get(int l,int r)
{
    //计算l到r之间的哈希值    
    return h[r]-h[l-1]*p[r-l+1];
}

int main()
{
    scanf("%d%d%s",&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)) puts("Yes");
        else puts("No");
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值