使用 Kruskal 算法检测图中的周期

Detect cycle in a graph using Kruskal's algorithm

本文关键字:周期 检测 Kruskal 算法 使用      更新时间:2023-10-16

我正在实现克鲁斯卡尔的算法,这是找到加权图的最小跨越树的众所周知的方法。但是,我正在调整它以在图中找到周期。这是Kruskal算法的伪代码:

KRUSKAL(G):
1 A = ∅
2 foreach v ∈ G.V:
3    MAKE-SET(v)
4 foreach (u, v) ordered by weight(u, v), increasing:
5    if FIND-SET(u) ≠ FIND-SET(v):
6       A = A ∪ {(u, v)}
7       UNION(u, v)
8 return A

我很难掌握FIND-SET()MAKE-SET()函数,或者它们使用不连接集合数据结构的实现。

我当前的代码看起来像这样:

class edge {
    public:      //for quick access (temp) 
      char leftV;
      char rightV;
      int weight;
};
std::vector<edge> kruskalMST(std::vector<edge> edgeList){
    std::vector<char> set;
    std::vector<edge> result;
    sortList(edgeList);    //sorts according to weight ( passed by reference)
    do{
        if(set.empty()){
            set.push_pack(edgeList[i].leftV);    //also only push them when
            set.push_pack(edgeList[i].rightV);    //they aren't there , will fix
            result.push_back(edgeList[i]);
            ++i;
        }
        else {
            if((setContains(set , edgeList[i].leftV)) && (setContains(set , edgeList[i].rightV)))
                ++i; //skip node 
            else {
                set.push_pack(edgeList[i].leftV);    //also only push them when
                set.push_pack(edgeList[i].rightV);    //they aren't there , will fix
                result.push_back(edgeList[i]);
                ++i;
            }
     } while(i<edgeList.size());
    return result;
}

set vector中已经存在的两个顶点再次出现时,我的代码在图中检测到一个周期。在大多数情况下,直到我遇到这样的情况之前,这似乎都起作用:

  [a]              [c]
   |                |
   |                |
   |                |
  [b]              [d]

当这些边缘以排序顺序出现时,这会发生,因为abcd已经将其推入set vector。将[a]加入[c]不会在图内产生周期,但由于当前实现而被检测为周期。

在我的情况下,是否有可行的替代方法可以检测周期?或者,如果某人可以解释MAKE-SETFIND-SETUNION在Kruskal的算法中工作,这将很有帮助。

MAKE-SET(v)表示您是您初始化一个初始化一个设置仅由顶点v组成。最初,每个顶点都在一个集合中。

FIND-SET(u)是一个函数,告诉您设置哪个顶点属于。它必须返回指针或唯一标识该集合的ID号。

UNION(u, v)表示您将包含u的集合与包含v的集合合并。换句话说,如果uv在不同的集合中,则UNION操作将形成一个包含集合FIND-SET(u)FIND-SET(v)的所有成员的新集合。

当我们使用不相交的数据结构实施这些操作时,关键想法是每个集合都由领导者表示。每个顶点都有一个指向其集合中一些顶点的指针。集合的领导者是指向自身的顶点。所有其他顶点都指向父母,指针形成了树结构,其领导者是其根。

要实现FIND-SET(u),您从u开始关注指针,直到您到达设定的领导者,这是集合指向自身的唯一顶点。

要实现UNION(u, v),您将一个固定点的领导者与另一组领导者成为领导者。

可以通过按等级和路径压缩的联合思想进行优化这些操作。

按等级的联合意味着您可以跟踪一组到领导者的任何顶点的最大指针数量。这与指针形成的树的高度相同。您可以通过为每个UNION操作执行恒定数量的步骤来更新排名,这是集合排名更改的唯一一次。假设我们正在合并集合A和B,以使A的排名大于B。我们将B的领导者指向A的领导者。结果组的等级与A相同。,我们使BOIND与B的领导者成为领导者,并且由此产生的集合的等级与B相同。如果A和B的排名相同,我们选择哪个领导者都没关系。无论我们是使B的领导者成为领导者还是反之亦然,结果组的排名将大于A。

的等级。

路径压缩意味着,当我们执行FIND操作时,该操作需要按照一系列指针到集合的领导者时,我们将使我们沿着沿途遇到的所有顶点直接指向领导者。这将当前FIND操作的工作量增加了一个恒定因素,并且减少了FIND的未来调用的工作量。

如果您按等级和路径压缩实施联盟,则将实现巨大的联合信息实现。 n 操作的时间复杂性是 o(n&alpha;(n)),其中&alpha; 是逆ackermann函数。此函数变得如此慢,以至于 n 是宇宙中原子的数量,&alpha;(n)是5脱节集数据结构实际上是Union-Find的线性时间实现。

我不会重复联合/查找算法的设置理论描述(Kruskal只是一种特殊情况),但使用更简单的方法(您可以在其上应用该方法按等级和路径压缩的联合。)

为简单起见,我认为每个顶点都有一个唯一的整数ID,范围从0到顺序-1(例如,顶点ID可以用作数组的索引。)

天真的算法是如此简单,以至于代码本身说话:

int find(int v, int cc[]) {
  while (cc[v] >= 0)
    v = cc[v];
  return v;
}
bool edge_union(int v0, int v1, int cc[]) {
  int r0 = find(v0, cc);
  int r1 = find(v1, cc);
  if (r0 != r1) {
    cc[r1] = r0;
    return true;
  }
  return false;
}

CC阵列以-1的到来初始化(当然它的大小反映了图顺序。)

路径压缩可以通过在查找函数的同时循环中遇到的顶点进行堆叠来完成,然后将相同的代表设置为所有这些代表。

int find2(int v, int cc[]) {
  std::deque<int> stack;
  while (cc[v] >= 0) {
    stack.push_back(v);
    v = cc[v];
  }
  for (auto i : stack) {
    cc[i] = v;
  }
  return v;
}

对于联合等级,我们只是使用数组的负值,值越小,等级就越大。这是代码:

bool edge_union2(int v0, int v1, int cc[]) {
  int r0 = find(v0, cc);
  int r1 = find(v1, cc);
  if (r0 == r1)
    return false;
  if (cc[r0] < cc[r1])
    cc[r1] = r0;
  else {
    if (cc[r1] < cc[r0])
      cc[r0] = r1;
    else {
      cc[r1] = r0;
      cc[r0] -= 1;
    }
  }
  return true;
}