2025 暑假集训 Day2
2025.8.5
Day 2 专题训练:Hash 表、并查集
Hash 表
核心代码有以下部分:
哈希函数 H(x)
常用构造方法(打※的表示个人认为最常用的):
- 【※除余法※】:选取一个模数构造哈希函数 \(h(x)=x \bmod P\),其中 \(P\) 最好是一个质数。质数是我们的得力助手。
- 【直接定址法】:比如 \(h(x)=x,h(x)=x+1218\) 直接以 \(x\) 本身或加上一个常数为哈希函数
- 【数字分析法】:根据数据的类型留下几个典型的位数作为哈希值,比如
2111218,2111417,2111319这一组数据,显然只有左数第 \(5,7\) 有区分度,其他数字都相同(都是2111x1x这种形式),于是哈希函数值分别为28,47,39。 - 【平方取中法】:将关键码的值平方,然后取中间的几位作为哈希函数值。具体取多少位视实际要求而定,取哪几位常常结合数字分析法。
- 【折叠法】:将数字拆开然后把这些拆开的数字加起来,比如
h(12180211)=1218+0211=1429 - 【基数转换法】:将 \(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\) 之间的关系:
\(A\) 是 \(B\) 的一个真子集,输出
A is a proper subset of B\(B\) 是 \(A\) 的一个真子集,输出
B is a proper subset of A\(A\) 和 \(B\) 是同一个集合,输出
A equals B\(A\) 和 \(B\) 的交集为空,输出
A and B are disjoint上述情况都不是,输出
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\) 是什么字符串:
- 若 \(del<mid\) 则 \(L=[1,del-1] \cup [dep+1,mid],R=[mid+1,n]\)
- 若 \(del=mid\) 则 \(L=[1,mid-1],R=[mid+1,n]\)
- 若 \(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
哈希表判断字符串相等
*/

浙公网安备 33010602011771号