算法训练 - 并查集
2018-02-18 00:37
1356 查看
这个是我做 kuangbin带你飞专题训练的第三个专题,对应的是 kuangbin带你飞专题五,这个专题是并查集专题,包含14道题目。
并查集属于树形结构,是一种用来管理元素分组情况的数据结构。并查集可以高效的进行如下操作:
查询元素 a 和元素 b 是否属于同一个组
合并元素 a 和元素 b 所在的分组
值得注意的是,并查集虽然可以进行合并操作,但是不能进行分割操作。当然了这并不意味着包含分割操作的题目就不能用并查集解决(如 ZOJ-3261 Connections in Galaxy War)。
并查集的实现
并查集实现中的注意点
避免退化
路径压缩
并查集的结构
并查集的综合应用
并查集与动态规划
并查集与贪心算法
以上就是最简单的并查集的实现,除了这样的,还有一种带权并查集。
经典例题 HDU-1213 How Many Tables
解题思路: 没什么好说的,就是以上并查集模板的简单应用
对于每棵树,记录这棵树的高度 rank
合并时如果两棵树的高度(rank)不同,那么从高度较小的向高度较大的连边
再使用这种简化方法时,为了简单起见,即使树的高度发生了变化,也不修改 rank 的值。当然了有时候,rank 不仅仅作为树的高度,还可以根据题意,赋予特殊的含义,比如说 ZOJ-3261 Connections in Galaxy War.
所以,如果并查集中的元素十分稀疏,在进行离散化的时候,一般不必考虑各个元素的相对大小,只要可以符合题意组成一棵树即可。
经典例题 POJ 1733 Parity game
解题思路:这道题是带权并查集的应用,首先要会推导路径压缩和合并分组时的公式,其次,这道题的数据比较稀疏,因此需要离散化,如上述所说,各个节点之间的关系并不严格,所以只要用 map 简单的离散化一下就可以了。
解题思路: 这道题比较有难度,关于并查集的部分倒是不难想。根据题意可以知道,说 yes 的人与被他说的人是同类,说 no 的人与被他说的人不是同类,这里可以用带权并查集,权值是当前节点与父节点的关系,关系有两种,一种是 0 表示同类,另一种是 1 表示不是同类。路径压缩时权值的的改变的方式是,当前节点与新父节点的关系等于 当前节点与旧父节点的关系加上旧父节点与旧…爷爷节点的关系 模2。在合并分组时权值改变的方式是,节点 x 加上节点 y 的权值再加上描述中两个节点之间的关系模2。
另一个知识点是动态规划,在经过以上步骤之后,现在有很多个连通分量,每个连通分量里面应该有两类,但是不知道哪一类是好人哪一类是坏人,所以要在每一个连通分量中取一类,最终组成一组,看看这组的人数是不是与好人人数相同。如果这样的取法仅有一种,则代表有正解。用 dp[i][j] 来表示从前 i 个连通分量中取 j 个人,一共有多少种取法,初始化 dp[0][0] = 1,余下所有为 0.确定有正解之后,再用 dp这个数组来逆推从每一个联通分量中选择了哪一类
解题思路: 先将给的数据按照 px 从大到小排序,再先按照最大售卖期限初始化并查集,然后对于结构体数组中的每个成员,如果它售卖期限在并查集中的父节点大于 0,那么 i 将会在这一天售卖,同时将其父节点减一,代表和 i 的期限在同一天的商品,最晚会在这一天之前卖出。如此,便可求出最大利润。
并查集属于树形结构,是一种用来管理元素分组情况的数据结构。并查集可以高效的进行如下操作:
查询元素 a 和元素 b 是否属于同一个组
合并元素 a 和元素 b 所在的分组
值得注意的是,并查集虽然可以进行合并操作,但是不能进行分割操作。当然了这并不意味着包含分割操作的题目就不能用并查集解决(如 ZOJ-3261 Connections in Galaxy War)。
并查集的实现
并查集实现中的注意点
避免退化
路径压缩
并查集的结构
并查集的综合应用
并查集与动态规划
并查集与贪心算法
并查集的实现
下面是并查集实现的例子。在例子中,用编号代替每个元素。数组 par 表示的是父节点的编号,当 par[x] = x 时,x 是所在树的根int par[MAX_N], rank[MAX_N]; void init(int n) { for (int i = 0;i < n;i++) { par[i] = i; rank[i]= 0; } } int find(int x) { if (x == par[x]) return x; return par[x] = find(par[x]); } void unite(int x, int y) { x = find(x); y = find(y); if (x != y) { if (rank[x] > rank[y]) par[y] = x; else { par[x] = y; if (rank[x] == rank[y]) rank[y] += 1; } } } bool same(int x, int y) { return (find(x) == find(y)); }
以上就是最简单的并查集的实现,除了这样的,还有一种带权并查集。
经典例题 HDU-1213 How Many Tables
解题思路: 没什么好说的,就是以上并查集模板的简单应用
#include <cstdio> #include <set> using namespace std; int const maxn = 1005; int N,M; int rankk[maxn], par[maxn]; // 并查集 void init(int n) { for (int i = 1;i <= n;i++) { rankk[i] = 1; par[i] = i; } } int find_node(int x) { if (x == par[x]) return x; return par[x] = find_node(par[x]); } void unite(int x, int y) { x = find_node(x); y = find_node(y); if (x != y) { if (rankk[x] > rankk[y]) par[y] = x; else { par[x] = y; if (rankk[x] == rankk[y]) rankk[x] += 1; } } } int main() { int tcs; scanf("%d", &tcs); set<int> uniqu; for (int i = 1;i <= tcs;i++) { scanf("%d%d", &N, &M); init(N); int a, b; for (int i = 0;i < M;i++) { scanf("%d%d", &a, &b); unite(a, b); } uniqu.clear(); for(int i = 1;i <= N;i++) uniqu.insert(find_node(i)); printf("%d\n",uniqu.size()); if (i < tcs) scanf("\n"); } return 0; }
并查集实现中的注意点
避免退化
在树形数据结构中,如果发生退化,那么复杂度就会变得很高。因此,有必要想办法避免退化的发生,在并查集中,有如下的方法可以避免退化:对于每棵树,记录这棵树的高度 rank
合并时如果两棵树的高度(rank)不同,那么从高度较小的向高度较大的连边
路径压缩
此外,通过路径压缩,可以使并查集变得更加高效。路径压缩是:对于每个节点,一旦向上走到了一次根节点,那么就把这个点到父节点的边改为直接连向根。在此之上,不仅仅是所查询的节点,在查询过程中向上所经过的所有节点,都改为直接连到根上。这样再次查询这些节点的时候,很快就可以知道根是谁了。再使用这种简化方法时,为了简单起见,即使树的高度发生了变化,也不修改 rank 的值。当然了有时候,rank 不仅仅作为树的高度,还可以根据题意,赋予特殊的含义,比如说 ZOJ-3261 Connections in Galaxy War.
并查集的结构
并查集也是使用树形结构实现的,不过不是二叉树。每一个元素对应一个节点,每个组对应一个棵树。在并查集中,哪个节点是哪个节点的父节点以及树的形状等信息无需多加关注,整体组成一个树形结构才是重要的。所以,如果并查集中的元素十分稀疏,在进行离散化的时候,一般不必考虑各个元素的相对大小,只要可以符合题意组成一棵树即可。
经典例题 POJ 1733 Parity game
解题思路:这道题是带权并查集的应用,首先要会推导路径压缩和合并分组时的公式,其次,这道题的数据比较稀疏,因此需要离散化,如上述所说,各个节点之间的关系并不严格,所以只要用 map 简单的离散化一下就可以了。
#include <cstdio> #include <map> #include <string> #include <iostream> #include <algorithm> using namespace std; int const maxn = 10010; int N, M, mi = 1; // 并查集 struct node{ int par, rea; }game[maxn]; void init(void) { for (int i = 0;i < maxn;i++) { game[i].par = i; game[i].rea = 0; } } int find_node(int x) { if (game[x].par == x) return x; int tmp = game[x].par; game[x].par = find_node(game[x].par); game[x].rea = game[tmp].rea ^ game[x].rea; return game[x].par; } bool unite(int x,int y, int rea) { int px = find_node(x); int py = find_node(y); if (px != py) { game[py].par = px; game[py].rea = game[x].rea ^ game[y].rea ^ rea; return true; } else { if ((game[x].rea^game[y].rea) == rea) return true; return false; } } int main() { scanf("%d%d", &N, &M); init(); map<int, int> mp; int a, b, x, y, ans; bool flag = true; string des; ans = M; for (int i = 0;i < M;i++) { scanf("%d%d", &a, &b); cin >> des; if (!flag) continue; x = min(a, b); y = max(a, b); x -= 1; int tmp_rea = (des=="even"?0:1); if (mp.find(x) == mp.end()) mp[x] = mi++; int mx = mp[x]; if (mp.find(y) == mp.end()) mp[y] = mi++; int my = mp[y]; flag = unite(mx, my, tmp_rea); if (!flag) ans = i; } printf("%d\n", ans); return 0; }
并查集的综合应用
并查集作为一种数据结构,很少会单独考察,通常是和其他的算法一同考察。并查集与动态规划
经典例题 POJ-1417 True Liars解题思路: 这道题比较有难度,关于并查集的部分倒是不难想。根据题意可以知道,说 yes 的人与被他说的人是同类,说 no 的人与被他说的人不是同类,这里可以用带权并查集,权值是当前节点与父节点的关系,关系有两种,一种是 0 表示同类,另一种是 1 表示不是同类。路径压缩时权值的的改变的方式是,当前节点与新父节点的关系等于 当前节点与旧父节点的关系加上旧父节点与旧…爷爷节点的关系 模2。在合并分组时权值改变的方式是,节点 x 加上节点 y 的权值再加上描述中两个节点之间的关系模2。
另一个知识点是动态规划,在经过以上步骤之后,现在有很多个连通分量,每个连通分量里面应该有两类,但是不知道哪一类是好人哪一类是坏人,所以要在每一个连通分量中取一类,最终组成一组,看看这组的人数是不是与好人人数相同。如果这样的取法仅有一种,则代表有正解。用 dp[i][j] 来表示从前 i 个连通分量中取 j 个人,一共有多少种取法,初始化 dp[0][0] = 1,余下所有为 0.确定有正解之后,再用 dp这个数组来逆推从每一个联通分量中选择了哪一类
#include <cstdio> #include <cstring> #include <map> using namespace std; int const maxp = 605; int dp[maxp][maxp], bag[maxp][2]; // 映射 map<int, int> cc; int cnt; void cc_insert(int x, int rea) { if (cc.find(x) == cc.end()) cc[x] = ++ cnt; bag[cc[x]][rea] += 1; } // 并查集 struct node{ int par, rea; }s[maxp]; void init(int n) { for (int i = 0; i <= n;i++) { s[i].par = -1; s[i].rea = 0; } } int find_node(int x) { if (s[x].par == -1) return x; int root = find_node(s[x].par); s[x].rea = (s[x].rea + s[s[x].par].rea)%2; return s[x].par = root; } void unite(int x, int y, int rea) { int px = find_node(x); int py = find_node(y); if (px != py) { s[px].par = py; s[px].rea = (s[x].rea + s[y].rea + rea)%2; } } bool same(int x, int y) { return (find_node(x) == find_node(y)); } int main() { int n, p1, p2; while (scanf("%d%d%d", &n, &p1, &p2), n+p1+p2) { if(n==0&&p1==0&&p2==0) break; cc.clear(); init(p1+p2); for (int i = 1;i <= n;i++) { int xi, yi; char ans[8]; scanf("%d%d%s", &xi, &yi, ans); int tmp_rea; if (ans[0] == 'y') tmp_rea = 0; else tmp_rea = 1; if (!same(xi, yi)) unite(xi, yi, tmp_rea); } // 将连通分量映射到 map 上,重新编号 cnt = 0; memset(bag, 0, sizeof(bag)); for (int i = 1;i <= p1+p2;i++) { int fi = find_node(i); cc_insert(fi, s[i].rea); } memset(dp, 0, sizeof(dp)); dp[0][0] = 1; for (int i = 1;i <= cnt;i++) { for (int j = 0;j <= p1;j++) { if (j >= bag[i][0]) dp[i][j] = dp[i-1][j-bag[i][0]]; if (j >= bag[i][1]) dp[i][j] += dp[i-1][j-bag[i][1]]; } } // 逆推路径并输出 if (dp[cnt][p1] == 1) { int choose[cnt+1], p = p1; memset(choose, -1, sizeof(choose)); for (int i = cnt;i > 0 && p > 0;i--) { if (dp[i][p] == dp[i-1][p-bag[i][0]]) choose[i] = 0; else if (dp[i][p] == dp[i-1][p-bag[i][1]]) choose[i] = 1; p -= bag[i][choose[i]]; } for (int i = 1;i <= p1+p2;i++) { int fa = find_node(i); int num = cc[fa]; if (s[i].rea == choose[num]) printf("%d\n", i); } printf("end\n"); } else printf("no\n"); } return 0; }
并查集与贪心算法
经典例题:POJ-1456 Supermarket解题思路: 先将给的数据按照 px 从大到小排序,再先按照最大售卖期限初始化并查集,然后对于结构体数组中的每个成员,如果它售卖期限在并查集中的父节点大于 0,那么 i 将会在这一天售卖,同时将其父节点减一,代表和 i 的期限在同一天的商品,最晚会在这一天之前卖出。如此,便可求出最大利润。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; int const maxn = 10005; struct node{ int px, dx; bool operator < (const node &c) { return px > c.px; } }prdct[maxn]; // 并查集 int par[maxn]; void init(int n) { for (int i = 1;i <= n;i++) par[i] = i; } int find_node(int x) { if (x == par[x]) return x; return par[x] = find_node(par[x]); } int main() { int N; while (~scanf("%d", &N)) { int maxd = 0; for (int i = 0;i < N;i++) { scanf("%d%d", &prdct[i].px, &prdct[i].dx); maxd = max(maxd, prdct[i].dx); } sort(prdct, prdct+N); init(maxd); int ans = 0; for (int i = 0;i < N;i++) { int ddl = find_node(prdct[i].dx); if (ddl > 0) { ans += prdct[i].px; par[ddl] = ddl - 1; } } printf("%d\n", ans); } return 0; }
相关文章推荐
- 《算法竞赛-训练指南》第三章-3.6_LA 3027(并查集)
- 算法训练 安慰奶牛 (并查集—路径压缩、Kruskal算法)
- 算法训练 图形显示
- 并查集(Union-Find)算法介绍
- 【UVA10462】【好题】【次小生成树并查集算法】
- SVM基础一:一个简单的训练算法
- 隐马尔可夫训练参数,BaumWelch算法,java实现【参考52nlp的博客算法原理实现】
- 算法训练 6-2递归求二进制表示位数
- 算法训练 水仙花
- 蓝桥杯 算法训练 矩阵乘法
- 算法训练 前缀表达式
- 算法训练 数组查找及替换
- 算法训练 安慰奶牛
- 普林斯顿公开课 算法1-7:并查集基本概念
- 蓝桥杯 ALGO-63 算法训练 乘法表
- 蓝桥杯-算法训练-和为T
- Java-蓝桥杯-算法训练 6-1 递归求二项式系数值
- 算法训练 纪念品分组
- 算法训练 字符删除
- 算法训练 P1103