对《Dynamic programming approaches for the traveling salesman problem with drone》论文的理解与代码实现

本文介绍了针对“无人机卡车结合的旅行商推销问题”的解决方案,运用动态规划和A*算法,通过合理的剪枝减少复杂度,实验表明结合无人机能大幅减少配送时间。算法设计包括标准TSP动态规划、卡车节点TSP、无人机节点TSP,并构建状态图进行优化。通过限制卡车节点数量,降低了时间复杂度,最后使用A*算法实现近似最优解。
摘要由CSDN通过智能技术生成

摘要:本文针对“无人机卡车结合的旅行商推销问题”,主要运用了动态规划、A*算法,通过合理的剪枝减少复杂度,自拟数据集进行了分析。实验结果表明:将无人机纳入送货系统能够极大地减少配送时间,并且通过合理的剪枝,算法往往确实能够得到最优解或者近似最优解,而时间复杂度能够得到大幅度的下降。

原文链接:Dynamic programming approaches for the traveling salesman problem with drone - Bouman - 2018 - Networks - Wiley Online Library

正文:

1、实验目的

经典的旅行商问题适用于目前的许多场景,如外卖骑手的路径规划,快递网点的分配。然而,近几年无人机技术的发展,也为配送行业开辟了新的道路。将无人机送货纳入经典卡车送货的系统,也为我们旅行商问题的算法设计提出了巨大的挑战。本实验旨在从经典的旅行商问题出发,提出一种“卡车-无人机”结合的最短路径规划,并通过近似算法努力降低算法的复杂度,使算法能够用于现实生活中节点数据集庞大的情况。

2、实验设计流程

   通读论文,我将代码的实现分为如下几个部分:

1、标准TSP的动态规划实现

2、编写只有卡车节点的TSP问题

3、编写纳入无人机之后的TSP问题

4、利用已初始化的卡车—无人机TSP问题构建状态图

5、通过限制卡车节点的方式重构状态图以减少时间复杂度

6、利用A*算法实现最终代码解决“卡车-无人机送货”问题

  1. 标准TSP的动态规划实现

 

论文中已经给出了标准TSP动态规划的递归函数,但仍需解决其中数据表示的问题。c(v,w)可以用邻接矩阵表示,接下来我们重点解决“点的集合”应该用何种方式表示的问题。

点集合的表示方式:

如果将点集合的状态用数组来表示的话,将会产生极大的时间与空间开销。我最初的想法是放弃C++,运用python中的集合字典元组等数据集来实现点集合的存储。搜查资料发现,即使是Python实现的TSP算法,也完全没有用到这样的数据结构,而是运用了一种“状态压缩”的方法。实现起来也很简单,将点的状态映射到一个二进制数,此二进制数从低位开始编号,如果位数为1,表示此点在集合中,0则代表不在。如1010,代表1,3节点不在集合中,2,4节点在集合中。接下来我介绍围绕此种表示方法的一些基本运算。

当u∈S时,表示为S&(1<<(u-1)),当u在集合中时取1。

S\{w},表示为S-(1<<(w-1))

S\V,表示为S-V,等等

最后还有一个关键是计算S中节点的个数,我立马就想到了之前计算机系统基础做的实验,当然这里我们可以有更多的运算工具,以下是其实现函数。即简单的取余运算就可以完成。

 

按照论文中给出的递归关系,核心代码如下:

 

其中,循环的最外层是S按集合中点的个数从2到最大,结合nf1函数判断完成。递归的第二层是w点对整个集合的遍历,u点是对当前点集合的遍历。其中的p数组是用来存储每个节点的父节点方便最后回溯输出最短路径。

  1. 编写只有卡车节点的TSP问题

   

 

   与标准TSP的动态规划相比,此算法的不同在于摒弃了单一的起点,将起点和终点都任意化。因为这一步已经进入了实际问题,是用来构建后来的状态函数,而这样做的目的在于为状态函数中的边权进行初始化。与经典TSP相比,此处对起点的任意化道理是一样的,无非是加一层循环。

   核心代码如下:

 

  1. 编写纳入无人机之后的TSP问题

 

将无人机结合进来的具体实现过程,我们从动态规划的单个子问题着手分析。在只有卡车节点动态规划的单个子问题中,S中所有的点都是卡车节点。而现在的S中允许存在一个无人机节点,这样在卡车从v到w的过程中,可以发射一个无人机来解决其中一个节点的送货。我们在算法中将此节点在算法中设为d,通过对d的遍历找到最小值。无人机节点不可以是起点和终点。

核心代码实现如下:

 

  1. 利用已初始化的卡车—无人机TSP问题构建状态图

 

    最开始读到这一步的时候,我感觉有些“多此一举”。之前的Dop算法似乎已经解决了问题,为什么还要创建一个状态函数的动态规划把问题复杂化呢?这篇论文我感觉最大的难点就在这里。前面的逻辑性都很强,顺水推舟一路下来,现在冷不丁冒出一个状态函数就让人很难理解。我猜测它的目的一定是用来降低算法的时间复杂度。我们先来理解一下所谓的状态函数。状态函数D(S,w)指从起点开始遍历完S中所有节点后以w为终点的最优解。这其实与最经典的旅行商TSP问题一样,只是这里这样做的目的是为了构造一个以状态函数为节点的图(经典的TSP问题以具体地点为节点),而两个节点的边权是从这个状态跳到另一个状态需要的代价(用Dop表示)。相比于以实际的地点为节点的图,这样表示显得很抽象,但概念本身不难理解。我们需要思考的是为什么要这么做——之后我们会看到这样做的目的是为了降低前两步的时间复杂度。

   状态图中的边权就是连接两个状态函数所付出的代价,可以理解为S的一个子集-通过其对S的补集的Dop-构造出了一条通往S的路。

核心代码如下:

 

  1. 通过限制卡车节点的方式重构状态图以减少时间复杂度

接下来才是重头戏,原理很简单,规定一个Dt和Dop中的卡车节点不能超过k个,也就相当于直接舍弃Dt和Dop中点集合大于k+2的子问题(因为起点和终点是必不可少的卡车节点,所以这里是k+2)。当然,这样做并不一定是最优解,而是删除一些不可能是最优解的边,对最后的状态图进行一种近似。近似的目的是为了降低Dt与Dop的时间复杂度,那么为了降低它们的时间复杂度而对状态图进行“剪枝”是否合理呢?

近似算法的合理性:

在最优解中,大部分情况,卡车节点在一个状态图边权的占比都应该尽可能低。尤其是当无人机比卡车快的时候(这也是实际情况),最优解的策略一定是尽可能多分配节点给无人机,从而最大化地提高并行性。因此,在Dt和Dop中,卡车节点过多的子问题就可以舍去,因为它们几乎不可能在最优解中。

(对于其降低时间复杂度的程度方面将在之后一并讨论)

  1. 利用A*算法实现最终代码解决“卡车-无人机送货”问题

经典的A*算法:

  1. 将起点装入优先队列,并用一个估计函数估计其到终点的距离。
  2. 弹出优先队列中最上面的点并将其标记,将这个点周围所有没有标记过的点压入优先队列。(优先级的依据:当前节点已经包含的路径长度加上通过估计函数算出的未来估计值)
  3. 重复步骤2,直到队列为空或弹出终点。

   其中的关键是估计函数的实现。论文中运用最小生成树产生估计函数的值,接下来需要探讨如何构造这个最小生成树。

   假设一共有V个点,在此问题中,对任何一个子点集都需要构造一颗最小生成树。我这里选用了经典的prim算法,为每个子集构造一颗最小生成树。

核心代码如下:

 

接下来根据论文中的伪代码就可以写出具体代码了,需要注意要限制Dop中的卡车节点数。(在大循环中,将V变为k+2)

 

代码的具体实现如下:

 

4、代码实现

以下是完整的代码实现:

#include <iostream>

#include<algorithm>

#include<functional>

#include <cstdio>

#include <cstdlib>

#include<queue>

#include<vector>

#define INIT 1000;

using namespace std;

int nf1(int n)//计算一个二进制数中1的个数,用于确定集合中包含的点的个数

{

      int count = 0;

      while (n)

      {

            if (n % 2 == 1)

            {

                   count++;

            }

            n /= 2;

      }

      return count;

}

typedef struct state {//用于最终的算法状态函数结构体

      int node;

      int S;

      int costs;

      int key;

      bool operator < (const state& a) const//为优先队列重定位排序项

      {

            return key > a.key;

      }

};

priority_queue<state>stateq;//初始化小顶堆队列

int main()

{/*第一阶段,实现只有卡车节点时的TSP算法,此处已是通过第三阶段限制卡车节点之后的代码*/

      int*** Dt = new int** [13];//初始化卡车状态函数

      for (int i = 1; i <= 12; i++) {

            Dt[i] = new int* [13];

      }

      for (int i = 1; i <= 12; i++) {

            for (int j = 1; j <= 12; j++) {

                   Dt[i][j] = new int[1 << 12];

            }

      }

      int c[12][12];  //初始化领接矩阵

      int V;

      cin >> V;

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   cin >> c[i][j];

            }

      }

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   for (int k = 1; k <= ((1 << V) - 1); k++) {

                         Dt[i][j][k] = INIT;//初始化为正无穷

                   }

            }

      }

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   Dt[i][j][1 << (j - 1)] = c[i][j];//对所有其集合是单节点的状态函数,都初始化其为到起点的距离

            }

      }

      /*此段代码及运用动态规划,将在实验报告中详细描述*/

      for (int i = 2; i <= 4; i++) {

            for (int j = 1; j <= ((1 << V) - 1); j++) {

                   if (nf1(j) == i) {

                         for (int v = 1; v <= V; v++) {

                               for (int w = 1; w <= V; w++) {

                                     if ((1 << (w - 1)) & j) {

                                            for (int u = 1; u <= V; u++) {

                                                  if ((1 << (u - 1)) & j) {

                                                        int z = Dt[v][u][j - (1 << (w - 1))] + c[u][w];

                                                        if (z < Dt[v][w][j]) {

                                                              Dt[v][w][j] = z;

                                                        }

                                                  }

                                            }

                                     }

                               }

                         }

                   }

            }

      }

      /*第二阶段,包含无人机-卡车节点的TSP算法,此处已是在第三阶段对卡车节点限制之后的代码*/

      int*** Dop = new int** [13];//初始化包含无人机节点的状态函数

      for (int i = 1; i <= 12; i++) {

            Dop[i] = new int* [13];

      }

      for (int i = 1; i <= 12; i++) {

            for (int j = 1; j <= 12; j++) {

                   Dop[i][j] = new int[1 << 12];

            }

      }

      int cd[12][12];//初始化无人机节点的邻接矩阵

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   cin >> cd[i][j];

            }

      }

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   for (int k = 1; k <= ((1 << V) - 1); k++) {

                         Dop[i][j][k] = INIT;

                   }

            }

      }

      for (int i = 1; i <= V; i++) {

            for (int j = 1; j <= V; j++) {

                   Dop[i][j][1 << (j - 1)] = Dt[i][j][1 << (j - 1)];//对所有只包含单节点的状态函数都初始化为其到起点的距离

            }

      }

      /*此段代码及运用动态规划,将在实验报告中详细描述*/

      for (int i = 2; i <= 4; i++) {

            for (int j = 1; j <= ((1 << V) - 1); j++) {

                   if (nf1(j) == i) {

                         for (int v = 1; v <= V; v++) {

                               for (int w = 1; w <= V; w++) {

                                     if ((1 << (w - 1)) & j) {

                                            for (int u = 1; u <= V; u++) {

                                                  int j2 = j - (1 << (v - 1)) - (1 << (w - 1));

                                                  if ((1 << (u - 1)) & (j2)) {

                                                        int z = max(cd[v][u] + cd[u][w], Dt[v][w][j - (1 << (u - 1))]);

                                                        if (z < Dop[v][w][j]) {

                                                              Dop[v][w][j] = z;

                                                        }

                                                 }

                                            }

                                     }

                               }

                         }

                   }

            }

      }

      int* spantree = new int[1 << V];//初始化最小生成树用于之后A*算法的估计函数

      for (int i = 1; i <= ((1 << V) - 1); i++) {

            spantree[i] = 0;

      }

      int ex[13];

      for (int S = 1; S <= ((1 << V) - 1); S++) {

            int nonu = 0;

            int fs = 0;

            int ld[13];

            for (int i = 1; i <= 12; i++) {

                   ex[i] = 0;

            }

            for (int i = 1; i <= V; i++) {

                   if ((1 << (i - 1)) & S) {

                         fs = i;

                         ex[i] = 1;

                         nonu++;

                   }

                   for (int k = 1; k <= V; k++) {

                         if (ex[k]) {

                               ld[k] = c[fs][k];

                         }

                   }

            }

            ex[fs] = 0;

            nonu--;

            while (nonu > 0) {

                   int min = INIT;

                   int nownode = 0;

                   for (int j = 1; j <= V; j++) {

                         if (ex[j]) {

                               if (ld[j] < min) {

                                     min = ld[j];

                                     nownode = j;

                               }

                         }

                   }

                   nonu--;

                   ex[nownode] = 0;

                   spantree[S] += min;

                   for (int j = 1; j <= V; j++) {

                         if (ex[j]) {

                               if (c[nownode][j] < ld[j]) {

                                     ld[j] = c[nownode][j];

                               }

                         }

                   }

            }

      }

      /*第三阶段,运用限制了卡车节点的Dop函数以及A*算法以及动态规划,同样将在实验报告中详细展开*/

      int** P = new int* [1 << 12];

      for (int i = 1; i <= ((1 << V) - 1); i++) {

            P[i] = new int[12];

      }

      for (int i = 1; i <= ((1 << V) - 1); i++) {

            for (int j = 1; j <= V; j++) {

                   P[i][j] = 1;

            }

      }

      state fs;

      fs.node = 1;

      fs.S = 1;

      fs.costs = 0;

      fs.key = spantree[((1 << V) - 1)];

      stateq.push(fs);

      while (!stateq.empty()) {

            state nstate = stateq.top();

            stateq.pop();

            P[nstate.S][nstate.node] = 0;

            if ((nstate.S == ((1 << V) - 1)) && (nstate.node == 1)) {

                   cout << nstate.costs << endl;

                   break;

            }

            for (int w = 1; w <= V; w++) {

                   for (int ops = 2; ops <= 4; ops++) {

                         for (int nS = 1; nS <= ((1 << V) - 1); nS++) {

                               if ((nf1(nS) == ops) && ((1 << (nstate.node - 1)) & nS)) {

                                     if (P[nS | nstate.S | (1 << (w - 1))][w]) {

                                            state newstate;

                                            newstate.S = nS | nstate.S | (1 << (w - 1));

                                            newstate.costs = nstate.costs + Dop[w][nstate.node][nS];

                                            newstate.key = newstate.costs + spantree[((1 << V) - 1) - newstate.S + (1 << (w - 1))];

                                           newstate.node = w;

                                            stateq.push(newstate);

                                     }

                               }

                         }

                   }

            }

      }

}

4、实验结果

(论文以及官网的文件均没有给出测试数据集,因此我选择自拟数据集)

由于较难拟出较大的数据集,并且我所设计的代码最多支持12个点的集合,因此我选取了节点较少的数据集进行测试。接下来的几个样例中,对于输入的矩阵数据,第一个矩阵代表以卡车速度为度量的邻接矩阵,第二个矩阵代表以无人机速度为度量的邻接矩阵。其中每个数据集都包含了三个对比样例:只有卡车节点的结果;包含无人机节点的A*算法结果;无人机速度较小时的A*算法近似结果;包含无人机节点的真实值;包含无人机节点且无人机速度较小时的真实值。

数据集1:

只有卡车节点

 

包含无人机节点的A*算法

 

无人机速度较小时的A*算法近似结果

 

包含无人机节点的真实值

 

包含无人机节点且无人机速度较小时的真实值

 

数据集2:

只有卡车节点

 

包含无人机节点的A*算法

 

无人机速度较小时的A*算法近似结果

 

包含无人机节点的真实值

 

包含无人机节点且无人机速度较小时的真实值

 

数据集3:

只有卡车节点

 

包含无人机节点的A*算法

 

无人机速度较小时的A*算法近似结果

 

包含无人机节点的真实值

 

包含无人机节点且无人机速度较小时的真实值

 

因为我是任意取的值,所选的数据集可能不具备代表性,但从实验结果的直观分析,无人机的速度对结果有很大影响;同时,纳入无人机也极大地提升了运送效率,因此我们可以说,无人机送货确实是未来的发展趋势。A*算法在节点数较小的时候近似效果比较好,而当节点数较大时就出现了一定的偏差。下面是论文中出现的一个证明,我用我自己的理解把它写了下来:

命题:一个把卡车节点限制为0的A*算法,最多是最优解两倍的时间。

 

5、复杂性分析

1、Dtsp(S,w)算法

假设节点数为n,共有2n个不同的集合S,n个不同的w,因此共有n*2n个需要解决的子问题;对于每一个子问题,又需要对其S中的每一个点进行遍历,数量级为n;所以算法的渐进上界为o(n2*2n)。

2、Dt(S,v,w)算法

相比于标准的Dtsp(S,w)算法,此算法相当于多遍历了一个点v,多了n倍,因此算法的渐进上界为o(n3*2n

3、Dop(S,v,w)算法

此算法需要用到初始化完的Dt(S,v,w)算法,有一个预先的复杂度o(n3*2n),而其本身与Dt(S,v,w)算法的算法相同为o(n3*2n),所以总渐进上界为o(n3*2n)。

4、D(S,w)

粗估计:一共有n*2n个子问题,其中每一个子问题都需要对S进行与原问题类似的一个遍历(详见代码设计流程部分),最多需要n*2n个子问题,那么总渐进上界为o(n2*4n)。

更进一步估计:

 

此算法的渐进上界为(n2*3n

5、限制卡车节点后的D(S,w)算法

将卡车节点限制在k之后,每一个算法的时间复杂度都会相应地减小。

Dt(S,v,w)算法:o(ni=1k+1nii2)

Dop(S,v,w)算法:o(ni=1k+1nii2)

D(S,w)算法:任然需要解决n*2^n个子问题,但是每一个子问题需要的操作只有nk+1,因此算法的渐进上界为o(2n*nk+2).

总结:

本实验是对一篇论文中的思想与算法的理解与实现,实现过程为从卡车节点、无人机节点的TSP算法到状态图的A*算法,最后通过限制卡车节点降低时间复杂度。其中算法的前几个过程都很好理解,但当进入状态图之后就变的难以理解起来。这是违背我定式思维的地方,我需要学习的地方。我们把状态图类比于电场与电势,普通图的边权是U=Ex,而状态图是U=U1-U2,我的理解是,这样做通过(U1-U2)实现最终算法,将Ex“解放出来”,从而可以想办法对其进行剪枝与近似。这一过程的设计中,我们不仅要考虑剪枝的合理性,而且还要考虑状态图本身的算法时间复杂度,从而判断这一过程是否值得。在此次算法中,这一过程是值得的,其在保证剪枝合理性的同时,降低了幂指函数的数量级。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值