【数据结构1-3】集合

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

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

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

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

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

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

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

AC代码:

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

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

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

绝大多数情况下,不要选择一个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 <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;
}

三、【P1955】程序自动分析(并查集+离散化)

         首先,这是一道很容易识破的并查集裸题,按照题目要求我们可以先排个序,把合并操作(题目中数字表示为1,即两个变量相等)放在前面预先执行,再进行判断操作(数字表示为0,即两个变量不相等),如果先执行判断操作再执行合并操作,那么会导致无法确定两个元素是否不相等

        但是由于题目中x的数量级达到了10^9,随意开一个这样大小的并查集数组,那么一定会出现超时和超内存,这里就要比较高级的离散化(因为只有10^6个操作,不可能1~10^9所有的变量都用到了)。

 离散化模板:https://www.cnblogs.com/cytus/p/8933597.html

离散化:把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。

举个例子,某个题目告诉你有1e5个数,每个数大小不超过1e9,要你对这些数进行操作(比如并查集之类的)。那么肯定不能直接开1e9大小的数组,但是1e5的范围就完全没问题。

再举个栗子,现在对{4,7,6,9}进行离散化,那么得到的结果是{1,3,2,4},也就是说,当我们并不需要这些数据具体是多少时,就只需要知道他们的相对大小就行了。 

const int N = 1e5 + 7;
int t[N], a[N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i], t[i] = a[i]; //t数组中存储副本
    sort(t + 1, t + n + 1); //将所有数组按大小排序,得到相对大小关系
    m = unique(t + 1, t + n + 1) - t - 1; //去重,注意最后需要减去起点,才能得到长度
    for (int i = 1; i <= n; i++)
        a[i] = lower_bound(t + 1, t + m + 1, a[i]) - t; //从t数组中取出a[i]对应的离散化数,注意lower_bound返回的是迭代器,需要减去起点得到t数组下标

 AC代码:

#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <set>
#include <unordered_map>

using namespace std;

const int N = 200005;
int pre[N];
int Rank[N];
int book[2*N];

struct node
{
	int a, b, opt;
}ques[N];

void init()
{
	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);
}

void join(int x, int y)
{
	x = find(x);
	y = find(y);
	if (Rank[x] == Rank[y])
	{
		pre[y] = x;
		Rank[x]++;
	}
	else if (Rank[x] > Rank[y])
		pre[y] = x;
	else if (Rank[x] < Rank[y])
		pre[x] = y;
}

bool cmp(node x, node y)
{
	return x.opt > y.opt;
}
int main()
{
	int t;	cin >> t;
	for (int i = 1; i <= t; i++)
	{
		bool flag = false;
		int n;	cin >> n;
		for (int i = 1; i <= n; i++)
		{
			cin >> ques[i].a >> ques[i].b >> ques[i].opt;
			book[2 * i - 1] = ques[i].a, book[2 * i] = ques[i].b;
		}
		sort(ques + 1, ques + n + 1, cmp);
		sort(book + 1, book + 2 * n + 1);
		int rev = unique(book + 1, book + 2 * n + 1) - book - 1;
		for (int i = 1; i <= n; i++)
		{
			ques[i].a = lower_bound(book + 1, book + rev + 1, ques[i].a) - book;
			ques[i].b = lower_bound(book + 1, book + rev + 1, ques[i].b) - book;
		}
		init();
		for (int i = 1; i <= n; i++)
		{
			if (ques[i].opt == 1)
			{
				join(ques[i].a, ques[i].b);
			}
			else
			{
				if (isSame(ques[i].a, ques[i].b))
					flag = true;
			}
		}

		if (flag) cout << "NO" << endl;
		else cout << "YES" << endl;
	}
}

四、【P1621】集合(并查集+质数筛)

先使用质数筛筛出在 ‘p~b’ 范围内的素数,然后用每个素数prime[i] 乘以某个值num,如果存在prime[i]*num 和prime[i]*(num+1) 都在 [a,b] 范围内,那么可以合并这两个数。 

质数筛法:

使用judge数组作为筛子,初始情况下认为所有数都是质数,然后从2开始枚举每个未被筛掉的数,将所有2的倍数筛除;将所有3的倍数筛除;将所有5的倍数筛除……直到边界情况。

我们认为剩余的数都是质数。

最后,用一个prime数组将所有质数保存起来。

int isprime(int p,int n) 
{
	int k = sqrt(n);
	memset(judge, 1, sizeof(judge));
	prime[0] = prime[1] = 0;
	for (int i = 2; i <= k; i++)
	{
		if (judge[i])
		{
			for (int j = 2 * i; j <= n; j += i)
				judge[j] = 0;
		}
	}
	int cnt = 1;
	for (int i = p; i <= n; i++)
	{
		if (judge[i])
		{
			prime[cnt] = i;
			cnt++;
		}
	}
	return cnt - 1;
}

 AC代码:

本题的难点在于质数的选择,这里可以使用普通筛或者埃式筛法,将范围内的所有质数找出,然后进行试探(假设每个质数为公因子),如果存在满足条件的两个数,那么使用并查集将两个数合并。

#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <map>
#include <cstring>
#include <set>
#include <unordered_map>

using namespace std;

const int N = 100005;
int pre[N];
int Rank[N];

int judge[N];
int prime[N];
int isprime(int p,int n) 
{
	int k = sqrt(n);
	memset(judge, 1, sizeof(judge));
	prime[0] = prime[1] = 0;
	for (int i = 2; i <= k; i++)
	{
		if (judge[i])
		{
			for (int j = 2 * i; j <= n; j += i)
				judge[j] = 0;
		}
	}
	int cnt = 1;
	for (int i = p; i <= n; i++)
	{
		if (judge[i])
		{
			prime[cnt] = i;
			cnt++;
		}
	}
	return cnt - 1;
}

void init(int a,int b)
{
	for (int i = a; i <= b; 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);
}

void join(int x, int y)
{
	x = find(x);
	y = find(y);
	if (Rank[x] == Rank[y])
	{
		pre[y] = x;
		Rank[x]++;
	}
	else if (Rank[x] > Rank[y])
		pre[y] = x;
	else if (Rank[x] < Rank[y])
		pre[x] = y;
}

int main()
{
	int a, b, p;
	cin >> a >> b >> p;
	int rev = isprime(p,b);
	init(a, b);
	for (int i = 1; i <= rev; i++)
	{
		int num = 0;
		while (num * prime[i] < a) //找到起始点
			num++;
		while (prime[i] * (num + 1) <= b)
		{
			join(prime[i] * num, prime[i] * (num + 1));
			num++;
		}
	}
	int ans = 0;
	for (int i = a; i <= b; i++)
	{
		if (pre[i] == i) ans++;
	}
	cout << ans << endl;
}

  • 34
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值