算法学习记录——暑假第二周(3)——Trie树、并查集以及哈希

本文复习内容概括:

Trie树和并查集都是针对集合插入/处理/查询操作所提出的高效处理算法
其中Trie树侧重于字符串集合的维护,并查集侧重于数字集合的合并

而哈希(hash)是将一个大集合按照一定的对应关系映射到小集合中,从而实现一定的优化目的。

字符串哈希是可以高效代替KMP的做法(求循环节除外,这个只能用KMP)。虽然有点复杂,但是实用性比较高。

Trie树

基本思路示意:树的存储方式:声明一个二维数组int son[N][26]。其意义是对于每个节点有26个子节点,但只有当这些节点上有非0值的时候才被视为真的子节点,然后再更新当前位置处理子节点的子节点。【在字符串结尾进行标记可区分abc与abcdf(如图所示)】
在这里插入图片描述

Tire树的插入与查询操作

全局变量声明
char str[N];
int son[N][26],counter[N],idx;//下标是0 的点,既是根节点,又是空节点
//son数组的本质是son[父节点]下有26个可能的分支,没有字母的话就一直是0不会被视为一个分支,
//如果有这个字母的话便创建这个节点给他赋值为非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];//让这个子节点成为下一个节点的父节点
	}
	counter[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 counter[p];
}

并查集

基本思路:

  • 用树来维护集合,树根的编号是整个集合的编号
  • 树每一个节点的父节点都是唯一的
  • 用一个一维数组存放当前节点的父节点是谁

问题:

  1. 如何判断树根:if(p[x] == x){除了树根以外的每一个点的父节点不会是本身}
  2. 如何求x的集合编号?while(p[x] != x) x=p[x]
  3. 如何合并两个集合?两个集合(两棵树)的两个根都是指向自己的,合并集合只需要把其中一个根指向另外一个根即可 p[x]=y

优化:

  • 路径压缩:每当一个点“费尽艰辛”找到了他的根节点的时候,将它与他的根节点直接相连,这样可以在很大程度上避免重复查找。
  • 路径压缩注:由于此函数是通过递归实现的,所以路径压缩过程完成后,那个点到根节点上的所有点的父节点都是根节点。
实现代码
其中p[x]中存放的是节点x的父节点

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

a b集合合并操作:
p[find(a)] = find(b);

查询ab集合是否在同一集合中:
if(find(a) == find(b)) 

哈希表

由于总体值域很大,而映射的目标的范围小,所以一定会产生冲突
冲突即是有不同的数映射到了同一个元素上,这样的话就需要通过方式以解决冲突问题

  • 第一种方法:拉链法
    开一个哈希数组存储所有哈希值。当把某一个数映射到哈希表上的某一个值时,在相应的数组下拉一条链用来储存当前槽上已有的数(未哈希之前的原数)
    每个拉链的表现形式是一个单链表
    ----这个链表一般常用操作是插入和查询。如果需要删除操作,没用必要进行真正意义上的删除,即:
    只需要为每个元素设立一个flag变量,删除操作时改变flag的值,也就是让链表忘记他,把他排除在外
    注:哈希算法是一种期望算法,一般情况下每一条链的长度可看成常数。所以一般情况下哈希表的时间复杂度都很好,为O(1)。
    小技巧:在取区间的长度时,区间的长度最好是一个质数,这样的话冲突的平均概率是最小的。这个结论可以通过数学证明出来(自己可以去要查一下)。

  • 方法二:开放寻址法
    只开一个数组,但是这个数组的长度要开到题目所给数据范围的两倍到三倍 存放所有的数
    处理冲突的方法是通过一个find函数实现的,具体为:
    find函数:返回值有两种情况:
    ---- 第一种是在插入过程中,返回的int值代表find函数已经找到了一个可以放数x的空位,通知主函数把数放到返回值对应的位置上去,跳出while循环的判断条件是当前位置为空值
    ---- 第二种是在查询过程中,返回的int值代表已经在k位置上找到了x,如果没有找到的话此返回位置对应的地方是null,依次判断yes or no,跳出while循环的判断条件是当前位置k上的值==x;如果没有找到x,则会因为该位置上是null而退出

拉链法(核心是单链表及其主要操作)
void insert(int x)
{
	int k=(x%N+N)%N;  //一种哈希方式    由于x%N可能是负数,所以加上一个N(x%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;
}
开放寻址法(null需要提前定义为无穷大并将数组的每一位初始化为无穷大)
int find(int x)
{
	int k=(x%N+N)%N;
	
	while(h[k]!=null&&h[k]!=x)  //当前位置为空值或者h[k]==x时会跳出循环。这两种跳出循环的情况分别是插入和查询
	{
		k++;  
		if(k==N) k=0; //如果while循环到了末尾还是没找到能插的位置就从数组头开始找
	}
	
	return k;
}

字符串哈希

核心思想是预处理前缀哈希值
学习笔记:
在这里插入图片描述
对应代码:

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

const int N=100002,P=131;
typedef unsigned long long ULL;
int n,m;
char str[N];
ULL h[N],p[N];//h[i]数组存放的是前i个字符的hash值(预处理前缀和) || 初始化h[0]=0,因为根据定义
			  //p[i]对应的第i个元素是p的多少次方,这个次方数即是数组值

	

ULL get(int l,int r)  //此函数用来计算[l,r]字符串的哈希值
{
	return h[r]-h[l-1]*p[r-l+1]; //[l,r]字符串哈希的值为:h[r]-h[l]*(p^(k-l+1))
}

int main()
{
	scanf("%d%d%s",&n,&m,str+1); //str[0]被略过
	
	//预处理p数组过程
	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;
		scanf("%d%d%d%d",&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、付费专栏及课程。

余额充值