算法导论实践——旅行商问题
这个算法感觉也很巧妙。抓住代价矩阵的性质便能够进行剪枝操作,从而大大减少搜索次数。你就感觉很神奇
理论
这部分按照上课的PPT给出。
- 首先定义旅行商问题
简单的说,我们有一张地图,地图上有很多景点,景点与景点之间有不同路程的小路。我们的任务就是如何 能够不走回头路就能够把所有景点逛完且走的路最少。 - 代价矩阵的性质
简单的理解,我们给出一个3*3的地图。C[i,j]表示从点i到点j的代价
按行减,表明点i到其他所有点的距离同时减少,不会影响最优解的选择;
按列减,表明点j到其他所有点的距离同时减少,不会影响最优解的选择; - 分支界限
由代价矩阵的变换给出代价矩阵下界的定义。当代价矩阵的每一行、每一列均含有至少一个0时,减的数的总和就是下界值。因为代价不能为负。 - 爬山法
一种贪心选择办法。以下图为例
爬山法选取局部最优解。运用爬山法,我们对上图进行遍历,得到:
第一次:0 -> 2 -> 1 -> 3 -> 4(红色)
第二次:1 -> 4 -> 3(蓝色)
第三次:0 -> 71(绿色) - 递归定义问题
-
分解:用路径[i,j]划分左右子树的根节点。左子树为包含路径[i,j],右子树为不包含路径[i,j]。[i,j] 的选取很讲究。首先,我们按照代价矩阵的性质,将代价矩阵转变成每一列、每一行均含有至少一个0的矩阵。接着,由于我们希望用到爬山法的策略,因此我们寻找转变后的代价矩阵中的所有0点位置,并将依据这些点计算右子树的下界。
假如C[i,j] = 0,则其右子树不包含路径[i,j],因此,我们将右子树的C[i,j]置为正无穷。再对其运用矩阵转换,重新求得矩阵下界。如此往复,我们需要找到所有0点中的最大下界。这样的目的是保证左右子树的代价差尽可能大,从而达到剪枝的作用,可以在代码中体会。 -
解决:选择路径[i,j]后,对于左子树。划去其i行,j列,表示我们的要求的总路径中不包含从i出的边和从入j的边了。然后置[j,i]为无穷大,表明不走回头路;对于右子树,置[i,j]为无穷大即可,因为总的路径中并不包含[i,j]。
-
合并:不需要合并。查找的路上需要维护一个路径栈,和一个记录分支下界的数组。
废话说了一大堆,直接实践。
C#实践
给出测试样例
应该的结果
TravelProblem.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace SearchStrategies
{
public static class TravelProblem
{
/*演算:旅行商问题
* 矩阵C代表代价矩阵。Cij代表从点i到点j所要花费的代价。
* 这里,我们需要在图中找一个Hamilton环,使之花费代价最小。
*
* 矩阵变换:
* 我们有在代价矩阵每行每列减去同一个数,不影响优化解的求解。在旅行商问题中,
* 相当于是说把一个结点到其他所有节点的路径都缩短相同的值,因此不影响最小代价求解。
*
* 代价下界:
* 实行矩阵变换后,减去的数之和
*
* 划分:
* 找到使有该条边和没该条边的代价下界(L.B)相差最大的边[i,j],并以有无为标准
* 划分为左右子树。
*
*
* 左子树:
* 有该条边[i,j],则去掉第i行、第j列,因为不可能再有边是出点i,入点j了。同时,去掉
* [j,i],因为Hamilton环不走回头路,一笔画。实行矩阵变换,更新L.B,继续递归划分。
*
* 右子树:
* (没有该条边[i,j],则对于Hamilton环必存在出点i的边和入点j的边。要使代价最小,我们
* 需要找到min{[i,s1]},其中s1∈{点集}且s1≠j,还需要找到min{s2,j},其中s2∈{点集}
* 且s2≠i,依据min{[i,s1]}和min{s2,j},可以计算代价,为了统一,我们采用接下来的方法)
* 没有该条边[i,j],则去掉边[i,j],实行矩阵变换,更新L.B,继续递归划分
*
* 爬山法:
* 每次选择最小的L.B,不断更新分支下界,以达到剪枝的目的。
*
*/
private static int dimension = 7;
private static List<Node> pathRecord = new List<Node>();
private static List<int> pathCost = new List<int>();
private static bool isInit = true;
private static int initLB = 0;
/// <summary>
/// 分支界限
/// </summary>
private static int cutBoundary = int.MaxValue / 2;
public static void SolveTravelProblem(int[,] C)
{
if (CheckPath())
{
int cost = pathRecord[pathRecord.Count - 1].LB;
pathCost.Add(cost);
cutBoundary = pathCost.Min();
GetResult();
}
else
{
Node node;
if (isInit)
{
initLB = MatrixTransform(C);
isInit = false;
node = Partion(C);
node.LB = initLB;
pathRecord.Add(node);
SolveTravelProblem(C);
}
else
{
node = Partion(C);
if(node != null)
{
Console.WriteLine("孩子为:[" + node.i + "," + node.j + "]");
//左子树
int[,] includeIJ = CopyMatrix(C);
for (int j = 1; j <= dimension; j++)
{
includeIJ[node.i, j] = int.MaxValue / 2;
}
for (int i = 1; i <= dimension; i++)
{
includeIJ[i, node.j] = int.MaxValue / 2;
}
includeIJ[node.i, node.j] = 0;
includeIJ[node.j, node.i] = int.MaxValue / 2;
int LBIncreasement = MatrixTransform(includeIJ);
//增加这个点到路径中
node.exist = true;
node.LB = pathRecord.Last().LB + LBIncreasement;
pathRecord.Add(node);
if (cutBoundary > pathRecord.Last().LB)
{
Console.Write("[" +
node.i + "," + node.j + "," + node.exist + "]的左");
//Console.WriteLine("包含路径[" + node.i + "," + node.j + "]" + "的结点:" + node.LB);
SolveTravelProblem(includeIJ); //递归求解
}
pathRecord.Remove(pathRecord.Last()); //从路径上移除最后一个结点
//右子树
int[,] notIncludeIJ = CopyMatrix(C);
notIncludeIJ[node.i, node.j] = int.MaxValue / 2;
LBIncreasement = MatrixTransform(notIncludeIJ);
node.exist = false;
node.LB = pathRecord.Last().LB + LBIncreasement;
pathRecord.Add(node);
if (cutBoundary > pathRecord.Last().LB)
{
Console.Write("[" +
node.i + "," + node.j +","+ node.exist +"]的右");
SolveTravelProblem(notIncludeIJ); //递归求解
}
pathRecord.Remove(pathRecord.Last()); //从路径上移除最后一个结点
//Console.WriteLine("当前查看不存在的结点[" + node.i + "," + node.j + "]");
}
else
{
Console.WriteLine("孩子:无");
}
}
}
}
private static Node Partion(int[,] C)
{
//找最大差值,方便剪枝
int max = int.MinValue / 2;
Node node = null;
for(int i = 1; i <= dimension; i++)
{
for(int j = 1; j <= dimension; j++)
{
if(C[i,j] == 0)
{
//右子树
int[,] notIncludeIJ = CopyMatrix(C);
notIncludeIJ[i, j] = int.MaxValue / 2;
int increasement = MatrixTransform(notIncludeIJ);
if(increasement > max)
{
max = increasement;
node = new Node
{
i = i,
j = j
};
}
}
}
}
return node;
}
/// <summary>
/// 矩阵变换
/// </summary>
/// <param name="C">代价矩阵</param>
/// <returns>需要减去的L.B的值</returns>
private static int MatrixTransform(int[,] C)
{
//在矩阵变换时不考虑已经考虑过的点
for (int i = 1; i < pathRecord.Count; i++)
{
C[pathRecord[i].i, pathRecord[i].j] = 0;
}
int LBIncreasement = 0;
//检查行
for(int i = 1; i <= dimension; i++)
{
bool zeroFlag = false;
for(int j = 1; j <= dimension; j++)
{
if(C[i,j] == 0)
{
zeroFlag = true;
}
}
if (!zeroFlag)
{
int min = int.MaxValue / 2;
for(int j = 1; j <= dimension; j++)
{
if(C[i,j] < min)
{
min = C[i, j];
}
}
for(int j = 1; j <= dimension; j++)
{
C[i, j] -= min;
}
LBIncreasement += min;
}
}
for (int j = 1; j <= dimension; j++)
{
bool zeroFlag = false;
for (int i = 1; i <= dimension; i++)
{
if (C[i, j] == 0)
{
zeroFlag = true;
}
}
if (!zeroFlag)
{
int min = int.MaxValue / 2;
for (int i = 1; i <= dimension; i++)
{
if (C[i, j] < min)
{
min = C[i, j];
}
}
for (int i = 1; i <= dimension; i++)
{
C[i, j] -= min;
}
LBIncreasement += min;
}
}
//矩阵变换完毕后,将路径上的点变成正无穷,表明下一次我们不会用到他们
for (int i = 1; i < pathRecord.Count; i++)
{
C[pathRecord[i].i, pathRecord[i].j] = int.MaxValue / 2;
}
return LBIncreasement;
}
private static bool CheckPath()
{
List<int> iArray = new List<int>();
List<int> jArray = new List<int>();
List<bool> existArray = new List<bool>();
for(int i = 0;i < pathRecord.Count; i++)
{
iArray.Add(pathRecord[i].i);
jArray.Add(pathRecord[i].j);
}
for(int i = 1;i <= dimension; i++)
{
if (!iArray.Contains(i) || !jArray.Contains(i))
return false;
if(pathRecord[i].exist == true)
existArray.Add(pathRecord[i].exist);
}
if (existArray.Count != dimension)
return false;
return true;
}
/// <summary>
/// 生成随机矩阵
/// </summary>
/// <param name="n">矩阵维度</param>
/// <returns></returns>
public static int[,] RandomCostMatrix(int n)
{
int[,] C = new int[n + 1,n + 1];
dimension = n;
for (int i = 1; i<= n; i++)
{
for(int j = 1;j <= n; j++)
{
C[i, j] = i == j ? int.MaxValue / 2 : new Random().Next(0, int.MaxValue / 4);
Console.Write(C[i, j]);
Console.Write(" ");
}
Console.WriteLine(" ");
}
return C;
}
private static int[,] CopyMatrix(int[,] from)
{
int[,] to = new int[dimension + 1, dimension + 1];
for(int i = 1; i <= dimension; i++)
{
for(int j = 1; j <= dimension; j++)
{
to[i, j] = from[i, j];
}
}
return to;
}
private static void GetResult()
{
Console.WriteLine("当前路径为:");
for(int i = 1; i < pathRecord.Count; i++)
{
Console.Write("[");
Console.Write(pathRecord[i].i);
Console.Write(",");
Console.Write(pathRecord[i].j);
Console.Write(",");
Console.Write(pathRecord[i].exist);
Console.Write("]");
}
Console.WriteLine(" ");
}
}
public class Node
{
public int i;
public int j;
public bool exist; //是否包含边[i,j]
public int LB;
}
}
Program.cs
using System;
namespace SearchStrategies
{
class Program
{
static void Main(string[] args)
{
int[,] C =
{
{0,0,0,0,0,0,0,0},
{0,int.MaxValue,3,93,13,33,9,57 },
{0,4,int.MaxValue,77,42,21,16,34 },
{0,45,17,int.MaxValue,36,16,28,25 },
{0,39,90,80,int.MaxValue,56,7,91 },
{0,28,46,88,33,int.MaxValue,25,57 },
{0,3,88,18,46,92,int.MaxValue,7 },
{0,44,26,33,27,84,39,int.MaxValue }
};
TravelProblem.SolveTravelProblem(C);
}
}
}
结尾
可能有BUG(手动滑稽,不过今天就暂时这样吧)