一、并查集定义
并查集是一种树型的数据结构,用于处理一些不想交集合的合并与查找问题。换言之,disjointSet主要包括unionSet(合并集合)与findRoot(查找集合)两种操作,程序实现上有quick-find和quick-union两种性能倾向。
二、并查集的用途
并查集主要应用在无向图的动态联通性上。例如网络连接判断,在pat中经常用于解决的一类题目是“城市联通的问题”,本题后面会以top1001的“Battle over cities--hard version"为例,在编程中使用并查集算法。
变量名等同性问题,在程序中,可以声明多个引用来指向同一对象,这个时候就可以通过为程序中声明的引用和实际对象建立动态连通图来判断哪些引用实际上是指向同一对象。
三、并查集的程序实现
并查集有单链表实现,也有用有根树来实现(双亲表示法)。
本文主要介绍后一种,这种方法更为快速,通用,易懂,一棵树代表一个集合,树的根结点的序号代表这个集合的编号。
在细分,有数组表示法。
vector<int> parents,ranks,dates;
类表示法
class disjointSet{
vector<int> parents,ranks,dates;
int cnt;
};
数组表示法最为灵活,适合小型题目。类表示法适合大型工程,更符合面向对象的路子,易维护,易扩展。
四、battle over cities---hardversion
题目中给出若干城市和某些道路,道路有好有坏。战争中,最重要的就是要确保城市之间的连通性。如果某个城市陷落,要重建连通性花费的代价越高,这个城市就越重要。
1,先用valid的路(出去包含沦陷的那城市的道路)将城市联通。
2,然后将invalid路按cost排序。
3,最后选择invalid边,将城市联通起来,直到只剩一个集合为止。
有点像变形的kruskal算法。
这里有两个特例,重建连通性的costAll为0,还有个是无法重建连通性,程序中用costAll=INT_MAX来表达。
1.使用并查集的数组表示法,属于quick-union的类型
添加了ranks的优化手段,来平衡一下树的高度
路径压缩那种优化手段一直都有,是在findRoot递归函数里使用的。
添加了dates来记录数据,这个例子里面表达集合里元素的数目。
不过dates在本题里面没有用。
本题里的并查集编码,采用的是“数组表示法”
Union,findRoot是明摆地写出来的,其他的并查集api也容易实现。
添加了ranks的优化手段,来平衡一下树的高度
路径压缩那种优化手段一直都有,是在findRoot递归函数里使用的。
添加了dates来记录数据,这个例子里面表达集合里元素的数目。
不过dates在本题里面没有用。
本题里的并查集编码,采用的是“数组表示法”
Union,findRoot是明摆地写出来的,其他的并查集api也容易实现。
#include<stdio.h>
#include<vector>
using std::vector;
#include<algorithm>
using std::sort;
#include<limits.h>
struct edge {
int a, b;
int cost;
edge(int a_ = -1, int b_ = -1, int cost_ = -1) :a(a_), b(b_), cost(cost_) {}
bool operator<(const struct edge &x) const {
return cost < x.cost;
}//operator<
};
int findRoot(vector<int> &parents, int x) {
if (parents[x] == x)return x;
return parents[x] = findRoot(parents, parents[x]);
}//findRoot
void Union(int rootA,int rootB,vector<int> &ranks,vector<int> &parents,vector<int> &dates,int &cnt) {
if (rootA == rootB)return;
if (ranks[rootA] == ranks[rootB])++ranks[rootB];
if (ranks[rootA] < ranks[rootB])parents[rootA] = rootB, dates[rootB] += dates[rootA];
else parents[rootB] = rootA, dates[rootA] += dates[rootB];
++cnt;
return;
}//Union
int main() {
freopen("in.txt", "r", stdin);
int N(-1), M(-1);
scanf("%d%d", &N, &M);
vector<edge> edges_valid, edges_invalid;
int validEdgeCnt(0);
for (int i(0); i < M; ++i) {
int a(-1), b(-1), cost(-1), valid(-1);
scanf("%d%d%d%d", &a, &b, &cost, &valid);
if (valid == 1)edges_valid.push_back(edge(a, b, cost)), ++validEdgeCnt;
else edges_invalid.push_back(edge(a, b, cost));
}//for i
sort(edges_invalid.begin(), edges_invalid.end());
vector<int> outputs;
int maxCost(0);
for (int i(1); i <= N; ++i) {
int cnt(0);
vector<int> parents(N + 1, -1);
for (int j(0); j < N + 1; ++j)parents[j] = j;
vector<int> ranks(N + 1, 0);
vector<int> dates(N + 1, 1);
for (int j(0); j < validEdgeCnt; ++j) {
if (cnt == N - 2)break;
if (edges_valid[j].a == i || edges_valid[j].b == i)continue;
int a = findRoot(parents, edges_valid[j].a);
int b = findRoot(parents, edges_valid[j].b);
Union(a,b,ranks,parents,dates,cnt);
}//for j
if (cnt == N - 2) continue;
int costAll(0);
for (int j(0); j < M - validEdgeCnt; ++j) {
if (cnt == N - 2)break;
if (edges_invalid[j].a == i || edges_invalid[j].b == i)continue;
int a = findRoot(parents, edges_invalid[j].a);
int b = findRoot(parents, edges_invalid[j].b);
if (a != b) {
Union(a, b, ranks, parents, dates, cnt);
costAll += edges_invalid[j].cost;
}//if
}//for j
if (cnt == N - 2) {
if (costAll > maxCost) {
maxCost = costAll;
outputs.clear();
outputs.push_back(i);
}
else if (costAll == maxCost)outputs.push_back(i);
}
else {
if(maxCost!=INT_MAX)maxCost = INT_MAX, outputs.clear(), outputs.push_back(i);
else outputs.push_back(i);
}//if else
}//for i
if (maxCost == 0) {
puts("0");
return 0;
}//if
int size = (int)outputs.size();
for (int i(0); i < size-1; ++i)printf("%d ",outputs[i]);
if (size > 0)printf("%d\n",outputs[size-1]);
return 0;
}//main
容易看出程序中的findRoot是递归函数。
2、封装disjointSet类后,编写主要业务逻辑代码如下。
#include<stdio.h>
#include<vector>
using std::vector;
#include<algorithm>
using std::sort;
#include<limits.h>
struct edge {
int a, b;
int cost;
edge(int a_ = -1, int b_ = -1, int cost_ = -1) :a(a_), b(b_), cost(cost_) {}
bool operator<(const struct edge &x) const {
return cost < x.cost;
}//operator<
};
//disjointSet的定义
int main() {
freopen("in.txt", "r", stdin);
int N(-1), M(-1);
scanf("%d%d", &N, &M);
vector<edge> edges_valid, edges_invalid;
int validEdgeCnt(0);
for (int i(0); i < M; ++i) {
int a(-1), b(-1), cost(-1), valid(-1);
scanf("%d%d%d%d", &a, &b, &cost, &valid);
if (valid == 1)edges_valid.push_back(edge(a, b, cost)), ++validEdgeCnt;
else edges_invalid.push_back(edge(a, b, cost));
}//for i
sort(edges_invalid.begin(), edges_invalid.end());
vector<int> outputs;
int maxCost(0);
for (int i(1); i <= N; ++i) {
disjointSet djset(N);
for (int j(0); j < validEdgeCnt; ++j) {
if (djset.getCount() == 2)break;
if (edges_valid[j].a == i || edges_valid[j].b == i)continue;
int aRoot = djset.findRoot(edges_valid[j].a - 1);
int bRoot = djset.findRoot(edges_valid[j].b - 1);
if (djset.isOneSet(aRoot, bRoot) == false)djset.unionSet(aRoot, bRoot);
}//for j
if (djset.getCount() == 2) continue;
int costAll(0);
for (int j(0); j < M - validEdgeCnt; ++j) {
if (djset.getCount() == 2) break;
if (edges_invalid[j].a == i || edges_invalid[j].b == i)continue;
int aRoot = djset.findRoot(edges_invalid[j].a - 1);
int bRoot = djset.findRoot(edges_invalid[j].b - 1);
if (djset.isOneSet(aRoot,bRoot)==false) {
djset.unionSet(aRoot, bRoot);
costAll += edges_invalid[j].cost;
}//if
}//for j
if (djset.getCount() == 2) {
if (costAll > maxCost) {
maxCost = costAll;
outputs.clear();
outputs.push_back(i);
}
else if (costAll == maxCost)outputs.push_back(i);
}
else {
if (maxCost != INT_MAX)maxCost = INT_MAX, outputs.clear(), outputs.push_back(i);
else outputs.push_back(i);
}//if else
}//for i
if (maxCost == 0) {
puts("0");
return 0;
}//if
int size = (int)outputs.size();
for (int i(0); i < size - 1; ++i)printf("%d ", outputs[i]);
if (size > 0)printf("%d\n", outputs[size - 1]);
return 0;
}//main
注意观察disjointSet定义在for(int i(1);i<=N;++i)循环里面。
下面给出disjointSet的定义
//quick-union
//findRoot是近似O(1)的,里面采用的是路径压缩,用来得到集合序号,findRoot本身是递归实现的
//isOneSet用来判断xRoot和yRoot是否为同一集合。
//unionSet是O(1)的,采用ranks来优化的,用来合并xRoot到yRoot里面
//getDates用来得到该集合的元素个数。因此初始化全为1。dates还有别的用途。
class disjointSet {
private:
vector<int> parents, ranks, dates;
int cnt;
public:
disjointSet(int size) {
cnt = size;
parents.resize(size);
for (int i(0); i < size; ++i)parents[i] = i;
ranks.assign(size, 0);
dates.assign(size, 1);
return;
}//disjointSet
inline int findRoot(int x) {
return parents[x] == x ? x : (parents[x] = findRoot(parents[x]));
}//findRoot
inline int getCount() {
return cnt;
}//getCount
inline bool isOneSet(int xRoot, int yRoot) {
return xRoot == yRoot;
}//inOneSet
void unionSet(int xRoot, int yRoot) {
if (ranks[xRoot] == ranks[yRoot])++ranks[yRoot];
if (ranks[xRoot] < ranks[yRoot])parents[xRoot] = yRoot, dates[yRoot] += dates[xRoot];
else parents[yRoot] = xRoot, dates[xRoot] += dates[yRoot];
--cnt;
return;
}//unionSet
inline int getDates(int x) {
return dates[x];
}//getDates
};
注意该并查集的实现 quick-union的,属于类表示法。更符合面向对象的思路。
下面是quick-find的disjointSet定义
//quick-find
//findRoot是O(1)的,用来得到集合序号
//isOneSet用来判断xRoot和yRoot是否为同一集合。
//unionSet是O(n)的,用来合并xRoot到yRoot里面
//getDates用来得到该集合的元素个数。因此初始化全为1。dates还有别的用途。
class disjointSet {
private:
vector<int> parents, dates;
int cnt, size;
public:
disjointSet(int size) {
this->size = size;
cnt = size;
parents.resize(size);
for (int i(0); i < size; ++i)parents[i] = i;
dates.assign(size, 1);
return;
}//disjointSet
inline int findRoot(int x) {
return parents[x];
}//findRoot
inline int getCount() {
return cnt;
}//getCount
inline bool isOneSet(int xRoot, int yRoot) {
return xRoot == yRoot;
}//inOneSet
void unionSet(int xRoot, int yRoot) {
for (int i(0); i < this->size; ++i) {
if (parents[i] != xRoot)continue;
parents[i] = yRoot;
}//for i
dates[yRoot] += dates[xRoot];
--cnt;
return;
}//unionSet
inline int getDates(int x) {
return dates[x];
}//getDates
};
注意到上面的disjointSet的实现里面的findRoot函数都是递归的,递归可能会造成函数调用栈的溢出。
findRoot还可以用while来实现,
下面这种最直观,易懂。tmpVec来存储中间结点,然后将tmpVec里面的点统统指向x。
但是由于findRoot调用频率高,所以会频繁产生tmpVec,虽然这个数组是在栈上开辟的,但是相应的分配消耗还是存在。
int findRoot(int x) {//有个case是超时的。
vector<int> tmpVec;
while (x != parents[x]) {
tmpVec.push_back(x);
x = parents[x];
}//while
for (auto tmp : tmpVec)parents[tmp] = x;
return x;
}//findRoot
用上面的代码替代disjointSet里面的findRoot递归实现。
这是由tmpVec导致的,所以采用了将x结点的父结点设置为它的爷爷结点这个策略。
这个方法的压缩幅度不太狠,但是总体看来,效果还算不错。
int findRoot(int x) {//那个超时解决了
while (x != parents[x]) {
//这行代码也算是路径压缩,将x结点的父结点设置为它的爷爷结点。
parents[x] = parents[parents[x]];
x = parents[x];
}//while
return x;
}//findRoot
3、其他优化手段
关于ranks的那种优化手段,效果不如findRoot里面路径压缩强。
类似的压缩手段还有很多,比如dates初始化为全1,里面的值表达该集合含有元素的个数,
可以
if(dates[xRoot]<=dates[yRoot])parents[xRoot]=yRoot;
else parents[yRoot]=xRoot;
这也是为了平衡一下这根树,尽量让“小树”向“大树”靠拢。