算法训练 - 并查集
这个是我做 kuangbin带你飞专题训练的第三个专题,对应的是 kuangbin带你飞专题五,这个专题是并查集专题,包含14道题目。
并查集属于树形结构,是一种用来管理元素分组情况的数据结构。并查集可以高效的进行如下操作:
- 查询元素 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;
}
上一篇: 下标之和的问题