动态规划法
动态规划法将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,一般来说,子问题的重叠关系表现在对给定问题求解的递推关系称为动态规划函数中,将子问题的解求解一次并填入表中,当需要再次求解此子问题时,可以通过查表获得该子问题的解,从而避免了大量重复计算。具体的动态规划法多种多样,但都具有相同的填表形式。一般来说,动态规划法的求解过程由以下三个阶段组成:
- 划分子问题:将原问题分解为若干个子问题,每个子问题对应一个决策阶段,并且子问题之间具有重叠关系。
- 确定动态规划函数:根据子问题之间的重叠关系找到子问题满足的递推关系式即动态规划函数,这是动态规划法的关键。
- 填写表格:设计表格,以自底向上的方式计算各个子问题的解并填表,实现动态规划过程。
上述动态规划过程可以求得问题的最优值即目标函数的极值,如果要求出具体的最优解,通常在动态规划过程中记录必要的信息,再根据最优决策序列构造最优解。
多段图最短路径问题
设图 G =(V,E)是一个带权有向图,如果把顶点集合 V 划分成 k 个互不相交的子集 Vi(2<=k<=n,1<=i<=k),使得 E 中的任何一条边 <u,v>,必有 u∈Vi, v∈Vi + m(1<=i<k, 1<i+m<=k),则称图 G 为多段图,称 s∈V1 为源点,t∈Vk 为终点。多段图的最短路径问题为从源点到终点的最小代价路径。
问题分析
根据多段图的性质,我们可以将这种特殊的图结构划分为多个子集,例如如图所示的多段图就可以分成 5 个子集,在图中以 5 种不同颜色来表示。可以明显地看到想要到达某一个子集的顶点,就必须从上一个相邻顶点集的顶点出发,不相邻的子集之间不存在可达的边。
针对这个特性可以推导出解决问题的方法,例如我想要到达顶点 10,那就必须要先到达顶点 8 或者顶点 9。换句话说,到达顶点 10 的最短距离就是在到达顶点8的最短距离 d(1,8) 加上边 (8,10) 的权重,和到达顶点 9 的最短距离 d(1,9) 加上边 (9,10) 的权重中取最小值。因为不相邻的顶点集之间不存在边,所以到达顶点 10 的方式有且仅有上述 2 种。设 C 为某条边的权重,d(m,n) 为从点 m 到点 n 的最短距离,则使用数学语言的描述如下:
再看一个例子,假设要分析到达顶点 8 的最短距离,则只有 3 种情况。即到达顶点 5 的最短距离 d(1,5) 加上边 (5,8) 的权重,和到达顶点 6 的最短距离 d(1,6) 加上边 (6,8) 的权重,和到达顶点 7 的最短距离 d(1,7) 加上边 (7,8) 的权重三者之间取最小值。使用数学语言的描述如下:
根据上面 2 个例子的论述,我们可以把情况从特殊推广到一般情况,设 Cuv 为多段图有向边 <u,v> 的权值,源点 s 到终点 v 的最短路径长为 d(s,v),终点为 t,则可以得到该问题的状态转移方程为:
最优子结构证明
设一个多段图有且仅有一个起点 S,有且仅有一个终点 T,S->S1->S2->…Sn->T 为从起点 S 到终点 T 的最短路径。设 S->S1 的开销已经求出,则从起点 S 到终点 T 的最小开销的求解将转换为对点 S1 到终点 T 的最小开销进行求解。
假设 S1->S2->S3…Sn->T 不是点 S1 到终点 T 的最短路径,则必然存在另一条路径 S1->R1->R2…Rn->T 的开销小于 S1->S2->S3…Sn->T 的路径开销,进而推出起点 S 到终点 T 的最短路径为 S->S1->R1->R2…Rn->T。然而已知路径 S->S1->S2->…Sn->T 为起点 S 到终点 T 的最短路径,不可能存在其他路径的总开销比该路径的开销还要小,产生了矛盾,因此多段图的最短路径问题满足最优子结构。
问题求解
例如上述例子中的多段图,可以建立图的邻接矩阵 topography.edges[MAXV][MAXV] 为:
需要一个辅助结构:一维数组 cost[MAXV] 存储到每个顶点的最小开销。例如我们已经求出了到达顶点 1~9 的最小开销如下:
则根据状态转移方程,可以得出从顶点 1 到顶点 10 的最小开销为:
如果我们要找出最短路径,还需要一个一维数组 path[MAXV],用于保存每个顶点的前驱顶点。
找到最短路径的方式为从 path[10] 开始依次访问对应的顶点,访问的次序即为所求的最短路径,直到访问了起点为结束。
程序编写
首先定义图结构的结构体,使用邻接矩阵来存储:
typedef struct //图的定义
{
int edges[MAXV][MAXV]; //邻接矩阵
int n; //顶点数
} MGraph;
定义求解问题需要的辅助结构:
MGraph topography; //保存城市关系的邻接矩阵
int path[MAXV] = {}; //保存到该顶点的最短路径对应的前驱
int min_cost[MAXV] = {}; //保存到每个顶点的最短路径长
要根据测试样例数据建立多段图的邻接矩阵:
MGraph CreateMGraph(int num) //建图
{
MGraph topography;
int n;
int point1, point2;
int value;
//初始化边为不存在
for(int i = 1; i <= num; i++)
{
for(int j = 1; j <= num; j++)
{
topography.edges[i][j] = 0;
}
}
cout << "请输入边数:";
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> point1 >> point2 >> value;
topography.edges[point1][point2] = value;
}
cout << "
建立的邻接矩阵为:" << endl;
for(int i = 1; i <= num; i++)
{
for(int j = 1; j <= num; j++)
{
printf("%2d ",topography.edges[i][j]);
}
cout << endl;
}
cout << endl;
topography.n = num;
return topography;
}
根据状态转移方程求解问题,最后输出最短路径及其开销:
int main()
{
int cities_num = 0; //城市数量
int a_cost; //当前路径的开销
int pre;
cout << "城市数量为:";
cin >> cities_num;
//建图
topography = CreateMGraph(cities_num);
//初始化路径开销
min_cost[1] = 0;
for(int i = 2; i <= topography.n; i++)
{
min_cost[i] = 99999;
}
//依次计算到达所有点的最短路径
for(int i = 2; i <= cities_num; i++)
{
//遍历之前的所有点,计算到达该点的最短路径
for(int j = 1; j < i; j++)
{
if(topography.edges[j][i] != 0) //若路径存在
{
a_cost = min_cost[j] + topography.edges[j][i];
if(a_cost < min_cost[i]) //更新最短路径长
{
min_cost[i] = a_cost;
path[i] = j; //记录前驱顶点
}
}
}
}
//输出到所有顶点的最短路径
for(int i = 1; i <= cities_num; i++)
{
cout << "到顶点" << i << "的最小开销为:" << min_cost[i] << ",路径:" << i;
pre = i;
while(path[pre])
{
cout << "<-" << path[pre];
pre = path[pre];
}
cout << endl;
}
return 0;
}
测试样例
样例一
输入数据
10
18
1 2 4
1 3 2
1 4 3
2 5 9
2 6 8
3 5 6
3 6 7
3 7 8
4 6 4
4 7 7
5 8 5
5 9 6
6 8 8
6 9 6
7 8 6
7 9 5
8 10 7
9 10 3
输出数据
样例二
输入数据
16
30
1 2 5
1 3 3
2 4 1
2 5 3
2 6 6
3 5 8
3 6 7
3 7 6
4 8 6
4 9 8
5 8 3
5 9 5
6 9 3
6 10 3
7 9 8
7 10 4
8 11 2
8 12 2
9 12 1
9 13 2
10 12 3
10 13 3
11 14 3
11 15 5
12 14 5
12 15 2
13 14 6
13 15 6
14 16 4
15 16 3
输出数据
样例三
输入数据
12
21
1 2 7
1 3 6
1 4 4
1 5 2
2 6 4
2 7 2
2 8 1
3 6 2
3 7 7
4 8 11
5 7 11
5 8 8
6 9 6
6 10 5
7 9 4
7 10 3
8 10 5
8 11 6
9 12 4
10 12 2
11 12 5
输出数据
算法分析
算法的时间复杂度主要由两部分组成:第一部分是依次计算从源点到各个顶点的最短路径长度,由两层嵌套的循环组成,外层循环执行 n-1 次,内层循环对所有入边进行计算,并且在所有循环中,每条入边只计算一次。假定图的边数为 m,则时间性能是 O(m)。第二部分是输出最短路径经过的顶点,设多段图划分为 k 段,其时间性能是 O(k)。综上所述,时间复杂度为 O(m+k)。
参考资料
《算法设计与分析(第二版)》——王红梅,胡明 编著,清华大学出版社