MENU

并查集例题

January 11, 2019 • Read: 4146 • 算法阅读设置

关于并查集的介绍可以看我之前的文章并查集 Union Find

POJ 2236展开目录

很水的并查集题目,结构体中除了存坐标 x,y 以外还要存是否被维修过

合并的时候考虑的点比较多,假如当前修好的计算机编号是 p,合并需要同时满足三个条件:1. 合并的计算机编号不是 p;2. 合并的计算机与 p 的距离小于等于 d;3. 合并的计算机也是好的

  • import java.util.*;
  • public class Main {
  • public static class Node {
  • int x, y;
  • boolean isGood;
  • Node(int x, int y, boolean isGood) {
  • this.x = x;
  • this.y = y;
  • this.isGood = isGood;
  • }
  • }
  • public static class unionFindSet {
  • public HashMap<Node, Node> fatherMap;
  • public HashMap<Node, Integer> sizeMap;
  • public unionFindSet() {
  • fatherMap = new HashMap<Node, Node>();
  • sizeMap = new HashMap<Node, Integer>();
  • }
  • public void makeSets(List<Node> nodes) {
  • fatherMap.clear();
  • sizeMap.clear();
  • for (Node node : nodes) {
  • fatherMap.put(node, node);
  • sizeMap.put(node, 1);
  • }
  • }
  • private Node findHead(Node node) {
  • Node father = fatherMap.get(node);
  • if (father != node)
  • father = findHead(father);
  • fatherMap.put(node, father);
  • return father;
  • }
  • public boolean isSameSet(Node a, Node b) {
  • return findHead(a) == findHead(b);
  • }
  • public void union(Node a, Node b) {
  • if (a == null || b == null) {
  • return;
  • }
  • Node aHead = findHead(a);
  • Node bHead = findHead(b);
  • if (aHead != bHead) {
  • int aSetSize = sizeMap.get(aHead);
  • int bSetSize = sizeMap.get(bHead);
  • if (aSetSize <= bSetSize) {
  • fatherMap.put(aHead, bHead);
  • sizeMap.put(bHead, aSetSize + bSetSize);
  • } else {
  • fatherMap.put(bHead, aHead);
  • sizeMap.put(aHead, aSetSize + bSetSize);
  • }
  • }
  • }
  • }
  • public static void main(String[] args) {
  • Scanner cin = new Scanner(System.in);
  • int n = cin.nextInt();
  • int d = cin.nextInt();
  • unionFindSet ufd = new unionFindSet();
  • List<Node> list = new LinkedList<Node>();
  • for (int i = 0; i < n; i++) {
  • int x = cin.nextInt();
  • int y = cin.nextInt();
  • list.add(new Node(x, y, false));
  • }
  • ufd.makeSets(list);
  • while (cin.hasNext()) {
  • String start = cin.next();
  • if (start.equals("O")) {
  • int p = cin.nextInt();
  • Node a = list.get(p - 1);
  • a.isGood = true;
  • list.set(p - 1, a);
  • for (int i = 0; i < list.size(); i++)
  • if (i != p - 1 && list.get(i).isGood && distance(a, list.get(i)) <= d * d)
  • ufd.union(a, list.get(i));
  • } else if (start.equals("S")) {
  • int p = cin.nextInt();
  • int q = cin.nextInt();
  • if (ufd.isSameSet(list.get(p - 1), list.get(q - 1)))
  • System.out.println("SUCCESS");
  • else
  • System.out.println("FAIL");
  • }
  • }
  • }
  • static double distance(Node a, Node b) {
  • return Math.pow((a.x - b.x), 2) + Math.pow((a.y - b.y), 2);
  • }
  • }

POJ 1182展开目录

经典的种类并查集问题,我在这道题上卡了至少有一个星期才搞懂

比较简单的并查集开一倍空间存节点即可,但是这道题需要开三倍空间,我们把这三个空间分别记为 A、B、C,对于任意一只动物,三个空间中都存有,比方说总共有 N 只动物,第 i 只动物的编号是 i,那么在 A、B、C 三个空间中的编号分别为 i、i + N、i + (N << 1)

这三个空间 A、B、C 之间相互有一定的关系,A 中的所有动物吃 B 中的所有动物;B 中的所有动物吃 C 中的所有动物;C 中的所有动物吃 A 中的所有动物

在不考虑矛盾的情况下,合并同种动物 a 和 b 的代码如下

  • conn(a, b)
  • conn(a + N, b + N)
  • conn(a + (n << 1), b + (n << 1))

上面代码很好理解,就是将 A 空间内的 a 和 b,以及 B 空间内的 a 和 b;C 空间内的 a 和 b 合并

由于三个空间具有相互捕食的关系,因此如果有动物 a 吃动物 b,他们之间的合并代码应该如下所示

  • conn(a, b + N)
  • conn(a + N, b + (n << 1))
  • conn(a + (N << 1), b)

如果这里不理解,就回过头仔细看看 A、B、C 三个空间的关系

上面是不考虑矛盾的情况,实际上在所有的合并之前,应该考虑会不会存在矛盾,下面我们探讨如何判断矛盾

假设动物 a 和 b 是同一物种,那就去查询 a 和 b + N、a 和 b + (N << 1) 是否在同一个集合内,前者判断的是 a 是否吃 b,后者判断的是 b 是否吃 a,只要这两者满足其一,就和当前的操作矛盾(当前操作是 a 与 b 是同一物种)

假设 a 吃 b,那就去查询 a 和 b、a 和 b + (N << 1) 是否在同一集合内,前者判断 a 和 b 是否是同一物种,后者判断 b 是否吃 a(不可能存在 a 吃 b,b 也吃 a),只要这两者满足其一,就和当前的操作矛盾(当前操作是 a 吃 b)

  • #include <iostream>
  • using namespace std;
  • int n, m;
  • int f[150004];
  • int size[150004];
  • int find(int a) {
  • if (f[a] == a)
  • return a;
  • return f[a] = find(f[a]);
  • }
  • bool same(int a, int b) {
  • return find(a) == find(b);
  • }
  • void conn(int a, int b) {
  • int fa = find(a);
  • int fb = find(b);
  • if (fa != fb) {
  • int sa = size[fa];
  • int sb = size[fb];
  • if (sa <= sb) {
  • f[fa] = fb;
  • size[fb] = sa + sb;
  • } else {
  • f[fb] = fa;
  • size[fa] = sa + sb;
  • }
  • }
  • }
  • int main() {
  • int ans = 0;
  • scanf("%d %d", &n, &m);
  • for (int i = 0; i <= (n << 1) + n; i++) {
  • f[i] = i;
  • size[i] = 1;
  • }
  • for (int i = 0; i < m; i++) {
  • int op, a, b;
  • scanf("%d %d %d", &op, &a, &b);
  • if (a > n || b > n || a < 1 || b < 1)
  • ans++;
  • else if (op == 2 && a == b)
  • ans++;
  • else if (op == 1) {
  • if (same(a, b + n) || same(a, b + (n << 1)))
  • ans++;
  • else {
  • conn(a, b);
  • conn(a + n, b + n);
  • conn(a + (n << 1), b + (n << 1));
  • }
  • } else {
  • if (same(a, b) || same(a, b + (n << 1)))
  • ans++;
  • else {
  • conn(a, b + n);
  • conn(a + n, b + (n << 1));
  • conn(a + (n << 1), b);
  • }
  • }
  • }
  • printf("%d\n",ans);
  • }

POJ 1703展开目录

题目大意是说,有两类团伙,输入 D a b 表示确认 a 和 b 是不同的团伙,输入 A a b 表示查询 a 和 b 是否是同一团伙

食物链相比这算是比较简单的种类并查集,问题在于如何处理不同的关系,并查集维护的是相同的集合,但这道题维护的是不同的

具体做法是开两倍空间 A 和 B,第 i 个人在 A 和 B 空间的编号分别为 i,i + N。conn (a, b) 表示 a 和 b 属于同一集合,conn (a, b + n) 和 conn (a + n, b) 表示 a 和 b 属于不同集合

如果 a 和 b 是同伙,则 same (a, b) 或 same (a + n, b + n) 返回 true

如果 a 和 b 不是同伙,则 same (a, b + n) 或 same (a + n, b) 返回 true

如果上面两种情况都没有,说明这两个人暂时不知道什么关系

  • #include <iostream>
  • using namespace std;
  • int n, m;
  • int f[200005];
  • int find(int a) {
  • if (f[a] == a)
  • return a;
  • return f[a] = find(f[a]);
  • }
  • bool same(int a, int b) {
  • return find(a) == find(b);
  • }
  • void conn(int a, int b) {
  • int fa = find(a);
  • int fb = find(b);
  • if (fa != fb)
  • f[fa] = fb;
  • }
  • int main() {
  • int ans = 0;
  • int t;
  • scanf("%d", &t);
  • while (t--) {
  • scanf("%d%d", &n, &m);
  • for (int i = 0; i <= (n << 1); i++)
  • f[i] = i;
  • for (int i = 0; i < m; i++) {
  • char op;
  • int a, b;
  • scanf("\n%c%d%d", &op, &a, &b);
  • if (op == 'D') {
  • conn(a, b + n);
  • conn(a + n, b);
  • } else {
  • if (same(a, b + n) || same(a + n, b))
  • printf("In different gangs.\n");
  • else if (same(a, b) || same(a + n, b + n))
  • printf("In the same gang.\n");
  • else
  • printf("Not sure yet.\n");
  • }
  • }
  • }
  • return 0;
  • }

AOJ 2170展开目录

这题目我看了半天实在是看不太懂,最后找谷歌翻译了全篇

大意是说输入 N 个点,Q 次操作。接下来输入 N-1 行数据,第一行输入 p1 表示第 2 个点的父节点是 p1,第二行输入 p2 表示第 3 个点的父节点是 p2。然后输入 Q-1 行操作,M a 表示标记 a 点,Q b 表示询问离 b 最近的被标记的祖先

水题,标记这个操作其实可以看作将节点的父节点指向自己,即将当前节点从其父节点中分离出来,但是跟在当前节点后面的所有节点还是跟着自己

为了保证节点间的关系,不能使用路径压缩

  • #include <stdio.h>
  • #define ll long long
  • int f[100001];
  • int find(int a) {
  • return a == f[a] ? a : find(f[a]);
  • }
  • int main() {
  • ll n, m;
  • while (scanf("%lld %lld", &n, &m) != EOF && n + m) {
  • for (int i = 2; i <= n; i++)
  • scanf("%d", &f[i]);
  • f[1] = 1;
  • ll ans = 0;
  • char op[5];
  • int t;
  • for (int i = 0; i < m; i++) {
  • scanf("%s%d",op, &t);
  • if (op[0] == 'Q')
  • ans += find(t);
  • else
  • f[t] = t;
  • }
  • printf("%lld\n", ans);
  • }
  • return 0;
  • }
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment