连通性问题指的是:有一组序列,然后再给定一组连通组合,可以判断其中两个元素的连接状态。
用个例子来说就是:如果A和B连接,同时A和C连接,那么就认为B和C是连接的。这里有个约定是连接是双向的。
再生活化一点的例子就比方说:A和B的网络是通的,A和C的网络也是通的,那么,我们可以认为B和C的网络也是通的。(B->A->C)
建模:
我们想象一下,用一个0-N的数组来表示各个节点的连接状态,假定共有10个元素,数组的每一个元素对应与起连接的节点index,我们用简单的遍历方式来修改数组,开始的时候是这样的。
int[] id= {0,1,2,3,4,5,6,7,8,9};
再给出一个连接操作(1,3),那么对应的节点“1”的下标就成为了“3”,a[1] = 3
int[] id= {0,3,2,3,4,5,6,7,8,9};
这时,id[1]和id[3]是一样的。嗯,我们看出来他们连接了。
如果再出一个连接操作(1,4),再改变节点“1”下标改成“4“,额。。。那3怎么办?把3也改成4好了。数组现在是这样:
a[1] = 4
a[3] = 4
id= {0,4,2,4,4,5,6,7,8,9};
这样,一个最简单的连通问题就解决了,现在看看算法的复杂度。
我们使用的是遍历数组的方式,来把1和3都变成4的。
对N个对象的连通性问题,如果执行M次合并操作,那么合并算法至少执行MN(每一次合并操作都要遍历一次数组)次指令。
思考:如何让让程序跑的更快
问题有了,我们思考看看哪里可以提高,对于简单的连通问题我们大概做了这样三件事情(模型不变的情况下):
建立数组来存储所有元素 2.做合并操作 3. 查询两个节点是否连接。
1看起来不是我们算法里面的内容。3暂且看起来蛮简单的,只要比较两个节点的内容是否相同,但是这个结果好像蛮依赖合并操作的,简单起见,先不管它。
重点看合并操作,这个操作又可以细分成下面的步骤:比如我们有节点a,b,那么,我们要找到所有与节点a相连的,然后再把它们连上b。
如果我们有一个偷懒的办法,利用数组的某些特性,可以提高效率呢?考虑只有4个元素的{0, 1, 2, 3},节点1和节点3相连,我们有看看有哪些结论?
a[1] = 3
a[3] = 3
a[a[1]] = 3
哈,我们看到一个巧合,a[2]这个节点被我们跳过去了。(当然不是巧合,不然你以为节点为什么要从0开始编号),那么,我们是不是感觉到这样一个树形
于是,我们只要写这一个类似的循环,就能跳过无关(不相连)的点,以下是伪代码
void merge(int p, int q)
{
validate(p);
validate(q):
if (p == q) return;
int i = a[p];
int j = a[q];
while(i != a[i]) { i = a[i]; }
while(j != a[j]) { j = a[j]; }
connect(i, j);
}
你可能会怀疑,这样找到“根”的方式,靠谱不靠谱,玩意有两个节点比如m,n 刚好a[m] = n, a[n] = m,不是这不是死循环了吗?
如果真有这两个节点,确实会出现这样的情况,所以,你自己得保证不出现这样的情况。
采用这样的方法,感觉会好一些,但是如果有这样的一组,[1,2,3,4,5,6],0节点连着1,1节点连着2,2...,就算while,还是每个元素都遍历了一遍啊,那就再想想法子,
我能不能尽量出现少的数字类型,上面是1-6,但是其实他们的节点都相连,我们代码再这么改一下
void merge(int p, int q)
{
validate(p);
validate(q):
if (p == q) return;
int i = a[p];
int j = a[q];
// 找到p那边的根
while(j != a[j]) { j = a[j];}
// 把a这边的分支包括都连到p那边
while(i != a[i]) { i = a[i]; a[i] = j;}
// p的根于q的根相连
connect(i, j);
}
每次合并的时候都把多余的数字去掉(改成q的根节点)