您的位置:首页 > 其它

算法训练 - 并查集

2018-02-18 00:37 1356 查看
这个是我做 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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 并查集