浅析递归,递推及动态规划续
蜜蜂采蜜问题
问题描述:蜜蜂从起点O出发,经过A、B、C、D、E后回到O,其中每个点的坐标已给定,并且每个点只能经过一次,求最短路径及走法。
问题解析:
这是一个典型的TSP问题,我们两种方法来解决,一种是暴力解法穷举所有的路径情况,另一种是用BP方法。
穷举法
容易想到排除O点外,我们只需要针对ABCDE进行全排列,列出每种情况的路径,最后比较所有的路径大小,从而找到最小的路径。
全排列的方法上一篇博客浅析递归,递推和动态规划已经介绍过,我们直接上代码:
als=[]
#记录全排序列
def pa(arr,i,walked):
'''
用于产生全排列数组
arr:待排列数组
i:arr中选中的元素
walked:记录每次选中的元素数组
'''
temp=arr[:]
temp.remove(i)
walked.append(i)
if not temp:
als.append(walked)
return
else:
for el in temp:
wk=walked[:]
pa(temp,el,wk)
为了距离计算方便我们通过给定的坐标先生成一个每个点之间距离的二位数组,如同这个形式:
两点之间距离计算很简单,如给定坐标A(x1,y1),B(x2,y2),那么距离为
d
=
(
x
2
−
x
1
)
2
+
(
y
2
−
y
1
)
2
d=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
d=(x2−x1)2+(y2−y1)2
因此可以写出下面这个程序:
def mes_dis(x1,y1,x2,y2):
'''
计算两个点的距离,保留两位小数
x1,y1:第一个点横纵坐标
x2,y2:第二个点横纵坐标
'''
dx=x2-x1
dy=y2-y1
dis=math.sqrt(dx*dx+dy*dy)
return dis
def dis_vector(location_v):
'''
根据坐标序列生成每两个点之间的距离矩阵
location_v:坐标序列
'''
vector=[]
for i,el1 in enumerate(location_v):
row=[]
for j,el2 in enumerate(location_v):
row.append(mes_dis(el1[0],el1[1],el2[0],el2[1]))
vector.append(row)
return vector
还需要一个函数用于计算给定顺序路径的距离,如给出了OABCDEO,那算出距离,这个简单在得出的距离二位数组中分别找到每段的值相加就行:
def mes_line(vector,line):
'''
计算每个路径的长度
vector:距离矩阵
line:路径
'''
dis=0
pre_index=0
for index in line:
dis=dis+vector[pre_index][index]
pre_index=index
return dis
准备工作完成,下面就是调用上面的函数实现我们的想法了,我把整个代码贴出来:
# -*- coding:utf-8 -*-
import math
#lv=[[2066,2333],[935,1304],[1270,200],[1389,700],[984,2810],[2253,478],[949,3025],[87,2483],[3094,1883],[2706,3130]]
lv=[[0,0],[1,2],[2,1],[2,2],[3,3],[4,2]]
arr=[x for x in range(1,len(lv))]
als=[]
#记录全排序列
def pa(arr,i,walked):
'''
用于产生全排列数组
arr:待排列数组
i:arr中选中的元素
walked:记录每次选中的元素数组
'''
temp=arr[:]
temp.remove(i)
walked.append(i)
if not temp:
als.append(walked)
return
else:
for el in temp:
wk=walked[:]
pa(temp,el,wk)
def mes_dis(x1,y1,x2,y2):
'''
计算两个点的距离,保留两位小数
x1,y1:第一个点横纵坐标
x2,y2:第二个点横纵坐标
'''
dx=x2-x1
dy=y2-y1
dis=math.sqrt(dx*dx+dy*dy)
return dis
def dis_vector(location_v):
'''
根据坐标序列生成每两个点之间的距离矩阵
location_v:坐标序列
'''
vector=[]
for i,el1 in enumerate(location_v):
row=[]
for j,el2 in enumerate(location_v):
row.append(mes_dis(el1[0],el1[1],el2[0],el2[1]))
vector.append(row)
return vector
def mes_line(vector,line):
'''
计算每个路径的长度
vector:距离矩阵
line:路径
'''
dis=0
pre_index=0
for index in line:
dis=dis+vector[pre_index][index]
pre_index=index
return dis
if __name__=="__main__":
vector=dis_vector(lv)
print(vector)
#需要全排的点阵
for i in arr:
pa(arr,i,[])
diss=[]
elMin=[]
disMin=1000000
#遍历每种路径的距离
for el in als:
el.insert(0,0)#将起点加入
el.append(0)#将终点加入
dis=mes_line(vector,el)
#print(f"路径为:{el},距离为:{dis}")
if dis < disMin:
disMin=dis
elMin=el
print("**********穷举算法**********")
print(f"最小路径为:{elMin},距离为:{disMin}")
动态规划
为方便分析,先将点简化为ABC三个,假设距离数组如下:
从O点出发,可以选择A、B、C,一旦选择A以后,再从B、C里面选一个,加入选择了B,那么第三个点只剩下C,最后从C再回到O,这就形成一条路径选择,我将所有选择用一个树状图表示如下:
从图可以分析出:我们要求O经过ABC后的最短距离,记为s(O,{A,B,C}),那么可以有三种方法,选择A点时为:s(A,{B,C})+OA,选择B点时为:s(B,{A,C})+OB,选择C点时为s(C,{A,B})+OC,只需要比较三者值求出最小就可以了;在分解,要求s(A,{B,C}),有两种,选择B点时为:s(B,{C})+AB,选择C点时为s(C,{A})+AC,也是求两者较小值就行了;再分解s(B,{C}),只有一个为:BC+CO。
我们把上面过程用图标表示如下:
第一列表示起点,第一行{},{A}…等表示需要经过的点集,为了后面计算机里面计算方便用二进制来表示点集,ABC分别用一个三位的二进制第1,2,3位分别表示,有则记为1,无则记为0;为了填表我们采用递归的算法来实现。
lv=[[2066,2333],[935,1304],[1270,200],[1389,700],[984,2810],[2253,478],[949,3025],[87,2483],[3094,1883],[2706,3130]]
#lv=[[0,0],[1,2],[2,1],[2,2]]
N=len(lv)-1
nx=2**N
arr=[x for x in range(1,len(lv))]
dps=[[0 for i in range(N+1)] for row in range(nx)]
dp=[[-1 for i in range(N+1)] for row in range(nx)]
#记录全排序列
def convert(arr):
'''
将数组转化为二进制表示
'''
s=0
for a in arr:
s=s|(1<<(a-1))
return s
def find_path(arr,vector,preIndex):
index=convert(arr)#转化为二进制
if dp[index][preIndex]!=-1:#从dp数组里面找,若能找到就返回
return dp[index][preIndex]
if not arr:#arr为空的时候直接返回选中元素到O的距离
return vector[preIndex][0]
min=10000000
minIndex=-1
for i in arr:
temp=arr[:]
temp.remove(i)#选中元素i
disMin=find_path(temp,vector,i)
dis=vector[preIndex][i]+disMin
if dis<min:
min=dis
minIndex=i
dp[index][preIndex]=min#将取到的最小值放到dp中
dps[index][preIndex]=minIndex#将取到最小值的序号放入dps数组中
return min
解释下find_path函数,输入arr是要走的点集,一开始给的是{1,2,3},vector是每两个城市间距离的二位数组,preIndex表示起点,一开始是0;dp用于记录每个起点对应点集的最短距离数组,dps用于记录每个起点对应点集选择最短路径的下一个起点是。
结合之前的过程最终形成的代码如下:
# -*- coding:utf-8 -*-
import math
lv=[[2066,2333],[935,1304],[1270,200],[1389,700],[984,2810],[2253,478],[949,3025],[87,2483],[3094,1883],[2706,3130]]
#lv=[[0,0],[1,2],[2,1],[2,2]]
N=len(lv)-1
nx=2**N
arr=[x for x in range(1,len(lv))]
dps=[[0 for i in range(N+1)] for row in range(nx)]
dp=[[-1 for i in range(N+1)] for row in range(nx)]
#记录全排序列
def convert(arr):
'''
将数组转化为二进制表示
'''
s=0
for a in arr:
s=s|(1<<(a-1))
return s
def find_path(arr,vector,preIndex):
index=convert(arr)#转化为二进制
if dp[index][preIndex]!=-1:#从dp数组里面找,若能找到就返回
return dp[index][preIndex]
if not arr:#arr为空的时候直接返回选中元素到O的距离
return vector[preIndex][0]
min=10000000
minIndex=-1
for i in arr:
temp=arr[:]
temp.remove(i)#选中元素i
disMin=find_path(temp,vector,i)
dis=vector[preIndex][i]+disMin
if dis<min:
min=dis
minIndex=i
dp[index][preIndex]=min#将取到的最小值放到dp中
dps[index][preIndex]=minIndex#将取到最小值的序号放入dps数组中
return min
def mes_dis(x1,y1,x2,y2):
'''
计算两个点的距离,保留两位小数
x1,y1:第一个点横纵坐标
x2,y2:第二个点横纵坐标
'''
dx=x2-x1
dy=y2-y1
dis=math.sqrt(dx*dx+dy*dy)
return dis
def dis_vector(location_v):
'''
根据坐标序列生成每两个点之间的距离矩阵
location_v:坐标序列
'''
vector=[]
for i,el1 in enumerate(location_v):
row=[]
for j,el2 in enumerate(location_v):
row.append(mes_dis(el1[0],el1[1],el2[0],el2[1]))
vector.append(row)
return vector
def mes_line(vector,line):
'''
计算每个路径的长度
vector:距离矩阵
line:路径
'''
dis=0
pre_index=0
for index in line:
dis=dis+vector[pre_index][index]
pre_index=index
return dis
if __name__=="__main__":
vector=dis_vector(lv)
print(vector)
arr=[x for x in range(1,len(lv))]
dis=find_path(arr,vector,0)
y=0#第一列
s=nx-1#最后一行
num=0
line=[]
while True:
point=dps[s][y]#dps里面的点序号
line.append(point)
s=s&(~(1<<(point-1)))#将取出点从集合里面排除
y=point
num=num+1
if num>N-1:
break
line.insert(0,0)
line.append(0)
print("**********dp算法**********")
print(f"最小路径为:{line},距离为:{dis}")
再说明下路径的获取,根据表可以知道的得到的dp表一行最后一列的值就是最短路径,即O到其余点集的最短距离,dps表中该位置处记录了选择的上一个点是什么(最后一个点一定是O),假如上一个点是A,那么我们就需要查以A为起点,{B,C}(排除选中点)的集合的最短距离,找到dps中的这个对应的值,依照此方法再往前找,知道所有需要经过的点都找到,那就是最短路径的走法(程序里面的行和列反了过来,意思一样)。