带权值的并查集及二分图的应用

例题1: 关押罪犯

做法1:二分 + 染色法判断二分图

思路:二分最大的影响力值,根据该值对图进行染色,判断是否可以构成二分图,如果可以构成二分图说明这种划分是可行的,否则说明不可行。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 20010, M = 200010;

int h[N], e[M], ne[M], w[M], idx; 
int color[N], n, m, l, r;

void add(int a, int b, int c)
{
	w[idx] = c;
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx ++ ;	
} 

bool bfs(int u, int c, int v)
{
	color[u] = c;
	for(int i = h[u]; i != -1; i = ne[i])
	{
		if(w[i] > v)
		{
			int j = e[i];
			if(!color[j])	//这里一定要加括号!!! 
			{
				if(!bfs(j, 3 - c, v))	return false;
			}
			else if(color[j] == c)	return false;
		}
	}
	return true;
}

bool check(int v)
{
	memset(color, 0, sizeof color);
	for(int i = 1; i <= n; i ++ )
	{
		if(!color[i])
			if(!bfs(i, 1, v))
				return false;
	}
	return true;
}

int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	while(m -- )
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
		add(b, a, c);
		r = max(r, c);
	}
	while(l < r)
	{
		int mid = l + r >> 1;
		if(check(mid))	r = mid;
		else	l = mid + 1;
	}
	
	cout << l << endl;
	
	return 0;
}

 做法2:并查集

思路:域并查集。将两个监狱划分成并查集的两个区间,每次贪心你的把怒气值最大的囚犯分到这两个域当中,如果发生冲突(这两个囚犯的根属于同一个监狱),说明此时发生冲突,该怒气值就是最大影响力。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 20010, M = 100010;

int n, m, res, pre[N << 1];

struct node
{
	int l, r, w;
}g[M];

bool cmp(const node &x, const node &y)
{
	return x.w > y.w;
}

int get(int x)
{
	if(pre[x] == x)	return x;
	return pre[x] = get(pre[x]);
}

void unite(int x, int y)
{
	pre[get(x)] = get(y);
}

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= m; i ++ )
	{
		int a, b, c;
		cin >> a >> b >> c;
		g[i] = {a, b, c};
	}
	
	for(int i = 1; i <= n * 2; i ++ )	pre[i] = i;
	
	sort(g + 1, g + 1 + m, cmp);
	
	int res;
	for(int i = 1; i <= m; i ++ )
	{
		int x = g[i].l, y = g[i].r;
		int xx = get(x), yy = get(y);
		if(xx == yy)
		{
			res = g[i].w;
			break;		
			/*wrong写法,注意答案可能是0 
			cout << g[i].w << endl;
			return 0;
			*/ 
		}			
		//x和y是仇人,那么x的仇人和y是一个集合,同样y的仇人和x在一个集合 
		pre[xx] = get(y + n);
		pre[yy] = get(x + n);
	}
	cout << res << endl;
	return 0;
} 


 例题2: 银河英雄传

带权值的并查集,主要是对普通并查集的find函数和union函数做一些修改。

考察了对find函数路径压缩的理解

参考题解

初见这道题,首先想到的方法当然是直接模拟,模拟每一次指令。当然这种方法对于小数据行得通,但对于此题的500,000个指令,肯定超时。

因此我们就要想其它方法。

先来分析一下这些指令的特点,很容易发现对于每个M指令,只可能一次移动整个队列,并且是把两个队列首尾相接合并成一个队列,不会出现把一个队列分开的情况,因此,我们必须要找到一个可以一次操作合并两个队列的方法。

再来看下C指令:判断飞船i和飞船j是否在同一列,若在,则输出它们中间隔了多少艘飞船。我们先只看判断是否在同一列,由于每列一开始都只有一艘飞船,之后开始合并,结合刚刚分析过的M指令,很容易就想到要用并查集来实现。

定义一个数组fa,fa[i]表示飞船i的祖先节点,即其所在列的队头。再定义一个用于查找飞船祖先的函数find,在每次递归找祖先的同时更新fa,压缩路径,大大减小以后的时间消耗。初始时对于每个fa[i]都赋值为i,合并时就先分别查找飞船i和飞船j的祖先,然后将飞船i的祖先的祖先(即fa[飞船i的祖先])赋值为飞船j的祖先。最后每次判断时只需要找到飞船i和飞船j的祖先,判断是否是同一艘飞船,若是,则在同一列,反之,则不在。

现在,判断是否在同一列以及如何一次操作合并两个队列的问题已经解决,但还有问题需要解决:如何在以上方法的基础上,进一步得到两艘飞船之间的飞船数量呢?

我们先来分析一下:两艘飞船之间的飞船数量,其实就是艘飞船之间的距离,那么,这就转换为了一个求距离的问题。两艘飞船都是在队列里的,最简单的求距离的方法就是前后一个一个查找,但这个方法太低效,会超时。看见多次求两个点的距离的问题,便想到用前缀和来实现:开一个front数组,front[i]表示飞船i到其所在队列队头的距离,然后飞船i和飞船j之间的飞船数量即为它们到队头的距离之差减一,就是abs(front[i]-front[j])-1。

解决了如何高效得到两艘飞船之间飞船数量的问题,便又发现了新的问题:如何在之前方法的基础上,得到每艘飞船和队头的距离呢?

来分析一下现在已经使用的算法——并查集,它的特点就是不是直接把一个队列里的所有飞船移到另一个队列后面,而是通过将要移动的队列的队头连接到另一个队列的队头上,从而间接连接两个队列。因此,我们在这个算法的基础上,每次只能更新一列中一艘飞船到队头的距离(如果更新多艘的话并查集就没有意义了)。

那么,该更新哪艘飞船呢?现在我们已经知道,使用并查集合并两个队列时只改变队头的祖先,而这个队列里其它飞船的祖先还是它原来的队头,并没有更新,所以这个队列里的其它飞船在队列合并之后,仍然可以找到它原来的队头,也就可以使用它原来队头的数据,因此,在每次合并的时候,只要更新合并前队头到目前队头的距离就可以了,之后其它的就可以利用它来算出自己到队头的距离。

理清了思路,但又有问题出现:该怎样更新呢?该怎么计算呢?

更新很容易,我们来分析一下:对于原来的队头,它到队头的距离为0,当将它所在的队列移到另一个队列后面时,它到队头的距离就是排在它前面的飞船数,也就是合并前另一个队列的飞船数量。因此,就知道该怎样实现了,我们再建一个数组num,num[i]表示以i为队头的队列的飞船数量,初始时都是1,在每次合并的时候,fx为合并前飞船i的队头,fy为合并前飞船j的队头,每次合并时,先更新front[fx],即给它加上num[fy],然后开始合并,即fa[fx]=fy,最后更新num, num[fy]+= num[fx];num[fx]=0。

现在就差最后一步了:如何计算每个飞船到队头的距离。再来分析一下:对于任意一个飞船,我们都知道它的祖先(不一定是队头,但一定间接或直接指向队头),还知道距离它祖先的距离。对于每一个飞船,它到队头的距离,就等于它到它祖先的距离加上它祖先到队头的距离,而它的祖先到队头的距离,也可以变成类似的。可以递归实现,由于每一次更新都要用到已经更新完成的祖先到队头的距离,所以要先递归找到队头,然后在回溯的时候更新(front[i]+=front[fa[i]]),可以把这个过程和查找队头的函数放在一起。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 30010;

int pre[N], size[N], d[N], n;

inline int get(int x)
{
	if(pre[x] == x)	return x;
	
	int root = get(pre[x]);
	d[x] += d[pre[x]];	//
		
	return pre[x] = root;
	
}

void unite(int x, int y)
{
	int fx = get(x), fy = get(y);
	pre[fx] = fy;
	
	d[fx] += size[fy];	//
	size[fy] += size[fx];	//
	size[fx] = 0;	//
}

int main()
{
	for(int i = 1; i <= N; i ++ )	pre[i] = i, size[i] = 1;	
	cin >> n;
	for(int i = 1; i <= n; i ++ )
	{
		char ch;
		int a, b;
		cin >> ch >> a >> b;
		if(ch == 'M')	unite(a, b);
		else	
		{
			if(get(a) != get(b))	cout << -1 << endl;
			else	cout << abs(d[a] - d[b]) - 1 << endl;
		}
	}
	for(int i = 1; i <= n; i ++ )	cout << d[i] << " ";
	cout << endl;
	return 0;
}


例题3: 食物链

例题1的拓展,例题一只需要两个域,而本题需要三个域:

把每个动物x拆成三个点,同类域xself,捕食域xeat,天敌域xenemy;

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 50010;

int pre[N * 3], n, k, res;
//1-n同类,n-2n食物,2n-3n天敌 

int find(int x)
{
	if(pre[x] == x)	return x;
	return pre[x] = find(pre[x]);
}

void unite(int x, int y)
{
	int fx = find(x), fy = find(y);
	pre[fx] = fy;
}

int main()
{
	cin >> n >> k;
	/*极其不推荐这么写
	for(int i = 1; i <= n; i ++ )	 
	{
		pre[i] = i;
		pre[i + n] = i;
		pre[i + 2 * n] = i; 
	}
	*/
	for(int i = 1; i <= 3 * n; i ++ )	pre[i] = i;
	
	while(k -- )
	{
		int p, x, y;
		cin >> p >> x >> y;
		if(x > n || y > n)	res ++ ;
		else if(p == 1)
		{
			if(find(x) == find(y + n) || find(x) == find(y + 2 * n))	res ++ ;
			else
			{
				unite(x, y);
				unite(x + n, y + n);
				unite(x + 2 * n, y + 2 * n);
			}
		}
		else if(p == 2)
		{
			if(x == y || find(x) == find(y) || find(x) == find(y + n))	res ++ ;
			else	//一般把并查集划分了几个域,这里就有几个unite 
			{
				unite(x + n, y);
				unite(x, y + 2 * n);
				unite(x + n + n, y + n);
			}
		}
	}
	
	cout << res << endl;
	
	return 0;
}


补充:二分图的应用

例题4: P1330 封锁阳光大学 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路:每在一个点上放一只河蟹,所有与该点相连的边都被封锁,确保一条边的两个端点上不会都有一只河蟹,很容易考虑到二分图,最少的河蟹数就是二分图的两个点集合中点数较少的那一个。

注意:连通是个永恒的话题。
千万不要忘记对不连通的考虑!!!
千万不要忘记对不连通的考虑!!!
千万不要忘记对不连通的考虑!!!

所以只需要累加每个连通图中二分图的最小点集合即可,如果不是二分图就退出

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 10010, M = 200010;

int h[N], e[M], ne[M], idx;
int n, m, res, color[N], l, r;

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool check(int u, int c)
{
	color[u] = c;
	if(c == 1)	 l ++ ;
	else	r ++ ;
	for(int i = h[u]; i != -1; i = ne[i]) 
	{
		int j = e[i];
		if(!color[j])
		{
			if(!check(j, 3 - c))	return false;
		}
		else if(color[j] == c)	return false;
	}
	return true;
}

int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	while(m -- )
	{
		int a, b;
		cin >> a >> b;
		add(a, b);
		add(b, a); 
	}
	
	bool flag = true;	
	for(int i = 1; i <= n; i ++ )
	{
		if(!color[i])
		{
			l = r = 0;
			if(!check(i, 1))
			{
				cout << "Impossible" << endl;
				return 0;
			}
			else	res += min(l, r);
		}
	}

	cout << res << endl;
	
	return 0;	
} 

posted @ 2022-05-05 08:41  光風霽月  阅读(85)  评论(0编辑  收藏  举报