所谓的子图同构任务,即给定一个target graph, G1=(N1,B1) 和一个query graph, G2=(N2,B2) 。其中 N 是点集, B 是边集。我们希望找到一组映射 (n,m) (其中 n∈G1 , m∈G2 ),使得两个图的对应点label相同,query graph中的边,在target graph中对应点之间都存在,且label相同。
如下图:
左边可以同构到右边的上面的三角,或者下面的三角。
这个算法是在2004年的A (sub) graph isomorphism algorithm for matching这篇论文中提出的。论文可以在这里下载。
VF2算法既可以做图同构,也可以做子图同构。这里以有向图的子图同构为例来讲。
核心思想其实很简单,就是搜索,加剪枝。重点就在于如何剪枝。
这里使用state s 来存储搜索过程中的部分匹配(partial mapping),以及其他算法需要的数据。
下面定义几个符号, M(s) 代表中间状态 s 代表的部分匹配。 M1(s) 和 M2(s) 表示当前state s 的部分匹配中, G1 和 G2 中的点。
算法伪代码如下:
起初,状态是 s0 , M(s0) 是空集,即还没有任何匹配。之后递归的进行搜索。
如果当前状态 s 代表的部分匹配 M(s) 包含了 G2 (query graph)中的所有节点,则已经找到了 G2 在 G1 中同构的子图,搜索结束。
否则,在当前的局部匹配基础上,再匹配一个点。首先,找出所以可能进行匹配点对集合 P(s) 。然后,对于每一个匹配对 p ,检查加入匹配 p 是否可行。即加入 p 后,两个图还是否同构。以及加入 p 之后,是否还有就扩展的可能性(即实行一些剪枝策略)。
如果加入匹配 p 可行,则将 p 加入 s ,递归调用 Match(s) ,继续搜索。
如果刚才若干次调用 Match(s) 后都没有找到同构的子图,则说明当前从状态不可能扩展出可行的子图同构匹配。所以,将生成改状态时加入的两点匹配 p 从s中删除,回溯到上一个状态。
对于之前算法框架中,新加入的匹配 p ,我们要检验其加入的可行性,从而对搜索空间进行剪枝,来提高算法的效率。
这里先定义几个符号: N1 和 N2 表示图1和图2中的点集。 n 和 m 分别表示图1和图2中的点。 Pred(G,n) 表示点 n 在图 G 中的前驱, Succ(G,n) 表示点 n 在图 G 中的后继。 Tin1(s) 和 Tin2(s) 表示状态 s 在图1和图2中,指向当前已经匹配的点集的所有边的source点集合(边的起点)。 Tout1(s) 和 Tout2(s) 表示状态 s 在图1和图2中,从当前已经匹配的点集出发的所有边的target点结合(边的终点)。 T1(s)=Tin1(s)∪Tout1(s) ,即当前状态 s 在图1中已经匹配的点集的所有一步邻居。 N˜=N1−M1(s)−T1(s) ,即图1中,除了 s 中已经匹配的点,和这些点的一步邻居以外的点。
具体的判定规则如下:
前两条保证加入新的匹配对 p 后,两个子图仍然是同构的。设新加入的匹配对是 (n,m) ,则对于 n 在图1中的所有前驱(或后继),必须能在图2中 m 的前驱(或后继)里有相应的点与之对应。同样,对于 m 在图2中的所有前驱(或后继),也必须能在图1中 n 的前驱(或后继)里有相应的点与之对应。
之后的三条都是剪枝策略。其中Card表示求集合中元素的个数。
三四条表示, n 在 Tin1 (或 Tout1 )中的前驱(或后继)的数目,必须大于等于 m 在 Tin2 (或 Tout2 )中的前驱(或后继)的数目。如果不满足,则说明对于query graph中新匹配的点 m ,其邻居个数是大于target graph中 n 的邻居个数的,所以说最终必然无法完全匹配query graph中所有的点。
第五条跟三四条思想类似,只不过考虑的两步邻居。具体来说,三四部中的考虑的邻居是 Tin1 和 Tout1 中的邻居。这些点即跟 n 相邻,又跟当前匹配中的其他点相邻。而第五条考虑的邻居,是只跟 n 相邻,跟当前匹配中其他店不相邻的邻居。这样细粒度的考虑的好处是,可以更细粒度的剪枝,从而提高剪枝效率。
如果 Tout1 和 Tout2 都不为空,则取这两个集合中的所有点两两组合,生成候选匹配对集合;
否则如果上面两个集合都为空,若 Tin1 和 Tin2 都不为空,则取这两个集合中的所有点两两组合,生成候选匹配对集合;
最坏的情况,上面四个集合都为空(对于非连通图可能有这种情况)。则只能找两个图中,所有没有匹配的点两两组合,生成候选匹配对集合了。
之所以这么细粒度的分情况讨论,是想尽量减少单次生成的候选匹配对的数量。否则如果每次都按上面第三种方式生成,则每次递归都会生成很多之前生成过的匹配对,造成重复计算。
其实,上述方法还可以进一步优化。
上述三个做法其实模式都是相同的,在target graph中找一个集合 Tt ,在query graph找一个集合 Qt 。让 Tt 和 Qt 中的点两两组合来生成候选匹配对集合。
其实,我们可以只选择 Qt 中的一个点(而不是 Qt 中所有点),跟 Tt 中的点组合,生成候选匹配对集合。这样仍然可以遍历整个搜索空间。
这么做之所以可行,是因为选了这个 Qt 中的点后,在下一步递归时, Qt 中的其他点还是会出现在 Qt+1 中,所以我们的搜索空间是不会受到损失的。
但是,这么做之后,我们每一步的候选匹配对数量就从 |Qt|∗|Tt| 缩减到了 |Tt| 。
在本文提供的代码中,我保留了这两种实现方式。实验发现这两种方法的效率差距还是很显著的。
我放在github上了,地址点我。注释写的还算比较详尽,变量名也基本跟论文中保持一致,应该还是挺好懂的。用java实现,虽然是用maven创建的项目,但是没有用任何外部的包。所以正常方式导入应该也可以运行。
其中还包括了一些实验数据(含target和query),以及VF2的论文。
这个代码其实是为“海量图数据的管理和挖掘”这门课的一个作业写的,实验数据也是作业提供的。虽然论文中该实现的特性都实现了,但是由于没有标准答案,所以暂时无法验证代码的正确性。我只是挑了几个子图匹配的结果,人工看了一下。人工看的结果倒是都是对的。
不过这个代码还是仅供学习吧,真的要用的话应该还需要进一步验证其正确性。
下面是用代码中提供的数据集测试的结果。
其中target graph有1万个,共6组query graph,每一组都是1000个query,不同之处是这6组的query graph的边数分别是:4,8,12,16,20,24
平均每个query耗时如下(在15年Mac Book Pro上跑的):
可以看到匹配耗时不随query的size增大而增大,符合论文中描述的特性(Ullmann算法的耗时是随着query的size增大而增大)。大概平均160毫秒的样子。