图论算法之Dijkstra算法【寻找无负权值有向图中一点到其余点的最短路径 C#实现】
Dijkstra算法是图论算法中十分重要的一种算法,其算法思想可以理解为以下两个步骤:
摸着石头过河 :
探索一次后更新一次,不断探索,直到探索完所有点,就可以得到起点到所有其他点的最短路径方案,这个方案是通过前驱节点来描述的。
向量的加法原则 :
如果发现 AB + BC 的模值小于 AC 的模,虽然都是从 A 到 C ,但是走得距离更短,则认为 B 可以作为跳板,就像城市AC之间只有绿皮火车,AB和BC之间是高铁,那么显然 A 通过跳板 B 到达 C 是更好的选择。
算法需要用:S{} 、U{} 、 Distance{} 、 Path{} 四个存储器。
S{} 用来存储可作为跳板的中间节点(前驱节点)
U{} 用来存储暂时不能作为跳板的节点(前驱节点)
Distance{} 用来存储起点到其余点的最短距离
Path{} 用来存储当前认为的到达各个节点的最短路径的前驱节点(A点前驱节点就是路径中到达A点的上一个节点)。
算法的流程如下:
1、初始化S{} 、U{} 、 Distance{} 、 Path{} ,将起点加入 S{} 并将该点作为第一个跳板点,将其余的点放入 U{} ,目前由于没有开始探索,认为到达任意一点的距离为无穷大,即是在 Distance{} 中记录起点到达每一个点的距离为无穷,
目前没有路径方案,也不知道到达各个点上一个节点是什么,因此在 Path{} 全部放入 -1(-1表示还没找打到达该点的方案)。
2、探索 S{} 中点到其余点的距离,并记录到 Distance{} 中,并找出 Distance{} 中最短的一个点加入到 **S{}**集合中,之所以要把最短的这个点加入到 {S} 集合中,是因为这样才有最大的可能通过这个点作为中间点减少到其他点的距离,同时,在 U{} 中移除最短距离点,在 {S} 中加入跳板点可以避免重复探索,。
此外,如果说刚刚使用的 S{} 中的这个跳板点后比之前到达其中一个点的距离缩短了,就将这个点加入到 Path{} 中作为其前驱节点。
3、重复第二步骤,这个过程就是不断更新前驱节点(跳板节点),有点类似于贪心算法,只要遇到更好的前驱节点就替换原来的前驱节点,这样最后当 U{} 中的点全部移除后,就可以在 Path{} 中得到起点到其余所有点的最短路径的前驱节点,通过这每个节点的前驱节点就可以知道完整的路径:
可以用下面这个过程来表示:
A 先去问 F : 我怎么到 F 最短 ; F 说:完整的路径我记不住,我只记得你通过 E 到我这最短;
A又去问 E :我怎么到 E 最短 ;E 说:完整的路径我记不住,我只记得你通过 C 到我这最短;
A 先去问 C : 我怎么到 C 最短 ; C 说:完整的路径我记不住,我只记得你通过 D 到我这最短;
A又去问 D :我怎么到 D 最短 ;D 说:完整的路径我记不住,我只记得你通过 B 到我这最短;
A又去问 B :我怎么到 B 最短 ;B 说:你直接过来就是最短的;
我的语言表达能力太差,下面直接上代码:这样应该更好理解:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Dijkstra GetPath = new Dijkstra();
string ExcelPath = @"test.xlsx";
double[,] pathdata = OpenExcel.GetDataFormExcel(ExcelPath);
ShowData(pathdata);
GetPath.GetAdjacentMat(pathdata,7);//第二个参数为节点的数目
Hashtable path = GetPath.GetPath(0,7);//第一个参数为起点、第二个参数为节点数目
Console.Read();
}
#region 显示输入的数据
static void ShowData(double[,] pathdata)
{
int a = 0;
Console.WriteLine("输入的数据为:".PadRight(20));
foreach (double item in pathdata)
{
a++;
MyConsole.Write(item.ToString(),10,' ');
if (a == 4)
{
Console.WriteLine();
a = 0;
}
}
Console.WriteLine();
}
#endregion
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
class Dijkstra
{
#region 参考资料
/// <summary>
/// https://haokan.baidu.com/v?vid=12876411808292764250&pd=bjh&fr=bjhauthor&type=video 参考视频
/// </summary>
#endregion
#region 所有参数说明
public int StartNode;
public List<int> S = new List<int>(); //源节点
public List<int> U = new List<int>(); //非源节点
public Hashtable Dist = new Hashtable();// 起点到各个节点的距离
public Hashtable Path = new Hashtable();//前驱节点
private double[,] AdjacentMat; //邻接矩阵
private int NodeNumber;//节点的总数
public int CurrentDriveNode;//当前驱动节点(中间节点)
public double P_Length = 0;//起点到中间节点的距离
int RefreshTimes = 0;//更新的次数
#endregion
#region 观察参数
private void ShowU()
{
Console.WriteLine("非源节点集合【U】:".PadRight(20));
foreach (int item in U)
{
Console.Write(item.ToString() + " ");
}
Console.WriteLine();
}
private void ShowS()
{
Console.WriteLine("源节点集合【S】:".PadRight(20));
foreach (int item in S)
{
Console.Write(item.ToString() + " ");
}
Console.WriteLine();
}
private void ShowAdjacentMat()
{
int num = 0;
Console.WriteLine("AdjacentMat邻接矩阵:".PadRight(20));
foreach (double item in AdjacentMat)
{
MyConsole.Write(item.ToString(), 10, ' ');
num++;
if (num == NodeNumber)
{
num = 0;
Console.WriteLine();
}
}
Console.WriteLine();
}
private void ShowDist()
{
Console.WriteLine("起点到各个点的距离 Dist:".PadRight(20));
foreach (object item in Dist.Keys)
{
MyConsole.Write(((int)item).ToString(), 10, ' ');
}
Console.WriteLine();
foreach (object item in Dist.Values)
{
MyConsole.Write(((double)item).ToString(), 10, ' ');
}
Console.WriteLine();
}
private void ShowPath()
{
Console.WriteLine("起点到各个点的路径方案 Path:".PadRight(20));
foreach (object item in Path.Keys)
{
MyConsole.Write(((int)item).ToString(),10,' ');
}
Console.WriteLine();
foreach (object item in Path.Values)
{
MyConsole.Write(((int)item).ToString(), 10, ' ');
}
Console.WriteLine();
}
private void ShowWhole()
{
Console.WriteLine("===============第 " + RefreshTimes++.ToString() + " 次更新===============");
ShowS();
ShowU();
ShowDist();
ShowPath();
Console.WriteLine();
Console.WriteLine();
}
#endregion
#region 获取邻接矩阵
/// <summary>
/// 为邻接矩阵赋值
/// </summary>
/// <param name="Amat">
/// 是一个N行4列的二维数组
/// 第0列是路段编号【不必须有】
/// 第1列是起点
/// 第2列是终点
/// 第3列是有向距离
/// </param>
/// <param name="NodeNum"></param>
public void GetAdjacentMat(double[,] Amat, int NodeNum)
{
NodeNumber = NodeNum;
AdjacentMat = new double[NodeNumber, NodeNumber];
for (int i = 0; i < NodeNum; i++) //不连通的距离为无穷大
for (int j = 0; j < NodeNum; j++)
{
if (i == j)
AdjacentMat[i, j] = 0;
else
AdjacentMat[i, j] = double.PositiveInfinity;
}
for (int row = 0; row < Amat.GetLength(0); row++)
{
AdjacentMat[(int)Amat[row, 1], (int)Amat[row, 2]] = Amat[row, 3];
}
ShowAdjacentMat();
}
#endregion
#region 参数初始化
private void Init()
{
S.Add(StartNode);
for (int a = 0; a < NodeNumber; a++)
{
Dist.Add(a, double.PositiveInfinity);
Path.Add(a, (int)-1);
if (a == StartNode)
continue;
else
U.Add(a);
}
Dist[StartNode] = (double)0;
Path[StartNode] = StartNode;
CurrentDriveNode = S.Last();
ShowWhole();
}
#endregion
#region 更新状态
private void RefreshState()
{
Refresh_Dist_Path();
Refresh_S_U();
}
private void Refresh_Dist_Path()
{
foreach (int item in U)
{
double newLength = AdjacentMat[CurrentDriveNode, item] + P_Length;
if (newLength < (double)Dist[item]) //如果经过中间节点的距离更短则更新 最短距离 和 路径
{
Dist[item] = newLength; //更新最短距离
Path[item] = CurrentDriveNode;//更新前驱节点
}
}
}
private void Refresh_S_U()
{
CurrentDriveNode = U.Last();
foreach (int item in U)
{
if ((double)Dist[item] < (double)Dist[CurrentDriveNode])
{
CurrentDriveNode = item;
}
}
U.Remove(CurrentDriveNode);
S.Add(CurrentDriveNode);
P_Length = (double)Dist[CurrentDriveNode];
}
#endregion
#region Dijkstra主函数
public Hashtable GetPath(int StartN,int NodeNum)
{
StartNode = StartN;//为全局的起点赋值
NodeNumber = NodeNum;//为全局的节点数目赋值
Init();
while (U.Count > 0)
{
RefreshState();
ShowWhole();
}
return Path;
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
class MyConsole
{
public static void Write(string strOriginal,int TrueLength, char chrPad)
{
string str = PadRightTrueLen(strOriginal, ref TrueLength, chrPad);
Console.Write(str);
}
public static void WriteLine(string strOriginal, int TrueLength, char chrPad)
{
string str = PadRightTrueLen(strOriginal, ref TrueLength, chrPad);
Console.WriteLine(str);
}
/// <summary>
/// 获取真实长度
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static int TrueLength(string str)
{
int lenTotal = 0;
int n = str.Length;
string strWord = ""; //清空字符串
int asc;
for (int i = 0; i < n; i++)
{
strWord = str.Substring(i, 1);
asc = Convert.ToChar(strWord);
if (asc < 0 || asc > 127) // 在0~127间字符长度加1,否则加2
{
lenTotal = lenTotal + 2;
}
else
{
lenTotal = lenTotal + 1;
}
}
return lenTotal;
}
/// <summary>
/// 得到正确的格式
/// </summary>
/// <param name="strOriginal"></param>
/// <param name="maxTrueLength"></param>
/// <param name="chrPad"></param>
/// <returns></returns>
private static string PadRightTrueLen(string strOriginal, ref int maxTrueLength, char chrPad)
{
string strNew = strOriginal;
if (strOriginal == null || maxTrueLength <= 0)
{
strNew = "";
return strNew;
}
int trueLen = TrueLength(strOriginal);
if (trueLen > maxTrueLength) // 如果字符串大于规定长度 将规定长度等于字符串长度
{
for (int i = 0; i < trueLen - maxTrueLength; i++)
{
maxTrueLength += chrPad.ToString().Length;
}
}
else// 填充 小于规定长度 用‘ ’追加,直至等于规定长度
{
for (int i = 0; i < maxTrueLength - trueLen; i++)
{
strNew += chrPad.ToString();
}
}
return strNew;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using System.Data.OleDb;
namespace Test
{
class OpenExcel
{
public static double[,] ExcelData;
public static double[,] GetDataFormExcel(string filepath)
{
string path = filepath;//获取打开文件的路径
string strCon = @"Provider=Microsoft.Ace.OleDb.12.0;Data Source =" + path + ";" + "Extended Properties='Excel 8.0;HDR=NO;IMEX=1;'";
OleDbConnection myConn = new OleDbConnection(strCon);
string strCom = " select * from [Sheet1$] ";
OleDbDataAdapter myCommand = new OleDbDataAdapter(strCom, myConn);
DataSet myDataSet = new DataSet();
myCommand.Fill(myDataSet);
ExcelData = new double[myDataSet.Tables[0].Rows.Count, myDataSet.Tables[0].Columns.Count];
for (int i = 0; i < myDataSet.Tables[0].Rows.Count; i++)
{
for (int j = 0; j < myDataSet.Tables[0].Columns.Count; j++)
{
ExcelData[i, j] = Convert.ToDouble(myDataSet.Tables[0].Rows[i][j]);
}
}
return ExcelData;
}
}
}