单源最短路径问题
问题描述:
有n个节点,其中源点为s,现在要求从源点出发到其他各个节点的最短路径问题。
问题分析:
单源点最短路径问题是一个非常经典的问题,该问题的最好求解方法是Dijkstra算法(一种经典的贪心算法)。本题使用Dijkstra算法来求解的效率是非常的高的。但是因为我们要从回溯法进行优化,所以我们这里使用分支限界法,还得慢慢来~~
与回溯法不同的是我们这里采用广度优先策略遍历解空间树,分支限界法会需要一个活结点表来存储可能成为扩展节点的节点,这里我们可以采用堆存储(最小堆、最大堆),也可以采用队列存储,然后每次选择节点扩展时,从活结点表中弹出满足要求的节点,一次性生成它的所有儿子节点,并将其儿子节点加入活结点表中。
两种策略:
FIFO策略(先进先出策略)
即按照一般的广度遍历顺序对问题的解空间树进行遍历求解,非常常规,不做赘述。
优先度队列策略
即按照我们设置的优先级去对活结点表中的节点进行排序,我们每次选择节点去扩展时都选择队头节点去进行扩展,这样能保证问题求解效率的优越性,这里有些类似于贪心算法的贪心策略,但是也有不同点就是贪心算法在每个阶段做出选择之后都不会回头,但是分支限界法虽然在每个阶段做出的决策都有可能只是暂时最优的决策,在下一阶段也许会换条路走。
抽象点说,贪心算法是一条路走到黑;分支限界法是可以换条路走,但不能回头;回溯法是可以回头换条路走。
问题解决:
本题的解决方法便是优先度队列策略了。
数据结构:
我们选择的优先度为当前最短的路径,这样选是有理由的,因为我们是根据当前的选择去更新我们的活结点表中的数据,最后我们得到的便是正确的结果,我们要求解的是从源点s出发到其余各个节点的最短距离,每一步我们从活结点表中选择去扩展的都是当前从s出发以后经过路径最短的节点,如果当前扩展节点是k点,那么当前求得的必定是s节点到k节点的最短路径,因为此时k节点所在路径是所有路径中最短的,不可能再存在另一条路径使得s到k的距离更小,因为k节点还未被扩展。所以当前算法每个节点被扩展时,该节点的最优解就被求出。
接下来就是我们的活结点表了,表中的每个Node节点属性有当前索引,当前路径长度以及当前的父节点,path用来记录当前阶段各个节点的最优路径长度,为我们的扩展节点选择提供优先度的评判标准,parentIndex则是用来记录当前状态下到各个节点的路径选择。
class Node {
public:
int index;
int path;
int parentIndex;
};
边长矩阵:
这里我们的∞用10000代替:
输入:
10000 10 10000 30 100 10000 10000 50 10000 10000 10000 10000 10000 10000 10 10000 10000 20 10000 60 10000 10000 10000 10000 10000
算法:
在开始阶段,我们将活结点表先初始化,源点s直接可达的节点我们直接将路径长度赋值到活结点表中。然后比较活结点表中的数据,根据优先度标准,我们选择当前路径最小的活结点为扩展节点,将它所有的子节点信息更新入活结点表中。步骤以此类推。。。
剪枝策略:
为了提高分支限界法求解问题的效率,我们需要合适的剪枝函数,本题我们选择的剪枝函数是,当前扩展节点有儿子节点v,v节点在活结点表内,如果v.path>=bestPath,说明v节点继续扩展下去的结果不会比当前最优解更好了,那么不扩展p节点;反之v.path<bestPath,说明v节点求得的解比当前的最优解更好,那么将v节点加入到活结点表中。
该剪枝策略大大提高了本题的求解效率,因为本题求解的关键就在于活结点表的变化,该剪枝函数省去了不必要添加进活结点表中的节点,大大减轻了工作量。
啥也不多说了,上代码。
代码:
#include <iostream>
#include <vector>
#define MAX_NUM 10000
using namespace std;
class Node {
public:
int index;
int path;
int parentIndex;
};
int n;//节点的个数
vector<Node> nodeListAlive;//活结点表
vector<Node> nodeList;//结果表
vector<vector<int>> matrix;//边长矩阵
Node copyNode(Node node) {
Node newNode;
newNode.index = node.index;
newNode.parentIndex = node.parentIndex;
newNode.path = node.path;
return newNode;
}
bool cut(Node child) {
//首先在结果表中查找当前最优结果
int index = child.index;
if (child.path >= nodeList[index].path) {
return false;
}
return true;
}
//将活结点队列排序
void sortNodeListAlive() {
for (int i = (nodeListAlive.size() - 2) / 2; i >= 0; i--) {
Node leftSon = nodeListAlive[2 * i + 1];
//首先比较左儿子和当前节点
if (leftSon.path < nodeListAlive[i].path) {
//交换左儿子节点和当前节点的位置
Node temp = leftSon;
leftSon = nodeListAlive[i];
nodeListAlive[i] = temp;
}
//判断最后一个节点是否是右儿子
if ((2 * i + 2) <= nodeListAlive.size() - 1) {
Node rightSon = nodeListAlive[2 * i + 2];
//再比较此时的节点与右儿子的大小
if (rightSon.path < nodeListAlive[i].path) {
//交换左儿子节点和当前节点的位置
Node temp = rightSon;
rightSon = nodeListAlive[i];
nodeListAlive[i] = temp;
}
}
}
}
void extendNode(Node node) {
int index = node.index;
//查找扩展节点n的孩子节点
for (int i = 0; i < n; i++) {
int path = matrix[index][i];
if (path != MAX_NUM) {
Node childNode;
childNode.path = node.path + path;
childNode.index = i;
childNode.parentIndex = index;
//如果满足不剪枝条件,返回true,将孩子节点放入活结点表,否则不放入活结点表
if (cut(childNode)) {
//如果活结点表中有该节点的重复节点,删除重复节点
for (int i = 0; i < nodeListAlive.size(); i++) {
if (nodeListAlive[i].index == childNode.index) {
nodeListAlive.erase(nodeListAlive.begin() + i);
}
}
//再将该节点插入活结点表
nodeListAlive.push_back(copyNode(childNode));
//将当前结果更新入结果表中
for (int i = 0; i < nodeList.size(); i++) {
if (childNode.index == i) {
nodeList[i] = copyNode(childNode);
}
}
}
}
}
//删除队头的元素
if (nodeListAlive.size() > 0) {
nodeListAlive.erase(nodeListAlive.begin());
}
//将活结点队列排序
if (nodeListAlive.size() > 1) {
sortNodeListAlive();
}
if (nodeListAlive.size() != 0) {
extendNode(nodeListAlive[0]);
}
}
int main() {
//输入节点的个数
cout << "请输入节点的个数:" << endl;
cin >> n;
//初始化结果表
for (int i = 0; i < n; i++) {
Node node;
node.index = i;
node.path = MAX_NUM;
nodeList.push_back(node);
}
//初始化活结点表
Node s;
s.index = 0;
s.parentIndex = -1;
s.path = 0;
nodeListAlive.push_back(s);
for (int i = 0; i < n; i++) {
vector<int> m;
matrix.push_back(m);
for (int j = 0; j < n; j++) {
cout << "请输入第:" << i << " 行,第:" << j << " 列个节点" << endl;
int path;
cin >> path;
matrix[i].push_back(path);
}
}
//扩展节点函数
extendNode(s);
for (int i = 1; i < n; i++) {
switch (i)
{
case 1:
cout << "a的最优路径为:" << nodeList[i].path << endl;
break;
case 2:
cout << "b的最优路径为:" << nodeList[i].path << endl;
break;
case 3:
cout << "c的最优路径为:" << nodeList[i].path << endl;
break;
case 4:
cout << "d的最优路径为:" << nodeList[i].path << endl;
break;
}
}
}
运行结果:
a的最优路径为:10
b的最优路径为:50
c的最优路径为:30
d的最优路径为:60
总结:
其实本题最好的办法是用Dijsktra算法去求解而不是分支限界法,但是分支限界法解决此问题又可以看成是复杂版的Dijkstra算法,差别就在于分支限界法虽然相较于回溯法已经优化了很多,但是仍然具有穷举的特性,而不是像Dijsktra算法在经过一系列处理之后再去做出下一步决断,Dijsktra算法会在每一步决断之后更新自己的节点信息表,将已经求解过的节点排除在外,而未求解的节点,在经过贪心决策之后求得的必然是它的最优解。
分支限界法复杂就复杂在它没有将执行选择策略贯彻到底,它先选择了,然后就穷举了,把儿子节点直接塞到活结点表里,如果它先判断一下,俩儿子是否已经在活结点表里,较表里的较优则代替表里的,反之则不加到表里,这也就是剪枝函数干的事情了,这样,也就趋近于了Dijsktra算法。毕竟Dijsktra算法就是在做出每一步的贪心选择后,择优更新节点信息表。