【数据结构1-3】集合

有时候,我们并不关心数据之间的前后关系,也不关心数据的层次关系。一些确定元素只是单纯的聚集在一起,这样的元素聚集体被称为集合。

当希望知道某个数据是否存在一个集合中,或者两个元素是否在同一个集合中时,就需要使用一些集合数据结构来维护集合元素之间的关系。

常见的集合分为并查集,哈希表,STL中的set容器和map容器。

 一、【P1536】村村通(并查集)

标准的并查集模板题,并查集一般具有如下功能。

  1. 动态连边,删边
  2. 动态维护边权,点权
  3. 查询、修改链上的信息(最值,总和等)
  4. 随意指定原树的根(即换根)
  5. 合并两棵树、分离一棵树
  6. 动态维护连通性

 总之,并查集最重要的功能是维护一个集合结构。

AC代码:

 init函数的功能是初始化指定数量的集合,find函数的功能是找到某个节点的父节点,isSame函数的功能是判断两个节点是否属于同一个集合,join函数的功能是将两个节点关联起来。

建立好每个节点的连接关系以后,重新遍历所有节点,默认第一条路径上的第一个点为根节点,所有与根节点不属于同一并查集的节点都视为不可到达。

#include 
#include 
#include 
#include 

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. 模数的选择:

绝大多数情况下,不要选择一个10^9级别的数,因为这样随机数据都会有hash冲突,根据生日悖论,随便找上​约10^5个串就有大概率出现至少一对Hash 值相等的串。

最稳妥的办法是选择两个10^9级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)。

如果能找出一个10^{18}级别的质数(Miller-Rabin),也是相对靠谱的办法。

 3. 常用的字符串hash分为以下几类:

  • 自然溢出hash:直接使用unsigned long long,不手动进行取模,溢出时会自动对2^{64}进行取模。这种方法虽然简单,但是可能会被卡数据。
  • 单模数hash:选择一个10^{18}级别的质数作为模数,那么理论上数据量超过10^9个才会出现哈希冲突,是相对安全的写法。
  • 双模数hash:选择两个10^9级别的质数作为模数,求两个哈希值,如果两个hash值都相等才能判断两个字符串相等。

AC代码(单模数hash):

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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;
}

你可能感兴趣的:(洛谷官方题单,数据结构)