哈希小题两则 —— 哈希集合、哈希搜索路径

E - Prefix Equality —— 集合哈希

题意
给定一个长度为 n 的数列 A,数列 B。
有 m 次询问,每次询问给出两个位置 x, y:

  • 数列 A 的前 x 个位置构成的集合 与 数列 B 的前 y 个位置构成的集合 是否完全相同?(集合不考虑元素顺序和元素重复)

1 ≤ N , Q ≤ 2 × 1 0 5 1 \leq N, Q \leq 2 \times 10^{5} 1N,Q2×105
1 ≤ a i , b i ≤ 1 0 9 1 \leq a_{i}, b_{i} \leq 10^{9} 1ai,bi109 1 ≤ x i , y i ≤ N 1 \leq x_{i}, y_{i} \leq N 1xi,yiN

思路
判断两个集合是否相同,可以将一个集合哈希成一个数,直接 O(1) 判断两个数是否相同。
因为不考虑两个集合是否相同,所以哈希的时候不能像 字符串哈希 那样进制类的哈希,而是采取元素相加取模的方式:

  • 将一个数哈希成比较大的数,然后集合中的所有数相加就将这个集合哈希成了一个值。

而如何哈希能够将冲突减少到最小呢?这里是一种方法:
在这里插入图片描述
( s s s 是哈希后的集合, a a a 是集合中的一个元素, P P P 是模数)

这道题就可以用这种方法 A 掉。

Code

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
#define endl '\n'

const int N = 200010, mod = 8128812800000059;
int T, n, m;
int a[N], f1[N], f2[N];
set<int> st;

signed main(){
	Ios;
	cin >> n;
	
	int s = 0;
	for(int i=1;i<=n;i++)
	{
		int x;cin >> x;
		if(st.find(x) == st.end()){
			s = (s + x * (x+1347)%mod * (x+9185)%mod) % mod;
		}
		f1[i] = s;
		st.insert(x);
	}
	
	st.clear(); s = 0;
	for(int i=1;i<=n;i++)
	{
		int x;cin >> x;
		if(st.find(x) == st.end()){
			s = (s + x * (x+1347)%mod * (x+9185)%mod) % mod;
		}
		f2[i] = s;
		st.insert(x);
	}
	
	cin >> m;
	while(m--)
	{
		int x, y;cin >> x >> y;
		if(f1[x] == f2[y]) cout << "Yes\n";
		else cout << "No\n";
	}
	
	return 0;
}

图像存储 —— dfs / bfs 路径哈希

题意
给定 n*m 的 01 矩阵,相邻的所有 1 构成一个连通块。
如果两个连通块通过上下左右平移可以完全重合,那么这两个连通块是同一种类的。
问,连通块的个数,连通块的种类数。

思路
连通块的个数好搞,bfs 或者 dfs 填充一下就行。

关键是如何判断两个连通块是不是同一种类的,这里做法是哈希 bfs 或者 dfs 的路径。
因为是从上到下,从左到右依次判断一个位置是否已经被标记(如果没有标记就递归标记这个连通块),所以每次第一个走的位置一定是每个连通块最左上角的位置。从这个位置出发,进行 bfs 或者 dfs 遍历连通块的所有位置,按照每一次遍历都有一个方向,将一个连通块遍历的所有方向记录下来便是这个连通块遍历的路径。

如果两个连通块相同的话,遍历路径一定相同;但是遍历路径相同,两个连通块却不一定相同。所以也要采取策略来减少冲突。

有两种遍历策略,dfsbfs

DFS思路:
为什么会有冲突呢?
考虑这样的情况:可能一条路走到头,得到的方向序列为012012;还有可能是一条路走到头,得到方向序列0120,然后回溯过来走另一条路得到方向序列12。那么这两种图形的方向序列是相同的,但是图形却不相同。
为了避免这种情况,可以遍历走到头的情况,方向序列后面加个字符 #

此外,考虑这样的情况:从上到下依次走过(1, 2), (2, 2), (3, 2), (4, 2)位置,后面不能走了,然后回溯到 (2, 2) 位置,再走 (2, 1) 位置。这样得到的方向序列是 下下下左;而如果有另一种图形,也是从上到下走过4个位置,但是回溯到(1, 2)位置,再走(1,1)位置。这样得到的方向序列也是 下下下左。方向序列一样,但是图形不同。
为了避免这种情况,可以在方向序列后面每回溯一次加上一个 $

Code

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define endl '\n'
map<string,int> mp;

const int N = 2010, mod = 1e9+7;
int T, n, m;
int a[N][N];
int f[N][N];
int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int cnt, ans;
string s;

void dfs(int x, int y)
{
	int flag = 0;
	for(int i=0;i<4;i++)
	{
		int tx = x + dir[i][0], ty = y + dir[i][1];
		if(tx < 1 || ty < 1 || tx > n || ty > m) continue;
		if(f[tx][ty] || !a[tx][ty]) continue;
		
		f[tx][ty] = 1;
		flag = 1;
        
		s += char(i + '0');
		dfs(tx, ty, i);
		s += '$';
	}
	if(!flag) s += '#';
}

signed main(){
//	Ios;
	while(cin >> n >> m && n && m)
	{
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++)
			{
				f[i][j] = 0;
				char c;cin >> c;
				a[i][j] = c-'0';
			}
		}
		
		mp.clear();
		
		cnt = 0, ans = 0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++)
			{
				if(f[i][j] || !a[i][j]) continue;
				++cnt;
				f[i][j] = 1;
				
				s = "";
				dfs(i, j);
				mp[s] = 1;
			}
		}
		
		cout << cnt << " " << mp.size() << endl;
	}
	
	return 0;
}

BFS思路
每次遍历四个方向判断是否可以拓展,如果可以,加入队列,将此方向加到方向序列。
同样可能出现冲突,所以每次遍历四个方向之后,方向序列之后加上字符#

Code

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<string,int> mp;

const int N = 2010, mod = 1e9+7;
int T, n, m;
int a[N][N];
int f[N][N];
int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int cnt, ans;
string s;

void bfs(int stx, int sty)
{
	queue<PII> que;
	que.push({stx, sty});
	
	while(que.size())
	{
		int x = que.front().fi, y = que.front().se;
		que.pop();
		
		for(int i=0;i<4;i++)
		{
			int tx = x+dir[i][0], ty = y+dir[i][1];
			if(tx < 1 || ty < 1 || tx > n || ty > m) continue;
			if(f[tx][ty] || !a[tx][ty]) continue;
			
			f[tx][ty] = 1;
			s += char(i + '0');
			que.push({tx, ty});
		}
		s += '#';
	}
}

signed main(){
//	Ios;
	while(cin >> n >> m && n && m)
	{
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++)
			{
				f[i][j] = 0;
				char c;cin >> c;
				a[i][j] = c-'0';
			}
		}
		
		mp.clear();
		
		cnt = 0, ans = 0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++)
			{
				if(f[i][j] || !a[i][j]) continue;
				++cnt;
				f[i][j] = 1;
				
				s = "";
				bfs(i, j);
				mp[s] = 1;
			}
		}
		
		cout << cnt << " " << mp.size() << endl;
	}
	
	return 0;
}

一开始觉得如果搜索路径相同的话,两个连通块一定是相同的,wa了几发才发现不是这样的。
以后再有这样的情况就尽可能多的增加哈希限制,让冲突减少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值