动态规划问题- 机器人行走最短路径
动态规划问题中,求最短路径是一类经典的动态规划问题,可以利用动态规划的思路进行有效求解。最初问题来源于Leecode网站,在此我们对原题进行进行简单的改动,并形成经典的最短路径问题。
a.) 问题描述
一个机器人位于m*n 矩阵的最左上角的角点上,机器人每次只能向下或向右移动一步至相应的顶点上,特别规定机器人不能回退或沿着对角线进行移动,请问如果机器人要到达最右下角的角点上,其走过的最短路径值为多少?
为了更清晰的描述问题,用3*3的角点表示机器人可能经过的顶点,每两个相邻顶点之间的边的距离表示机器人需要移动的长度。紫色顶点①表示起始点,橙色顶点⑨表示到达目的顶点。
b) 求解过程
此问题可以利用图的算法的中的拓扑排序算法和关键路径算法实现,因为上图从本质上属于有向无环图(DAG),通过拓扑排序可以找到从起点①到终点⑨的拓扑有序路径,由于拓扑排序后的顶点只和后续顶点相关,而与前面的顶点无关,所以可以采用关键路径遍历的方法,按照拓扑排序的顶点顺序,不断找寻最小值,最后求得①-⑨顶点之间所经历的最短路径。
本算法重点关注动态规划求解过程和基本算法,采用CRCC的经典方式,对此问题进行解析,最终求得机器人从①-⑨所经过的最短距离。
应用动态规划之前,我们需要判断问题是否能应用动态规划进行求解,其实要求之一就是各个子问题互相独立,如果用图来分析,那么要求整个问题形成的图必须有向无环,否则如果有环存在,必然形成各个子问题相互依赖,最终导致各个子问题无法求解的困境,环必然导致各个顶点之间相互依附。
本问题的各个顶点均位于有向无环图的路径上,所以各个子问题互相独立,不存在相互的依附关系,所以可以采用动态规划的方法进行求解。
老规矩,按照《算法导论》上提到的CRCC步骤对此动态规划问题进行求解。
- 表征最优解的结构(Characterize the structure of the optimal solution)
首先需要表征最优解的结构,我们定义m[i,j]为从原点到达坐标(i,j)的最短路径,如果原点定义为(0,0),那么⑨号顶点位置的坐标为(2,2),现在要找到从原点(0,0)到达此点的最短距离,我们有两个选择,选择之一是经由⑥号顶点,向下直接到达;选择之二是经由⑧号顶点,从左边直接到达。因为需要求解最短路径,所以最终的决定只能二选一,选择最小的值,也即最短路径。最终求得关系式:
m
[
i
,
j
]
=
{
m
[
i
−
1
,
j
]
+
t
o
p
d
i
s
t
a
n
c
e
,
m
[
i
,
j
−
1
]
+
l
e
f
t
d
i
s
t
a
n
c
e
}
m[i,j]=\left\{ m[i-1,j]+topdistance, m[i,j-1]+leftdistance\right\}
m[i,j]={m[i−1,j]+topdistance,m[i,j−1]+leftdistance}
其中topdistance 表示边(i-1,i)的距离,此向量自上而下;leftdistance表示(j-1,j)的距离,此向量自左而右。上述关系式当中,m[i-1,j]和m[i,j-1]表示的是原点到此顶点的最短距离。
- 递归定义最优解的值(Recursively define the value of the optimal solution)
递归定义上数字表达式的值,需要分为5类不同的条件限制,
1. 如果i=-1,同时j≠0
2. 如果j=-1,同时i≠0
3. 如果i=-1,同时j=0
4. 如果j=-1, 同时i=0
5. 除上述情况外的一切i和j值
上述分类其实处理是边界条件,也就是第一行和第一列的情形,图中可以明显看出,除了①号顶点外,所有蓝色箭头代表的值均为无穷大(infinity),这时候需要在递归程序中进行特别的判断,及时终止递归。
为了更好进行计算,基本数据结构采用邻接矩阵比较合适,在邻接矩阵中,我们对每个顶点定义两个基本的元素值,
typedef struct ArcCell
{
int left_distance;
int top_distance;
}ArcCell, AdjMatrix[M][N];
对于⑨号顶点,我们可以定义m[3] [3].left_distance=42 , m[3] [3].top_distance=9,表示顶点⑨的自左而右 与 自上而下的入边距离值。
这两个数据作为邻接矩阵的基本要素,在程序当中加以使用。
- 计算最优解的值(Compute the value of the optimal solution)
利用递归结构计算最解的值得过程中,我们采用memo的方式对重复的解的值进行储存,如果再遇到相同的问题,直接返回值,减少运算开销。数组d[i] [j]肩负的就是这个任务。学习过程中,对重叠子问题的概念不是非常理解,随着练习增加,逐步对此问题有些许理解,就这个问题而言,我们看到红色椭圆形框内的顶点⑤,我们在历经⑤->⑥->⑨(黄色曲线)和⑤->⑧->⑨(黑色曲线)的过程中,都需要用到顶点⑤的最短路径解,如果没有memo数组,那么就需要重复计算顶点⑤的最最短路径,如果有了记忆功能,第二次需要顶点⑤的时候,我们直接可以调用值,减少不必要的计算开销。
在递归算过程中,我们的代码如下:
a-1) 递归算法头文件(robot_mininum_path.h)
/**
* @file robot_mininum_path.h
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-02-24
*
* @copyright Copyright (c) 2023
*
*/
#ifndef ROBOT_MINIMUM_PATH_H
#define ROBOT_MINIMUM_PATH_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define INFINITY 9999
#define M 3
#define N 3
typedef struct ArcCell
{
int left_distance;
int top_distance;
}ArcCell, AdjMatrix[M][N];
/**
* @brief Find the minimum distance between vertex i and vertex j
*
* @param arcs Adjacent matrix that stores the left_distance and top_distance
* @param d Memo array d[i][j]
* @param i Row indicator starting from 0 index
* @param j Column indicator starting from 0 index
* @return int Return the shortest patth from source to destination
*/
int find_minimum_distance(AdjMatrix arcs, int d[M][N],int i, int j);
/**
* @brief Find the minimum distance between vertex i and vertex j
*
* @param arcs Adjacent matrix that stores the left_distance and top_distance
* @param d Memo array d[i][j]
* @param i Row indicator starting from 0 index
* @param j Column indicator starting from 0 index
* @return int Return the shortest patth from source to destination
*/
int find_minimum_distance_aux(AdjMatrix arcs, int d[M][N], int i, int j);
/**
* @brief Find the minimum value between d1 and d2
*
* @param d1 distance 1
* @param d2 distance 2
* @return int -return miniumum distance
*/
int min(int d1, int d2);
#endif
b-1) 函数的实现,递归的终结判断条件显得复杂,没有迭代算法简洁,如果有更好的计算方法,欢迎在评论区给出答案。
/**
* @file robot_minimum_path.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-02-24
*
* @copyright Copyright (c) 2023
*
*/
#ifndef ROBOT_MINIMUM_PATH_C
#define ROBOT_MINIMUM_PATH_C
#include "robot_minimum_path.h"
int find_minimum_distance(AdjMatrix arcs, int d[M][N], int i, int j)
{
int m;
int n;
for(m=0;m<=i;m++)
{
for(n=0;n<=j;n++)
{
d[m][n]=INT_MAX;
}
}
return find_minimum_distance_aux(arcs,d,i,j);
}
int find_minimum_distance_aux(AdjMatrix arcs, int d[M][N], int i, int j)
{
int dist_1;
int dist_2;
int min_dis;
if(i>=0 && j>=0 && d[i][j]<INT_MAX)
{
return d[i][j];
}
if(i==0&&j==-1)
{
min_dis=0;
}
else if(i==-1 && j==0)
{
min_dis = 0;
}
else if(i!=0 &&j==-1)
{
min_dis = INFINITY;
}
else if(i==-1 && j!=0)
{
min_dis = INFINITY;
}
else
{
dist_1 = find_minimum_distance_aux(arcs,d, i - 1, j) + arcs[i][j].top_distance;
dist_2 = find_minimum_distance_aux(arcs,d,i, j - 1) + arcs[i][j].left_distance;
min_dis = min(dist_1, dist_2);
d[i][j]=min_dis;
}
return min_dis;
}
int min(int d1, int d2)
{
return (d1<d2?d1:d2);
}
#endif
c-1) 主函数测试,为了程序运行方便,本次放弃了比较常用的从文件(FILE)创建邻接矩阵表的做法,直接进行赋值,方便大家测试。
/**
* @file robot_minimum_path_main.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-02-24
*
* @copyright Copyright (c) 2023
*
*/
#ifndef ROBOT_MINIMUM_PATH_MAIN_C
#define ROBOT_MINIMUM_PATH_MAIN_C
#include "robot_minimum_path.c"
int main(void)
{
AdjMatrix arcs;
int min_dist;
int dp[M][N];
int i;
int j;
i=M-1;
j=N-1;
arcs[0][0].left_distance=0;
arcs[0][0].top_distance = 0;
arcs[0][1].left_distance = 5;
arcs[0][1].top_distance = INFINITY;
arcs[0][2].left_distance = 30;
arcs[0][2].top_distance = INFINITY;
///
arcs[1][0].left_distance = INFINITY;
arcs[1][0].top_distance = 10;
arcs[1][1].left_distance = 15;
arcs[1][1].top_distance = 7;
arcs[1][2].left_distance = 11;
arcs[1][2].top_distance = 3;
///
arcs[2][0].left_distance = INFINITY;
arcs[2][0].top_distance = 16;
arcs[2][1].left_distance = 18;
arcs[2][1].top_distance = 12;
arcs[2][2].left_distance = 42;
arcs[2][2].top_distance = 9;
min_dist=find_minimum_distance(arcs, dp, i, j);
printf("The minimum distance is %d\n",dp[2][2]);
getchar();
return EXIT_SUCCESS;
}
#endif
我们继续探讨迭代方式的求解过程,在本问题上,迭代求解程序看起来简洁大方, 比较优雅,上述递归求解的过程的确看起来比较臃肿。我们利用迭代过程中,自下而上的方式进行计算。
我们要从底层dp[i] [0] 或dp[0] [j]的子问题开始计算值,这属于子问题范畴中的“小的子问题”,从图中我们看出,对于dp[i] [0],我们仅需从起点开始,沿着第0列,逐个边的长度递加即可,因为它只有向下的方向可以进行,没有从左边而来的边;同理对于dp[0] [j]只需要关注第0行的元素即可,沿着第0行逐个边长递加,因为没有从顶部输入的第二个子问题(或者说第二个子问题的距离∞,∞的概念我们在上述递归过程中有效使用)。那么问题就可以有效简化,在正式迭代之前,我们可以定义三类已知解:
- DP[0] [0]=0;
- DP[i] [0]=DP[i-1] [0]+DP[i] [0].top_distance;
- DP[0] [j]= DP[0] [j-1] +DP[0] [j].left_distance;
完成了这三类已知最短距离的定义,我们可以从第二行/第二列开始正式迭代,非常优雅地完成整体的迭代过程。迭代过程的代码供大家参考。
void find_minimum_distance(AdjMatrix arcs, int dp[M][N], int i, int j)
{
int k;
int m; //row indicator
int n; //column indicator
dp[0][0]=0;
/**Initialize the dp value in the first row(i=0)*/
for(k=1;k<j;k++)
{
dp[0][k]=dp[0][k-1]+arcs[0][k].left_distance;
}
/** Initialize the dp value in the first column*/
for(k=1;k<i;k++)
{
dp[k][0]=dp[k-1][0]+arcs[k][0].top_distance;
}
for(m=1;m<i;m++)
{
for(n=1;n<j;n++)
{
dp[m][n]=min(dp[m-1][n]+arcs[m][n].top_distance,dp[m][n-1]+arcs[m][n].left_distance);
}
}
return;
}
最后一个需要解决的问题是,根据前面的递归或迭代计算的过程,记录机器人所走过的最短路径,我们可以定义p[i] [j]的字符串二维数组,在整体的遍历过程中,可以用’L’和’T’对当前最短距离路径来源于左侧还是上侧进行标记,如果dp[m] [n]=dp[m-1] [n]+arcs[m] [n].top_distance, 那么就标记p[m] [n]=‘T’。
我们后面可以利用递归对p[m] [n]进行遍历,打印出最短的路径。
后记
学习动态规划问题已两周,随着学习的深入,愈发觉得动态规划的博大精深和深不可测,希望能按照算法导论的模板思路对后面的练习题进行分解,并深入学习。
机器人最短路径可以归纳为二位数组的动态规划问题,有些问题可以理解问题一维数组的动态规划问题。
参考资料:
1.《Introduction to algorithm, 4ed, edition》