有时候,我们并不关心数据之间的前后关系,也不关心数据的层次关系。一些确定元素只是单纯的聚集在一起,这样的元素聚集体被称为集合。
当希望知道某个数据是否存在一个集合中,或者两个元素是否在同一个集合中时,就需要使用一些集合数据结构来维护集合元素之间的关系。
常见的集合分为并查集,哈希表,STL中的set容器和map容器。
一、【P1536】村村通(并查集)
标准的并查集模板题,并查集一般具有如下功能。
- 动态连边,删边
- 动态维护边权,点权
- 查询、修改链上的信息(最值,总和等)
- 随意指定原树的根(即换根)
- 合并两棵树、分离一棵树
- 动态维护连通性
总之,并查集最重要的功能是维护一个集合结构。
AC代码:
init函数的功能是初始化指定数量的集合,find函数的功能是找到某个节点的父节点,isSame函数的功能是判断两个节点是否属于同一个集合,join函数的功能是将两个节点关联起来。
建立好每个节点的连接关系以后,重新遍历所有节点,默认第一条路径上的第一个点为根节点,所有与根节点不属于同一并查集的节点都视为不可到达。
cpp
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
const int INF = 0x7fffffff / 4; //若直接为INT_MAX,则会发生溢出
const int N = 1005;
int pre[N] = { 0 }; //前驱节点
int Rank[N] = { 0 }; //树的高度
void init(int n)
{
for (int i = 1; i <= n; i++)
{
pre[i] = i;
Rank[i] = 1;
}
}
int find(int x)
{
if (pre[x] == x) //找到集合的代表元素
return x;
return pre[x] = find(pre[x]);
}
bool isSame(int x, int y)
{
return find(x) == find(y);
}
bool join(int x,int y)
{
x = find(x);
y = find(y);
if (x == y) //两者已经在一个集合里面了
return false;
if (Rank[x] > Rank[y])
pre[y] = x;
else if (Rank[x] == Rank[y])
{
Rank[x]++;
pre[y] = x;
}
else if (Rank[x] < Rank[y])
{
pre[x] = y;
}
return true;
}
int main()
{
while (1)
{
int n, m;
cin >> n;
if (n == 0) return 0;
cin >> m;
if (m == 0)
{
cout << n - 1 << endl;
continue;
}
init(n); //初始化
int gen, ye;
cin >> gen >> ye;
join(gen, ye);
for (int i = 2; i <= m; i++)
{
int a1, a2;
cin >> a1 >> a2;
join(a1, a2);
}
int cnt = 0;
for (int i = 1; i <= n; i++)
{
if (pre[i] == i && Rank[i] == 1)
{
join(i, gen);
cnt++;
}
else if (!isSame(gen, i))
{
join(gen, i);
cnt++;
}
}
cout << cnt << endl;
}
}
二、【P3370】字符串哈希(hash)
Hash就是一个像函数的东西,你放进去一个值,它给你输出来一个值。输出的值就是Hash值。一般Hash值会比原来的值更好储存(更小)或比较。
字符串hash就是把字符串转换成一个整数的函数,且要尽量不同字符串对应不同的哈希值。
字符串哈希的主要思路是选取恰当的进制,可以把字符串中的字符看成一个大数字中的每一位数字,不过比较字符串和比较大数字的复杂度并没有什么区别(高精数的比较也是O(n)的),但只要把它对一个数取模,然后认为取模后的结果相等原数就相等,那么就可以在一定的错误率的基础上以O(1)复杂度进行判断了。
1. 进制的选择:
首先不要把任意字符对应到数字0,假如把a对应到数字0,那么将不能只从Hash结果上区分ab和b(虽然可以额外判断字符串长度,但不把任意字符对应到数字0更加省事且没有任何副作用),一般而言,把a-z对应到数字1-26比较合适。
关于进制的选择实际上非常自由,大于所有字符对应的数字的最大值,不要含有模数的质因子(那还模什么),比如一个字符集是a到z的题目,选择27、233、19260817 都是可以的。
2. 模数的选择:
绝大多数情况下,不要选择一个级别的数,因为这样随机数据都会有hash冲突,根据生日悖论,随便找上约个串就有大概率出现至少一对Hash 值相等的串。
最稳妥的办法是选择两个级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)。
如果能找出一个级别的质数(Miller-Rabin),也是相对靠谱的办法。
3. 常用的字符串hash分为以下几类:
- 自然溢出hash:直接使用unsigned long long,不手动进行取模,溢出时会自动对进行取模。这种方法虽然简单,但是可能会被卡数据。
- 单模数hash:选择一个级别的质数作为模数,那么理论上数据量超过个才会出现哈希冲突,是相对安全的写法。
- 双模数hash:选择两个级别的质数作为模数,求两个哈希值,如果两个hash值都相等才能判断两个字符串相等。
AC代码(单模数hash):
cpp
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <queue>
using namespace std;
typedef unsigned long long ull;
ull base = 131; //进制
ull a[10005]; //用于存储字符串hash
int prime = 233317; //强化hash
ull mod = 212370440130137957ll; //10^18大素数
ull HASH(string s)
{
ull ans = 0;
for (int i = 0; i < s.length(); i++)
ans = (ans * base + (ull)s[i] % mod + prime);
return ans;
}
int main()
{
int n; cin >> n;
int ans = 0;
for (int i = 1; i <= n; i++)
{
string s;
cin >> s;
a[i] = HASH(s);
}
sort(a + 1, a + n + 1);
for (int i = 1; i < n; i++)
{
if (a[i] != a[i + 1])
ans++;
}
cout << ans + 1;
}