链接:https://pan.baidu.com/s/1bspfq4eEOGp2tLM0kmbVMw?pwd=5y6f
提取码:5y6f
算法分析与设计 | |||||
时间 | 2020.4.23 | ||||
实验名称 | 汽车加油行驶问题 | ||||
实验目的 | 通过上机实验,要求掌握动态规划算法的问题描述、算法设计思想、程序设计。 | ||||
实验原理 | 设计一个算法,利用动态规划法的思想,根据题目所给出的条件,求出汽车从起点出发到达终点所付的最小费用。 | ||||
实验步骤 | 问题分析:该问题有最优子结构,在(x,y)坐标处,汽车运动到(x,y)坐标的途径有(x-1,y)、(x,y-1)、(x+1,y)、(x,y+1),因此只需要取经这四个坐标到达(x,y)坐标所需花费的最小值即可得到到(x,y)处的最小值,如果(n-1,n)、(n,n-1)这两个坐标的最小花费确定,则可求得问题的解(x,y)处的最小花费; 这里用f(x,y,0)、f(x,y,1)表示从(1,1)到(x,y)的最小费用和剩余能够行驶的距离; 建立状态转移方程为: f(1,1,0)=0,f(1,1,1)=K; f(x,y,0)+=A、f(x,y,1)=K;(x,y)为油库; f(x,y,0)+=C+A、f(x,y,1)=K;(x,y)处非油库且油用尽; f(x,y,0)=min{f(x+s[i][0],y+s[i][1]),0)}+s[i][2]} 0<=i<4; 其中数组s中保存的是四个方向移动; 解题步骤①读取文件中题目信息; ②初始化**f数组,令f[][][0]全为整数最大值(f[1][1][0]起始地点为0),f[][][1]=K; ③循环进行④处理,若处理过程中标记位置1则处理结束后进行继续循环处理,直到某次标记位不为1,结束循环;(标记位置1是在f[][][0]有更小值而更新的时候,如果有最小值更新那么从更新的点的周围四个点就有可能会被更新,进而扩展到会影响f[n][n][0]的值); ④从第一行第一列开始遍历,从左至右从上至下遍历整个地图,对每一个点(i,j)求从它周围四个点到它所需费用的最小值(需要进行边界检查),如果最小值小于f[i][j][0]则说明可以更新这个点,因此将标记位置为1; ⑤从③的循环中出来后说明遍历图的最后一次没有一个点被更新,所有点都是最优解,因此f[n][n][0]为最优解,可以直接输出; 纠错事实上跟同学讨论的时候发现这个解题方法具有非常大的漏洞,极大概率得出的结果会是错误的; 如图:
我们可以根据上述算法得到它最后所有子问题的最终解:
我们可以明显地看到, 经过(5,2)到达(5,5)的大小为13,也就是说(5,5)处的最优解应该是比108更小的13,进而整个问题的最优解也应该是(1,2)->(3,3)-> (5,2)->(5,5)->(6,6)的得到的最优解13; 然而用上面的解题步骤得到的解为106,明显错误; 原因其实很简单,(5,5)的解只跟(5,4)、(5,6)、(4,5)、(6,5)有关,这里(5,5)的解是根据(5,4)得来的; 但是呢,(5,4)的最优解来自于(3,3)经过(5,3)的路径,因为(5,3)的解按照上面的方法来的话如果经过(5,4)就会是比4大的11,不可能更新,因此此时(5,4)的解就与(5,2)没有关系了,这个时候(5,4)还剩下1步可走,到(5,5)剩余步数为0,需要建立加油站并加油; 因此,这种方法对于往回走会产生的最优解无法有效的反应,需要改进算法; 优化方法一种最简单的优化方法就是令更新后的子问题的解(x,y)在当前行或列往后处理剩余步数的路径,比如剩余2步,那就处理(x+1,y)、(x+2,y)、(x,y+1)、(x,y+2),看(x,y)的最优值更新是否会对后面这些解有影响; 但是这种方法的时间复杂度会非常大; 因此用建图求单源最短路径的方法来重新做这个实验: 大致思路:我们把每个点都看成一个状态就可以,只不过这里每个点会因为剩余油量的问题多出k个状态。每个点的状态相当于是(i,j,k)(i,j,k)了,分别表示在网格(i,j)这个点,剩余流量是k的一个状态。然后建图: ①对于(i,j)点是油库。那么我们对于(i,j,l)(i,j,l)到(i,j,k)(i,j,k)连一条边,花费为a。l的范围是0到k-1. ②对于(i,j)点不是油库。那么我们对于(i,j,l)(i,j,l)到(i,j,k)(i,j,k)连一条边,花费为a+c。l的范围是0到k-1. ③对于(i,j)点是油库。那么我们对于(i,j,k)(i,j,k)到(i1,j1,k−1)(i1,j1,k−1)连一条边,花费为0|b。这里表示的是这个点能够到达的相邻的点。花费按照横纵坐标的变化而不同。 ④对于(i,j)点不是油库。那么我们对于(i,j,l)(i,j,l)到(i1,j1,l−1)(i1,j1,l−1)连一条边,花费为0|b。l的范围是1到k.这里表示的是这个点能够到达的相邻的点。花费按照横纵坐标的变化而不同。 然后跑一个(0,0,k)(0,0,k) 的最短路,更新到(n−1,n−1,l)(n−1,n−1,l) 的答案即可。l从0到k。 建图描述:三维点表示的是(x,y,k),x,y代表坐标,k代表层数 1、 源点->(1,1,k),流量为1,花费为0 2、(x,y,k)->(x,y,k-1),流量为无穷大,花费符合条件2 3、当前点是否为油库: 是油库:(x,y,t)t∈[0,k-1]->(x,y,k),流量为无穷大,花费为a 不是油库:(x,y,0)->(x,y,k),流量为无穷大,花费为a+c (n,n,t)t∈[0,k]->汇点,流量为无穷大,花费为0 算法步骤:1、设立一个先进先出的队列用来保存待优化的结点。 2、优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。 3、这样不断从队列中取出结点来进行松弛操作,直至队列空为止 | ||||
关键代码 | 关键代码(带注释)首先是第一种解题步骤(有缺陷)的代码:
以上为初始化**f数组部分; pd就是所定义的标志位; (Prex,Prey)是可以到达(x,y)的前一个点(最多有四种); 比较经过(Prex,Prey)到达(x,y)所需的费用,取最小值 最小值与此时的f[x][y][0]比较,如果更小则更新,并将标记位置1; 对于向回走只能反应一步,但是可能多向后走几步就会出有一个更优的解; 因此这种方法并不适合所有情况; 接着是用新的方法实现的动规部分:大致上为单源最短路径算法SPFA算法的扩展应用; 当前结点now如果遇到了加油站,则加油之后再入队列,下一次出队列后再处理; 当前now结点用完了油,并且没有加油站,则设油库加油并入队列; 否则,就根据当前结点now预期它的下一个结点,如果有更新则更新后令下一结点入队(入队列); 读取文件 | ||||
测试结果 | 运行结果截图及分析题目样例: 对于题目样例,往回走并不会影响到后面的子问题,因此运行无误; 而对于第二种方法:最后一行为输出 对与往回走影响后面子问题解的情况,刚才的6*6网格可以输出正确的最优解; 时间复杂度分析:第一种方法的时间复杂度为O(n^3); 第二种方法最坏时间复杂度为O(VE),顶点和边数的乘积; | ||||
实验心得 | 这个实验相比较0-1背包问题还要更加的复杂,但是只要把握好了状态转移方程,紧紧围绕着方程式来进行程序的编写就可以很轻松的写出动态规划的代码了,但其中需要注意对子问题的解的更新可能会反过来对已经处理过的子问题的解产生影响,因此求过一次所有子问题的解后一定要确保所有子问题的解都已经是最优的了;第一种方法只能判断一步这种影响,因此它并不总是对的,而第二种方法的影响是持续的,因此可以证明是正确的; |