动态规划问题- 机器人的最短路径

动态规划问题- 机器人行走最短路径

动态规划问题中,求最短路径是一类经典的动态规划问题,可以利用动态规划的思路进行有效求解。最初问题来源于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[i1,j]+topdistance,m[i,j1]+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》

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值