测试运行

贪心算法和最大团

James McCaffrey

下载代码示例

James McCaffery

在本月的专栏中,我将介绍图形最大 clique 问题的解决方案。解决方案使用所谓的贪婪的算法,并且我将介绍如何设计和测试这些算法。最大 clique 问题的想法是所有连接到另一个图中找到的最大组中的节点。看一看简单的图表,在图 1。图形有九个节点和 13 边。图表是 unweighted (有没有与边缘相关联的优先级) 和不定向 (所有边都是双向)。节点 2、 4 和 5 窗体大小三的 clique。最大 clique 是大小的节点集 0、 1、 3 和 4,窗体四 clique。

 

Graph for a Greedy Maximum Clique Algorithm
图 1 图为贪婪的最大值 Clique 算法

由于多种原因,最大 clique 问题非常有趣。尽管它不是很明显,从简单的图表,在图 1,最大 clique 问题是一个计算机科学中最具挑战性。许多重要的应用程序,如社交网络通信分析、 其中节点表示人和边缘表示消息或关系中,它会发生。和最大 clique 问题适用于解决方案贪婪的算法,这是一种在计算机科学中的基本技术。

非正式地讲,贪婪的算法是一种算法,以困难问题的简单、 不完整解决方案,然后反复寻找改进解决方案的最佳方法。重复此步骤,直到达到某些停止条件。图 2 说明了最大 clique 问题的重要思想转变,并显示其中我正在向此文章中。

Greedy Maximum Clique Demo Run
图 2 贪婪的最大值 Clique 演示运行

我有一个控制台应用程序,首先需要将加载到内存中对应于中所示的图表图 1。设计和编码高效的图形数据结构是在本身的重大问题。我我最后一列 (msdn.microsoft.com/magazine/hh456397) 中所述的图表数据结构代码。该算法首先单个节点,将随机选择在此案例的节点 2,并初始化与该节点 clique。接下来,该算法寻找最佳节点将添加到 clique。如果您引用图 1,您将看到有只有两个节点,4 和 5,将形成较大的 clique 的。我将解释什么最佳节点意味着不久。

该算法选择,并向 clique 添加节点 4,因此现在是 clique {2,4}。此时,在图表中,5,将增加 clique,大小,因此该算法选择节点 5,并将其添加到 clique 的是只有一个节点。现在没有节点可用,从而提高 clique 的大小。算法可以添加一个新的、 不同的节点,将使进度购并的 clique 从中删除最佳节点。同样,我稍后解释"最佳"意味着什么。该算法从 clique 中删除节点 4。正如您可以看到通过查看图形,没有方法来取得了进展。这种情况是与贪婪的算法,常见的因此必须有一种方法的 dead-end 解决方案中获取。

该算法跟踪多长时间以来已经操作已经进行 — 即已找到 clique 较大。在一定时间内没有进度之后, 该算法自行重新启动。Clique 会被清除。这一次该算法将随机地挑选节点 3 作为初始节点。该算法来查找要添加或删除的最佳节点使用相同的迭代过程,最终发现 {0、 1、 3、 4 个最大 clique。贪婪的算法的使用位置的大多数问题,在不是已知的最佳解决方案,以便该算法将继续直到满足某些停止条件。在这种情况下,该算法被配置为 20 次迭代后停止。此时,将显示找到最佳 clique。

在后面的章节中,我将指导您完成的代码生成中的屏幕快照, 图 2。完整的源代码,请访问 code.msdn.microsoft.com/mag201110TestRun (此列使用相同的代码从上个月)。本文假定您拥有中间级编程技能与 C 系列语言或 vb.NET 的语言。使用 C#,但是,这样您将能够重构它到另一种语言不太多困难的情况下,如果您愿意,我曾经写代码。

整体程序结构

所示的程序的总体结构图 2 中提供图 3。该程序在系统、 System.Collections.Generic (clique 表示为类型列表 <int>)、 System.IO (用于从文本文件加载源图形) 和 System.Collections (程序定义的 MyGraph 类使用 BitArray 类) 的命名空间上具有依赖关系。重命名模板生成的 Visual Studio 程序从 GreedyMaxCliqueProgram 到主类为清楚起见。我使用名为"随机"类范围内随机对象初始化并重新 clique 设置为一个随机的节点并添加或删除多个最佳节点时并列。

图 3 综合程序结构

using System;
using System.Collections.Generic;
using System.IO;
using System.Collections;
 
namespace GreedyMaxClique
{
  class GreedyMaxCliqueProgram
  {
    static Random random = null;
 
    static void Main(string[] args)
    {
      try
      {
        Console.WriteLine("\nBegin greedy maximum clique demo\n");
 
        string graphFile = "..
\\..
\\DimacsGraph.clq";
        Console.WriteLine("Loading graph into memory");
        Console.WriteLine("Graph loaded and validated\n");
        MyGraph graph = new MyGraph(graphFile, "DIMACS");
 
        int maxTime = 20;
        int targetCliqueSize = graph.NumberNodes;
 
        List<int> maxClique = FindMaxClique(graph, maxTime,
          targetCliqueSize);
        Console.WriteLine("\nMaximum time reached");
        Console.WriteLine("\nSize of best clique found = " +
          maxClique.Count);
 
        Console.WriteLine("\nBest clique found:");
        Console.WriteLine(ListAsString(maxClique));
 
        Console.WriteLine("\nEnd greedy maximum clique demo\n");
      }
      catch (Exception ex)
      {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    } // Main
 
    static List<int> FindMaxClique(MyGraph graph, int maxTime,
      int targetCliqueSize)
    {
      // ...
}
 
    static List<int> MakePossibleAdd(MyGraph graph, List<int> clique)
    {
      // ...
}
 
    static bool FormsALargerClique(MyGraph graph, List<int> clique,
      int node)
    {
      // ...
}
 
    static int GetNodeToAdd(MyGraph graph, List<int> possibleAdd)
    {
      // ...
}
 
    static int GetNodeToDrop(MyGraph graph, List<int> clique,
      List<int> oneMissing)
    {
      // ...
}
 
    static List<int> MakeOneMissing(MyGraph graph, List<int> clique)
    {
      // ...
}
 
    static string ListAsString(List<int> list)
    {
      // ...
}
  } // class Program
 
  public class MyGraph
  {
    // ...
}
} // ns

图形表示为一个程序定义的 MyGraph 对象。 图形加载从外部文本文件,它使用一个名为 DIMACS 的标准文件格式。 MyGraph 的主要方法是 AreAdjacent,则返回 true,如果两个节点都连接。 我将 maxTime 设置为绝对停止为设置条件的贪婪的算法的 20 时。 设置 targetCliqueSize 建立第二个的停止条件 ; 如果 clique 找到的具有相同的节点数,如有图表中,最大 clique 必须被找到。

FindMaxClique 方法

所有工作是通过 FindMaxClique,要搜索的最大可能的 clique 使用贪婪算法的方法。 FindMaxClique 调用几个帮助器方法,我将详细介绍。 我的结构使用静态方法,该程序,但您可能要重构代码,我在此向完全面向对象的方法。 FindMaxClique 方法进行迭代,直到两个停止条件之一,满足,然后返回找到最佳 clique。 计划的定义包括嵌入的 MyGraph 类,是上个月的文章的主题。

在高级别的伪代码,FindMaxClique 算法是:

initialize clique with a single node
get list of candidate nodes to add
loop until stop condition met
  if there are nodes that can be added
    find best node to add and add to clique
  else
    find best node to drop and drop from clique
 
  if lack of progress
    restart algorithm
 
  update list of candidate nodes
end loop
return largest clique found

FindMaxClique 方法定义开始:

static List<int> FindMaxClique(MyGraph graph, int maxTime,
  int targetCliqueSize)
{
  List<int> clique = new List<int>();
  random = new Random(1);
  int time = 0;
  int timeBestClique = 0;
  int timeRestart = 0;
  int nodeToAdd = -1;
  int nodeToDrop = -1;
...

本地 clique 对象是当前 clique。 我实例化 Random 对象使用任意的种子值为 1。 时间变量是循环计数器变量 ; 因为它是独立的是一个很好的替代名称"刻度线"。跟踪当前最佳 clique 找时的时间和上次重新启动来控制重新启动时将发生的时间。 我虚拟-1 为变量赋值的添加节点和拖放节点:

int randomNode = random.Next(0, graph.NumberNodes);
Console.WriteLine("Adding node " + randomNode);
clique.Add(randomNode);
...

Random.Next(x,y) 方法返回一个值大于或等于 x 并严格小于 y。 通过设置 x = 0 和 y = NumberNodes,将随机节点从 0 到 8 包括示例图形:

List<int> bestClique = new List<int>();
bestClique.AddRange(clique);
int bestSize = bestClique.Count;
timeBestClique = time;
...

BestClique 列表用来跟踪该算法的搜索过程中找到的最大 clique 的副本。 使用方便的 AddRange 方法将当前 clique 中的项复制到 bestClique。 为方便起见,则使用 bestSize 变量。 TimeBestClique 用来确定是否算法的重新启动将会发生:

List<int> possibleAdd = MakePossibleAdd(graph, clique);
List<int> oneMissing = MakeOneMissing(graph, clique);
...

MakePossibleAdd 帮助器方法检查当前 clique,并构造可以通过一项将添加到要增大 clique clique 的所有节点的列表。 此列表是最佳的节点将添加到 clique 的候选人的源。 MakeOneMissing 帮助器方法是变得有点棘手。 该方法构造的所有节点连接到当前 clique 中的所有除一个节点的列表。 如您所见,此 oneMissing 列表用于确定要从 clique 中除去的最佳节点。 现在,我开始主处理循环:

while (time < maxTime && bestSize < targetCliqueSize)
{
  ++time;
  bool cliqueChanged = false;
...

前面我已指出,贪婪算法通常需要一个或多个停止条件。 此处循环就会终止如果超过了最大迭代次数或当前 clique 的大小符合目标大小。 CliqueChanged 变量用于控制之间将节点添加和删除节点分支逻辑。 我可能已经省略此变量并使用如果。.其他 控制结构,而不是单独的 如果。.然后语句,但在此情况下,分支的控制变量的使用导致代码更易于阅读和修改,在我看来,:

if (possibleAdd.Count > 0) {
  nodeToAdd = GetNodeToAdd(graph, possibleAdd);
  Console.WriteLine("Adding node " + nodeToAdd);
  clique.Add(nodeToAdd);
  clique.Sort();
  cliqueChanged = true;
  if (clique.Count > bestSize) {
    bestSize = clique.Count;
    bestClique.Clear();
    bestClique.AddRange(clique);
  }
} // add
...

检查以确保有至少一个节点,可以添加到 clique,然后调用帮助器方法 GetNodeToAdd。 此方法将扫描 possibleAdd 列表中的节点列表并返回要添加的最佳节点 (我介绍 GetNodeToAdd 时,将显示"最佳"能够承诺解释)。 此节点被添加到当前 clique。 此时我排序 clique,因为我需要以后在算法中搜索 clique,并且如果 clique 进行排序,我可以使用快速二进制搜索而不是线性的搜索。 新 clique 检查以查看它是否更好 (大于) 当前最佳 clique。

下一步有:

if (cliqueChanged == false) {
  if (clique.Count > 0) {
    nodeToDrop = GetNodeToDrop(graph, clique, oneMissing);
    Console.WriteLine("Dropping node " + nodeToDrop);
    clique.Remove(nodeToDrop);
    clique.Sort();
    cliqueChanged = true;
  }
} // drop
...

如果当前 clique 的大小不能增加,算法将尝试从 clique 中删除一个节点。 帮助器方法 GetNodeToDrop,选择从 clique 中除去的最佳节点:

int restart = 2 * bestSize;
if (time - timeBestCliqueFound > restart &&
  time - timeLastRestart > restart) {
   Console.WriteLine("\nRestarting\n");
   timeLastRestart = time;
   int seedNode = random.Next(0, graph.NumberNodes);
   clique.Clear();
   Console.WriteLine("Adding node " + seedNode);
   clique.Add(seedNode);
} // restart
...

此时,该算法检查是否发生了缺少的进度。 重新启动变量等待重新启动之前确定时间长度。 在这里我使用值为当前最佳 clique 的两倍。 在研究上的最大 clique 值,重新启动的最佳值仍然是一个开放的问题。 此处使用的值基于我已经使用多个准则图形问题执行的试验。 如果已缺乏进展中查找新的最佳解决方案,或者它已被相对较长的时间,自上次重新启动后,将会重新启动。 如果重新启动,算法将清空当前 clique,然后选择从关系图中的所有节点的随机节点。 请注意这是一种成熟的方法 ; 如果您回头演示在运行图 2,您将看到上次重新启动领取作为第一个种子节点已使用过的初始节点:

...
possibleAdd = MakePossibleAdd(graph, clique);
    oneMissing = MakeOneMissing(graph, clique);
  } // loop
  return bestClique;
}

FindMaxClique 方法从头计算基于新 clique possibleAdd 和 oneMissing 列表。 当主处理循环终止时,该方法将返回找到最佳 clique。

添加最佳节点

有两个步骤来确定最佳的节点将添加到当前 clique。 首先,需要将增大当前 clique 的所有节点的列表。 第二,被需要一些方法来确定哪一项这些节点是最佳的节点:

static List<int> MakePossibleAdd(MyGraph graph, List<int> clique)
{
  List<int> result = new List<int>();
  for (int i = 0; i < graph.NumberNodes; ++i) {
    if (FormsALargerClique(graph, clique, i) == true)
      result.Add(i);
  }
  return result;
}

在图表中的每个节点通过扫描 MakePossibleAdd 方法。 如果该节点所检查连接到所确定的帮助器 FormsALargerClique,当前 clique 中的所有节点则所检查的节点是可能的"添加"节点,并加入结果列表。 请注意,结果可能是一个空列表,但永远不能为空:

static bool FormsALargerClique(MyGraph graph, List<int> clique, int node)
{
  for (int i = 0; i < clique.Count; ++i) {
    if (graph.AreAdjacent(clique[i], node) == false)
      return false;
  }
  return true;
}

FormsALargerClique 方法将对当前 clique 中的所有节点的一个节点进行比较。 如果候选节点不是旁边甚至 clique 中的节点之一,然后不能将候选节点添加到当前 clique。 请注意与自身进行比较的节点时,AreAdjacent 将返回 false,因为不会在当前 clique 中的节点添加到 possibleAdd 节点列表。

添加背后确定最佳的节点的主要思想是从大多数的 possibleAdd 节点列表中选择一个节点连接到 possibleAdd 集合中的所有其他节点。 一个高度连接的节点产生最大可能的新算法的下一次迭代中添加的节点的列表。

下一步有:

static int GetNodeToAdd(MyGraph graph, List<int> possibleAdd)
{
  if (possibleAdd.Count == 1)
    return possibleAdd[0];
...

GetNodeToAdd 方法假定 possibleAdd 列表具有至少一个节点。 如果有一个节点,然后的必须是最佳的节点:

int maxDegree = 0;
for (int i = 0; i < possibleAdd.Count; ++i) {
  int currNode = possibleAdd[i];
  int degreeOfCurrentNode = 0;
  for (int j = 0; j < possibleAdd.Count; ++j) {
    int otherNode = possibleAdd[j];
    if (graph.AreAdjacent(currNode, otherNode) == true)
      ++degreeOfCurrentNode;
  }
  if (degreeOfCurrentNode > maxDegree)
    maxDegree = degreeOfCurrentNode;
}
...

可能有 possibleAdd 列表中与其他人为与列表中的其他节点连接最密切相关的多个节点。 例如,假设在分析图表是图表中所示图 1 和当前 clique 已不仅仅是节点 0。 PossibleAdd 节点是 {1,3,4}。 节点 1 连接到 possibleAdd 中的节点中的两个 — — 并且,事实上,因此是节点 3 和 4。 因此进行初步的扫描,以确定可用连接的最大数量:

List<int> candidates = new List<int>();
for (int i = 0; i < possibleAdd.Count; ++i) {
  int currNode = possibleAdd[i];
  int degreeOfCurrentNode = 0;
  for (int j = 0; j < possibleAdd.Count; ++j) {
    int otherNode = possibleAdd[j];
    if (graph.AreAdjacent(currNode, otherNode) == true)
      ++degreeOfCurrentNode;
    }
    if (degreeOfCurrentNode == maxDegree)
      candidates.Add(currNode);
}
...

一旦已知的最大连接数,算法将重新扫描 possibleAdd 列表中,添加每个节点中具有列表的候选节点连接的最大数量的 possibleAdd。 请注意双扫描无法通过使用辅助数据存储区来跟踪每个 possibleAdd 节点的连接数来消除:

...
return candidates[random.Next(0, candidates.Count)];
}

此时,候选列表中有一个或多个最佳添加节点到 clique。 请注意候选必须具有至少一个计数,因为 possibleAdd 列表假定的至少一个计数。 算法会随机选择一个候选节点并将其返回。 我无法编写了候选项列表的大小准确,以及如果是这样,返回的单个节点中的候选项检查中。

正在删除的最佳节点

确定要从当前 clique 中除去的最佳节点是有点微妙。 其目的是将一个节点放在 possibleAdd 节点列表中的最大增加将导致当前 clique 中。

若要执行此操作的一种方法是,每个节点通过测试中当前 clique 实际上将其从当前 clique 中移除,然后计算所得的 possibleAdd 列表的大小。 但是还有得更有效的方法,使用连接到所有但仅有一个当前 clique 中的节点的节点的列表。 如果没有此类 oneMissing 节点的列表,可以按如下所述使用列表: 当前 clique 中每个节点进行扫描,并计算 oneMissing 列表中的节点数未连接到 clique 节点。 是最少连接的 oneMissing 列表中的节点当前 clique 中的节点是要除去的最佳节点。 拖放后此最少连接的节点,未连接到拖放节点 oneMissing 列表中的所有节点将现在完全都连接到 clique,因此成为新的 possibleAdd 节点中的其余节点。

MakeOneMissing 方法显示在图 4。 在图表中的每个节点通过扫描方法。 其目的是进行计数 clique 中的多少个节点连接到当前所检查的节点。 如果连接的节点数为总正是一个小于当前 clique,大小,则所检查的节点是 oneMissing 节点。 如果当前节点所检查的少于所需数量的邻居,short-circuits 方法。 该方法必须筛选出已被 clique,因为它们连接到他们自己 clique (即,本身) 中的所有而不是一个节点的节点。 因为当前 clique 进行排序 (每个添加或拖放) 之后,可以使用二进制搜索来代替较慢的线性搜索 (请参阅图 4)。

图 4 使列表中的 oneMissing 节点

static List<int> MakeOneMissing(MyGraph graph, List<int> clique)
{
  int count;
  List<int> result = new List<int>();
  for (int i = 0; i < graph.NumberNodes; ++i) {
    count = 0;
    if (graph.NumberNeighbors(i) < clique.Count - 1) continue;
    if (clique.BinarySearch(i) >= 0) continue;
    for (int j = 0; j < clique.Count; ++j) {
      if (graph.AreAdjacent(i, clique[j]))
        ++count;
    }
    if (count == clique.Count - 1)
      result.Add(i);
  }
  return result;
}

GetNodeToDrop 方法开始:

static int GetNodeToDrop(MyGraph graph, List<int> clique,
  List<int> oneMissing)
{
  if (clique.Count == 1)
    return clique[0];
...

此方法假定当前 clique 具有至少一个节点。 如果在当前 clique 中只有一个节点,则可以删除的唯一节点。 该方法确定最大数量的 oneMissing 列表中未连接到当前 clique 中的任何节点中,因为可能有多个节点 clique 产出也会减少断开连接到 oneMissing 列表中的节点:

int maxCount = 0;
for (int i = 0; i < clique.Count; ++i) {
  int currCliqueNode = clique[i];
  int countNotAdjacent = 0;
  for (int j = 0; j < oneMissing.Count; ++j) {
    int currOneMissingNode = oneMissing[j];
    if (graph.AreAdjacent(currCliqueNode, currOneMissingNode) == false)
      ++countNotAdjacent;
  }
  if (countNotAdjacent > maxCount)
    maxCount = countNotAdjacent;
}
...

一旦断开连接的最大数量已知的该方法重新扫描以查找那些最断开连接,并添加到候选项列表的每个节点 clique。 使用 GetNodeToAdd 方法时,如 GetNodeToDrop 无法通过维护辅助数据存储区来避免重新扫描:

List<int> candidates = new List<int>();
for (int i = 0; i < clique.Count; ++i) {
  int currCliqueNode = clique[i];
  int countNotAdjacent = 0;
  for (int j = 0; j < oneMissing.Count; ++j) {
    int currOneMissingNode = oneMissing[j];
    if (graph.AreAdjacent(currCliqueNode, currOneMissingNode) == false)
      ++countNotAdjacent;
  }
  if (countNotAdjacent == maxCount)
    candidates.Add(currCliqueNode);
}
...

通过从要除去的最佳节点列表中返回随机选择的节点完成方法 GetNodeToDrop:

...
return candidates[random.Next(0, candidates.Count)];
} // GetNodeToDrop

结论

如何有效是贪婪的算法? 不论其相对简单起见,已经证明贪婪的算法是相当有效,在很多问题的方案。 但是,可以使用多个标准,包括解决方案的速度和质量定义有效性。 我对由 DIMACS 研究机构维护的几个非常困难的准则图形问题运行此算法和算法已相当有效,迅速投入运行并出具通常是 5%的已知最好的解决方案中的最大 cliques。

测试贪婪的算法可能会比较棘手。 除了所有常用测试技术 (如单元测试,测试,等等的边界条件 — 和因为贪婪算法都有不断增长的解决方案 — 有效的测试策略是编写反复检查算法所用的数据结构的状态的帮助器方法。 例如,测试时此处提供的最大 clique 算法,我编写验证当前 clique 中没有任何重复节点的方法,请验证当前 clique 中的任何节点处于当前 possibleAdd 或当前 oneMissing 列出,依此类推。

可以产生更好的结果,以下几种方式修改了此处提供的贪婪的最大 clique 算法。 最贪婪的算法,以常见的一种方法是添加所谓 tabu 功能。 Tabu 算法维护最近使用过的数据的列表和列表可能是最近观看过的解决方案。 Tabu 列表中的数据不用来构建新的解决方案。 此外,贪婪算法通常可以使用称为平稳搜索战略。 当贪婪的算法不能提高其当前的解决方案时,平稳生成搜索新的解决方案而不向后的比如除去最大 clique 问题中的一个节点。 在以后的专栏中,我将介绍这些有趣且有用的 tabu 和稳定技术。

Dr. James McCaffrey 适用于伏信息科学 Inc.,他管理着 Microsoft 雷蒙德,Wash.,在园区工作的软件工程师的技术培训。 他参与过多项 Microsoft 产品的研发工作,包括 Internet Explorer 和 MSN Search。 Dr. McCaffrey 是作者的"。NET 测试自动化食谱"(Apress,2006年),并可以达到 jammc@microsoft.com。

多亏到下面的技术专家审阅这篇文章: Paul KochDan Liebling 王小姐 Loomis Thompson 孙 Williams