2025 暑假集训 Day2

2025.8.5

Day 2 专题训练:Hash 表、并查集

Hash 表

核心代码有以下部分:

哈希函数 H(x)

常用构造方法(打※的表示个人认为最常用的):

  1. 【※除余法※】:选取一个模数构造哈希函数 \(h(x)=x \bmod P\),其中 \(P\) 最好是一个质数。质数是我们的得力助手。
  2. 【直接定址法】:比如 \(h(x)=x,h(x)=x+1218\) 直接以 \(x\) 本身或加上一个常数为哈希函数
  3. 【数字分析法】:根据数据的类型留下几个典型的位数作为哈希值,比如 2111218,2111417,2111319 这一组数据,显然只有左数第 \(5,7\) 有区分度,其他数字都相同(都是 2111x1x 这种形式),于是哈希函数值分别为 28,47,39
  4. 【平方取中法】:将关键码的值平方,然后取中间的几位作为哈希函数值。具体取多少位视实际要求而定,取哪几位常常结合数字分析法。
  5. 【折叠法】:将数字拆开然后把这些拆开的数字加起来,比如 h(12180211)=1218+0211=1429
  6. 【基数转换法】:将 \(x\) 看成别的进制的数然后转换回十进制作为哈希函数值,比如 \(x=1218_{(10)}\),先把它看成 \(16\) 进制的数得到 \(x^\prime=1218_{(16)}=4632_{(10)}\)(这个进制一般选一个质数,但是我这里计算器只能转换 \(2,8,10,16\) 懒得搞了)

除余法构造哈希函数:(注意函数名不要写 hash,这是一个 C++ 关键字)

inline int H(int s)
{
	return s%mod;
}

定位元素

插入和查询元素都要给一个元素先定位,找出这个元素存在数组的哪里。可以使用线性探测法,思路为先对元素 \(s\) 求哈希值,如果哈希数组里面第 \(H(s)\) 元素已经存放过值并且存放的值不为 \(s\),那么就查找下一个空的格子把这个元素放进去,在找过一轮之后还没有可以放的格子证明已经放满了。

constexpr int mod=150001;
int h[mod];
inline int locate(int s)
{
	int w=H(s),i=0;
	while(i<mod&&h[(i+w)%mod]&&h[(i+w)%mod]!=s) i++;
	return (i+w)%mod;
}

有了定位之后,存放和查找元素都很简单,直接使用 h[locate(s)] 即可。

T1 集合

给定两个集合 \(A,B\),集合内的任一元素 \(x\) 满足 \(1 \le x \le 10^9\),并且每个集合的元素个数不大于 \(10^5\)。我们希望求出 \(A,B\) 之间的关系:

  1. \(A\)\(B\) 的一个真子集,输出 A is a proper subset of B

  2. \(B\)\(A\) 的一个真子集,输出 B is a proper subset of A

  3. \(A\)\(B\) 是同一个集合,输出 A equals B

  4. \(A\)\(B\) 的交集为空,输出 A and B are disjoint

  5. 上述情况都不是,输出 I'm confused!

样例输入

3 1 2 3
4 1 2 3 4

样例输出 A equals B

题目可转化为计算 \(A \cap B\) 的元素个数 \(x\),然后根据 \(x\) 可以很容易判断出属于哪种情况。可以使用哈希表将 \(A\) 中的元素存入,然后再把 \(B\) 中的元素在哈希表查一下,查得到一个就 \(x \gets x+1\)。如果集合内有重复的数字怎么办?不可能,因为集合具有互异性。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=-1;
constexpr int mod=150001;
int h[mod];
inline int H(int s)
{
	return s%mod;
}
inline int locate(int s)
{
	int w=H(s),i=0;
	while(i<mod&&h[(i+w)%mod]&&h[(i+w)%mod]!=s) i++;
	return (i+w)%mod;
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	int n,m,a,cnt=0;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a;
		h[locate(a)]=a;
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>a;
		if(h[locate(a)]==a) cnt++;
	}
	if(cnt==n&&cnt==m) cout<<"A equals B";
	else if(cnt==n) cout<<"A is a proper subset of B";
	else if(cnt==m) cout<<"B is a proper subset of A";
	else if(cnt==0) cout<<"A and B are disjoint";
	else cout<<"I'm confused!";
	return 0;
}

T2 魔板(洛谷 P2730)

在成功地发明了魔方之后,拉比克先生发明了它的二维版本,称作魔板。这是一张有8个大小相同的格子的魔板:

1 2 3 4
8 7 6 5

我们知道魔板的每一个方格都有一种颜色。这8种颜色用前8个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用 \(12345678\) 来表示。这是基本状态。

这里提供三种基本操作,分别用大写字母“A”,“B”,“C”来表示(可以通过这些操作改变魔板的状态):

  • “A”:交换上下两行;8 7 6 5 \n 1 2 3 4
  • “B”:将最右边的一列插入最左边;4 1 2 3 \n 5 8 7 6
  • “C”:魔板中央四格作顺时针旋转。1 7 2 4 \n 8 6 3 5

假设初始状态为 \(12345678\),使用以上三种操作可以变成:

  • 使用 A:\(87654321\)
  • 使用 B:\(41236785\)
  • 使用 C:\(17245368\)

对于每种可能的状态,这三种基本操作都可以使用。计算用最少的基本操作完成基本状态到目标状态的转换,输出最少步数以及其基本操作序列。

样例输入:2 6 8 4 5 7 3 1

样例输出:

7 
BCABCCB

考虑使用搜索,显然 DFS 是不行的,因为会一路走到黑,会超时,所以使用 BFS。状态怎么标记?每一个状态可以视作一个字符串,比如 12345678,然后就可以使用哈希表了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
constexpr int seed=17;
constexpr int mod=60493;
constexpr int opt[3][8]=
{
	{8,7,6,5,4,3,2,1},
	{4,1,2,3,6,7,8,5},
	{1,7,2,4,5,3,6,8}
};
string h[N],state[N],S;
char ans[N];
int step[N],fa[N],head=0,tail=1;
inline int H(string s)
{
	int x=0;
	for(int i=0;i<8;i++) x=(x*seed+s[i]-48);
	return x;
}
inline int locate(string s)
{
	int w=H(s),i=0;
	while(i<mod&&h[(w+i)%mod]!=""&&h[(w+i)%mod]!=s) i++;
	return (w+i)%mod;
}
inline bool vis(string s)
{
	int idx=locate(s);
	if(h[idx].empty())
	{
		h[idx]=s;
		return 0;
	}
	else return 1;
}
inline string magic(string t,int op)
{
	string res;
	for(int i=0;i<8;i++) res.push_back(t[opt[op][i]-1]);
	return res;
}
inline void bfs()
{
	vis("12345678");
	step[tail]=0; state[tail]="12345678";
	while(head<tail)
	{
		head++;
		for(int i=0;i<3;i++)
		{
			string news=magic(state[head],i);
			if(!vis(news))
			{
				tail++;
				step[tail]=step[head]+1;
				ans[tail]=i+'A';
				fa[tail]=head;
				state[tail]=magic(state[head],i);
				if(state[tail]==S) return;
			}
		}
	}
}
inline void print(int x)
{
	if(x==1) return;
	print(fa[x]);
	cout<<ans[x];
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	int x;
	for(int i=0;i<8;i++)
	{
		cin>>x;
		S.push_back(x+'0');
	}
	if(S=="12345678")
	{
		cout<<"0";
		exit(0);
	}
	else
	{
		bfs();
		cout<<step[tail]<<'\n';
		print(tail);
	}
	return 0;
}

T3 子段统计

给定一个字符串 \(S\),只由 \(k\) 种小写字母组成。现在给定一个长度 \(L\),要求统计一下 \(S\) 有多少种不同的长度为 \(L\) 的子串。\(1 \le k \le 26,k^L \le 2 \times 10^7\)。样例输入 1 2 ababab 样例输出 2

可以考虑建立一个 unordered_set,将每一段长度为 \(L\) 的子串的哈希值丢进去,最后这个 unordered_set 的大小就是答案了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e6+7;
constexpr int seed=131;
//constexpr ll mod=1.8e12+47;
int len,k,l,r,n;
unsigned int h[N],p[N];
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	string s;
	cin>>len>>k;
	cin>>s;
	n=s.size();
	s=" "+s;
	unordered_set<unsigned int> st;
	p[0]=1;
	for(int i=1;i<=n;i++) p[i]=p[i-1]*seed;
	for(int i=1;i<=n;i++) h[i]=(h[i-1]*seed+(s[i]-'a'));
	for(int i=1;i+len-1<=n;i++)
	{
		l=i,r=i+len-1;
		st.insert(h[r]-h[l-1]*p[r-l+1]);
//		cerr<<l<<' '<<r<<' '<<h[r]-h[l-1]*p[len]<<'\n';
	}
	cout<<st.size();
	return 0;
}

T4 调皮的小 Biu

小 Biu 的期中考试刚刚结束,调皮的小 Biu 不喜欢学习,所以他考试中抄袭了小 Piu 的试卷。

考试过程中一共有 \(n\) 道题目,每道题目的标准答案为区间 \([1,5]\) 中的一个正整数。

现在有小 Piu 和小 Biu 的答案序列 \(a\)\(b\),现在老师想知道两个答案序列最长相等的连续子串的长度是多少。

比如一共有 \(10\) 道题,\(a\) 序列为 \((1,1,2,1,2,1,2,1,1,5)\)\(b\) 序列为 \((3,3,2,3,1,2,1,1,3,4)\),则最长相等的子串为 \((1,2,1,1)\),所以答案为 \(4\)

数据范围:\(1 \le n \le 10^5\)

不难想到一个 \(O(n^2)\) 的暴力做法,就是把 \(a,b\) 一起扫若干次,每次扫的时候比较 \(a_i,b_i\) 是否相等,记录出一个子串长度,然后把 \(b\) 的最后一个元素丢到最前面继续扫,直到 \(b\) 中所有元素已经被丢过一次。但是这样会超时。

不妨考虑一个二分答案的做法,二分一个长度 \(L\)check 函数检测在 \(a,b\) 中是否有一个长度为 \(L\) 的子串。检测的时候使用一个 unordered_set\(a\) 中每一个长度为 \(L\) 的子串都丢进去,然后对于 \(b\) 中每一个长度为 \(L\) 的子串都在 unordered_set 里面查找一遍,如果有一个找到了就算成功。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=100007;
constexpr int seed=1009;
int n,a[N],b[N];  //p[i]预处理pow(seed,i) 
ull ha[N],hb[N],p[N];
inline void debuga(int l,int r)
{
	for(int i=l;i<=r;i++) cerr<<a[i];
	cerr<<'\n';
}
inline void debugb(int l,int r)
{
	for(int i=l;i<=r;i++) cerr<<b[i];
	cerr<<'\n';
}
inline bool check(int len)
{
//	cerr<<"len="<<len<<'\n';
	unordered_set<ull> s;
	int l,r;
	for(int i=1;i+len-1<=n;i++)
	{
		l=i,r=i+len-1;
		s.insert(ha[r]-ha[l-1]*p[r-l+1]);
//		debuga(l,r);
//		cerr<<"ha"<<ha[r]-ha[l-1]*p[r-l+1]<<'\n';
	}
	for(int i=1;i+len-1<=n;i++)
	{
		l=i,r=i+len-1;
//		debugb(l,r);
//		cerr<<"hb"<<hb[r]-hb[l-1]*p[r-l+1]<<'\n';
		if(s.find(hb[r]-hb[l-1]*p[r-l+1])!=s.end()) return 1;
	}
	return 0;
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	cin>>n;
	p[0]=1;
	for(int i=1;i<=n;i++) p[i]=p[i-1]*seed;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		ha[i]=ha[i-1]*seed+a[i];
	}
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
		hb[i]=hb[i-1]*seed+b[i];
	}
	int l=1,r=N;
	while(l+1<r)
	{
		int mid=(l+r)>>1; 
		if(check(mid)) l=mid;
		else r=mid;
	}
	cout<<l;
	return 0;
}
//计算s的子串s[l~r]的hash值:hash[r]-hash[l-1]*pow(seed,r-l+1) 

T5 Three Friends(洛谷 P6739)

题面略。

首先显然地,如果长度是个偶数肯定是无解的,因为字符串要复制两遍还要新增加一个字符,肯定长度是奇数。

可以枚举插入字符的位置 \(del\),然后根据 \(del\)\(mid\) 的关系分三种情况(其中 \(mid\) 为字符串的中间值的下标,为 \(\left\lfloor \dfrac{n+1}{2} \right\rfloor\))求出应该删除的字符的左边 \(L\) 和右边 \(R\) 是什么字符串:

  1. \(del<mid\)\(L=[1,del-1] \cup [dep+1,mid],R=[mid+1,n]\)
  2. \(del=mid\)\(L=[1,mid-1],R=[mid+1,n]\)
  3. \(del>mid\)\(L=[1,mid-1],R=[mid,del-1]\cup[del+1,n]\)

如果 \(L=R\) 那么就视为找到了一组解。

注意这题有一个坑,比如这个字符串 NEUVILLETTENEUVILLETTEE,正确答案应该是 NEUVILLETTE,如果你的代码是判断了一组解之后就把有解的标记设置为 true 的话那就会判出 NOT UNIQUE,因为可以有两个删除方案(删除倒数第二或者倒数第一个 E 都可以构造出原串 NEUVILLETTE),可以先设置一个变量 anshash 为一个非常奇怪的值(比如 \(1.2e18\)),在记录答案的时候如果新答案的 Hash 值和 anshash 不同那么就视为多组解。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=5e5+7;
constexpr int mod=1e9+7;
constexpr int seed=211;
int n;
ll h[N],p[N]={1};
string s,ans;
inline ll substr(int l,int r)
{
	return (h[r]%mod+mod-h[l-1]*p[r-l+1]%mod)%mod;
}
inline bool check(int del)
{
	int mid=(1+n)>>1;
	if(del<mid)       //ABXCABC L=[1,del-1]∪[del+1,mid]  R=[mid+1,n]
	{
		ll l=(substr(1,del-1)*p[mid-del]%mod+substr(del+1,mid)%mod)%mod;
		ll r=substr(mid+1,n);
		if(l==r)
		{
			for(int i=mid+1;i<=n;i++) ans+=s[i];
			return 1;
		}
	}
	else if(del==mid) //ABCXABC L=[1,mid-1]  R=[mid+1,n]
	{
		ll l=substr(1,mid-1);
		ll r=substr(mid+1,n);
		if(l==r)
		{
			for(int i=1;i<mid;i++) ans+=s[i];
			return 1;
		}
	}
	else if(del>mid)  //ABCABXC L=[1,mid-1]  R=[mid,del-1]∪[del+1,n] 
	{
		ll l=substr(1,mid-1);
		ll r=(substr(mid,del-1)*p[n-del]%mod+substr(del+1,n)%mod)%mod;
		if(l==r)
		{
			for(int i=1;i<mid;i++) ans+=s[i];
			return 1;
		}
	}
	return 0;
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	cin>>n>>s;
	s=" "+s;
	for(int i=1;i<=n;i++)
	{
		p[i]=p[i-1]*seed%mod;
		h[i]=(h[i-1]*seed%mod+s[i]-'A')%mod;
	}
	if(n%2==0) return 0&puts("NOT POSSIBLE");
	bool ok=0;
	for(int i=1;i<=n;i++)  //寻找第i个字符删掉 
	{
		if(check(i))
		{
			if(ok) return 0&puts("NOT UNIQUE");
			else ok=1;
		}
	}
	if(ok==0) puts("NOT POSSIBLE");
	else cout<<ans;
	return 0;
}
/*
将U串删掉一个字符可以得到T=S+S串,将T串折半可以得到S串 
U=ABCAABC  枚举一个可以删掉的字符 
T=ABCABC
S=ABC

哈希表判断字符串相等 
*/
posted @ 2025-08-06 19:03  wwwidk1234  阅读(22)  评论(0)    收藏  举报