• 【算法设计与分析】基于Go语言实现动态规划法解决TSP问题


     本文针对于最近正在学习的Go语言,以及算法课实验所需内容进行Coding,一举两得!     

    一、前言 

            由于这个实验不要求向之前的实验一样做到那种连线的可视化,故可以用图形界面不那么好实现的语言进行编写,考虑到Go语言的方兴未艾,所以采用此种语言解决问题。


     二、问题

            TSP问题的大致解法,老师在课上已经说过了,清华大学出版社的《算法设计与分析》(第二版,然而书上伪代码存在一些疏漏)里面也有所阐述,这里不做细致解释。


    三、代码分析

            主要可分为三个部分,输入、输出、计算。

    1.输入

            输入部分需要一个整型变量存点(城市)的数量,一个矩阵存点到点的距离,另外增设一个矩阵存到某个点的最近的前驱。这里还有一个重要的问题是如何做出这些点的子集,也就是所要画的图(实验结果)的表头横向内容,代码如下:

    1. ·········
    2. var nums int
    3. // 读取二维数组的行数和列数
    4. fmt.Print("请输入(城市)点数: ")
    5. fmt.Scanln(&nums)
    6. // 初始化一个二维数组
    7. arc := make([][]int, nums)
    8. for i := range arc {
    9. arc[i] = make([]int, nums)
    10. }
    11. // 从控制台读取二维数组的值
    12. fmt.Println("请输入二维数组的元素,每行输入完毕后按回车键:")
    13. for i := 0; i < nums; i++ {
    14. for j := 0; j < nums; j++ {
    15. var putin int
    16. fmt.Scanf("%d", &putin)
    17. if putin == 0 {
    18. putin = 2004
    19. }
    20. arc[i][j] = putin
    21. }
    22. fmt.Scanln() // 跳过每行输入后的换行符
    23. }
    24. var LengthOfd int = int(math.Pow(2, float64(nums-1)))
    25. //下面的这个d就是那个动态规划法的表
    26. d := make([][]int, nums)
    27. for i := range d {
    28. d[i] = make([]int, LengthOfd)
    29. }
    30. //同样设置一个跟d一样的矩阵,来存最近的前驱
    31. front := make([][]int, nums)
    32. for i := range front {
    33. front[i] = make([]int, LengthOfd)
    34. }
    35. //初始化设置成很大的
    36. for i := range d {
    37. for j := range d[i] {
    38. d[i][j] = 2004
    39. front[i][j] = 2004
    40. }
    41. }
    42. for i := 0; i < nums; i++ {
    43. d[i][0] = arc[i][0]
    44. front[i][0] = 0
    45. }
    46. // 创建一维存所有要做子集的点。
    47. numName := make([]int, nums) //生成1,2,3
    48. for i := 1; i <= nums; i++ {
    49. numName[i-1] = i
    50. }
    51. numName = numName[:len(numName)-1]
    52. subset := subsets(numName) //生成子集
    53. sort.Slice(subset, func(i, j int) bool {
    54. return len(subset[i]) < len(subset[j])
    55. })
    56. fmt.Println(subset)
    57. ·······

            这里前面几个变量我不加以赘述,简单的创建和初始化(如果你用的其他语言写这道题,相信你能做到这个),这里说一下最小子集的寻找,我参考了LeetCode上一道题(力扣上的子集问题)以及CSDN上相关博主给出的解答(子集问题的GO语言其中一解,这里选择解题代码并未考虑时间及空间复杂度,大家可以试着采用leetcode上更快更好的代码),代码大意是采用了深度优先搜索的方式进行生成,下面是本题借用函数:

    1. //来自于本站其他用户博文
    2. func subsets(nums []int) [][]int {
    3. l := list.New()
    4. result := list.New()
    5. for i := 0; i <= len(nums); i++ {
    6. dfs(nums, 0, i, l, result)
    7. }
    8. arr := make([][]int, result.Len())
    9. k := 0
    10. for e := result.Front(); e != nil; e = e.Next(){
    11. curl := e.Value.(*list.List)
    12. arr[k] = make([]int, curl.Len())
    13. k++
    14. }
    15. i := 0;
    16. for e := result.Front(); e != nil; e = e.Next() {
    17. curl := e.Value.(*list.List)
    18. j := 0
    19. for p := curl.Front(); p != nil; p = p.Next() {
    20. arr[i][j] = p.Value.(int)
    21. j++
    22. }
    23. i++;
    24. }
    25. return arr
    26. }
    27. func dfs(nums []int, start int, len int, l *list.List, result *list.List) {
    28. if start == len {
    29. a := list.New()
    30. for e := l.Front(); e != nil; e = e.Next() {
    31. a.PushBack(e.Value)
    32. }
    33. result.PushBack(a)
    34. return
    35. }
    36. for i := start; i < len; i++ {
    37. l.PushBack(nums[i])
    38. dfs(nums, i+1, len, l, result)
    39. b := l.Back()
    40. l.Remove(b)
    41. }
    42. }

             之后就是对得到的子集排序,因为上面代码跑出来的子集并非是有序的。

            这样便得到了计算所需的所有变量。

    2.计算 

            计算这个方面可以参考书中的伪代码,值得注意的是在判断大小的一些地方需要改变传参。大家可通过书中样例矩阵以及表格进行相关推导,不难发现第0列被初始化,第1到3列依靠0列更新,3到5列依靠1到3列更新,第七列特别只有第0行需要更新,而我们的最终结果需要的表格的横向表头的二维数组长这样:

            所以,我们不难发现,它的长度似乎和计算有一些联系:在遍历{1}的时候,填充2和3,依靠0到1的数值,所以计算d[2][1] = arc[2][1] + d[1][0],d[3][1] = arc[3][1] + d[1][0]。依照此种规律,便可结合下文的代码内容分析:

    1. //下面才刚刚开始tsp
    2. for j := 1; j < len(subset); j++ { //以d中的列开始扫描,也就是上面的subset(V),表头内容
    3. for i := 1; i < nums; i++ { //挨个查找看看那个数字没在V的里面,真没在就去赋值,正在查找横表头有无
    4. judge := false
    5. for _, theNum := range subset[j] {
    6. if theNum == i {
    7. judge = true //在的在的,就不用操作了
    8. continue
    9. }
    10. }
    11. if judge == false { //wok,真没在
    12. var edge int = 1314
    13. for _, theNum := range subset[j] { //theNum,表头中含有的内容
    14. temp := arc[i][theNum] + d[theNum][j-theNum-len(subset[j])+1]
    15. if temp < edge {
    16. edge = temp
    17. d[i][j] = edge
    18. front[i][j] = theNum
    19. }
    20. //d[i][j] = int(math.Min(float64(d[i][j]), float64(arc[i][theNum]+d[theNum][j-1])))
    21. }
    22. }
    23. }
    24. }
    25. //求解最终结果
    26. TheLastEdge := 520
    27. for k := 1; k < nums; k++ {
    28. temp := arc[0][k] + d[k][LengthOfd-1-int(math.Pow(2, float64(k-1)))]
    29. if temp < TheLastEdge {
    30. TheLastEdge = temp
    31. d[0][LengthOfd-1] = TheLastEdge
    32. front[0][LengthOfd-1] = k
    33. }
    34. }

             最后求解最后的那个框,例如本题目就是{1,2,3}下面的第0行的内容。和上面写到的,平常的点求该问题的解法如出一辙,这里并不是很好理解,计算公式甚至可以理解成是我在纯粹的找规律凑数。值得注意的是,在计算的同时front矩阵也在记录他们的上一个点(前驱),这个在后面输出发挥着重要作用。

    3.输出

            这里要注意以下路径是怎么打印出来的。i是直接指向最后更新的那个框框的纵坐标,j是横坐标,count用来计数确保不会在移动的途中迷路,根据书上表格,我们并不难发现下面的规律,j直接取front中的值会直接得到前驱节点的值,那么我们就应该在j行去定位这个前驱的下一个前驱,那么我们就只差寻找这个地方的纵坐标,相当巧合的是,现在的i减去现在的j的值再减去这次遍历计数器的值,正好到了我们要寻找的那个地方。(这里在草稿纸上列出我们需要加起来的点,恰好可以得出普适的规律)。

    1. fmt.Print("TSP最短的路径是:", "0")
    2. //打印路径
    3. for i, j, count := LengthOfd-1, 0, 0; ; {
    4. j = front[j][i]
    5. i = i - j - count
    6. fmt.Print("->", j)
    7. count++
    8. if i <= 0 {
    9. fmt.Println("->0")
    10. break
    11. }
    12. }
    13. //画表
    14. for i := 0; i < len(subset); i++ {
    15. str := fmt.Sprint(subset[i])
    16. fmt.Printf("%10s", str)
    17. //fmt.Print(subset[i], "\t\t")
    18. }
    19. fmt.Println()
    20. for i := 0; i < nums; i++ {
    21. for j := 0; j < LengthOfd; j++ {
    22. bye := "-"
    23. if d[i][j] == 2004 {
    24. fmt.Printf("%10s", bye)
    25. } else {
    26. fmt.Printf("%10d", d[i][j])
    27. }
    28. }
    29. fmt.Println()
    30. }
    31. fmt.Println("最短路径是:", d[0][LengthOfd-1])

            大致就是这样得到了最后结果,然后再对齐一下表格。


    四、代码

    1. // TSP Problem
    2. // Created By DDD on 2024.5.25
    3. //
    4. package main
    5. import (
    6. "container/list"
    7. "fmt"
    8. "math"
    9. "sort"
    10. )
    11. func subsets(nums []int) [][]int {
    12. l := list.New()
    13. result := list.New()
    14. for i := 0; i <= len(nums); i++ {
    15. dfs(nums, 0, i, l, result)
    16. }
    17. arr := make([][]int, result.Len())
    18. k := 0
    19. for e := result.Front(); e != nil; e = e.Next() {
    20. curl := e.Value.(*list.List)
    21. arr[k] = make([]int, curl.Len())
    22. k++
    23. }
    24. i := 0
    25. for e := result.Front(); e != nil; e = e.Next() {
    26. curl := e.Value.(*list.List)
    27. j := 0
    28. for p := curl.Front(); p != nil; p = p.Next() {
    29. arr[i][j] = p.Value.(int)
    30. j++
    31. }
    32. i++
    33. }
    34. return arr
    35. }
    36. func dfs(nums []int, start int, len int, l *list.List, result *list.List) {
    37. if start == len {
    38. a := list.New()
    39. for e := l.Front(); e != nil; e = e.Next() {
    40. a.PushBack(e.Value)
    41. }
    42. result.PushBack(a)
    43. return
    44. }
    45. for i := start; i < len; i++ {
    46. l.PushBack(nums[i])
    47. dfs(nums, i+1, len, l, result)
    48. b := l.Back()
    49. l.Remove(b)
    50. }
    51. }
    52. func main() {
    53. var nums int
    54. // 读取二维数组的行数和列数
    55. fmt.Print("请输入(城市)点数: ")
    56. fmt.Scanln(&nums)
    57. // 初始化一个二维数组
    58. arc := make([][]int, nums)
    59. for i := range arc {
    60. arc[i] = make([]int, nums)
    61. }
    62. // 从控制台读取二维数组的值
    63. fmt.Println("请输入二维数组的元素,每行输入完毕后按回车键:")
    64. for i := 0; i < nums; i++ {
    65. for j := 0; j < nums; j++ {
    66. var putin int
    67. fmt.Scanf("%d", &putin)
    68. if putin == 0 {
    69. putin = 2004
    70. }
    71. arc[i][j] = putin
    72. }
    73. fmt.Scanln() // 跳过每行输入后的换行符
    74. }
    75. var LengthOfd int = int(math.Pow(2, float64(nums-1)))
    76. //下面的这个d就是那个动态规划法的表
    77. d := make([][]int, nums)
    78. for i := range d {
    79. d[i] = make([]int, LengthOfd)
    80. }
    81. //同样设置一个跟d一样的矩阵,来存最近的前驱
    82. front := make([][]int, nums)
    83. for i := range front {
    84. front[i] = make([]int, LengthOfd)
    85. }
    86. //初始化设置成很大的
    87. for i := range d {
    88. for j := range d[i] {
    89. d[i][j] = 2004
    90. front[i][j] = 2004
    91. }
    92. }
    93. for i := 0; i < nums; i++ {
    94. d[i][0] = arc[i][0]
    95. front[i][0] = 0
    96. }
    97. // 创建一维存所有要做子集的点。
    98. numName := make([]int, nums) //生成1,2,3
    99. for i := 1; i <= nums; i++ {
    100. numName[i-1] = i
    101. }
    102. numName = numName[:len(numName)-1]
    103. subset := subsets(numName) //生成子集
    104. sort.Slice(subset, func(i, j int) bool {
    105. return len(subset[i]) < len(subset[j])
    106. })
    107. fmt.Println(subset)
    108. //下面才刚刚开始tsp
    109. for j := 1; j < len(subset); j++ { //以d中的列开始扫描,也就是上面的subset(V),表头内容
    110. for i := 1; i < nums; i++ { //挨个查找看看那个数字没在V的里面,真没在就去赋值,正在查找横表头有无
    111. judge := false
    112. for _, theNum := range subset[j] {
    113. if theNum == i {
    114. judge = true //在的在的,就不用操作了
    115. continue
    116. }
    117. }
    118. if judge == false { //wok,真没在
    119. var edge int = 1314
    120. for _, theNum := range subset[j] { //theNum,表头中含有的内容
    121. temp := arc[i][theNum] + d[theNum][j-theNum-len(subset[j])+1]
    122. if temp < edge {
    123. edge = temp
    124. d[i][j] = edge
    125. front[i][j] = theNum
    126. }
    127. //d[i][j] = int(math.Min(float64(d[i][j]), float64(arc[i][theNum]+d[theNum][j-1])))
    128. }
    129. }
    130. }
    131. }
    132. //求解最终结果
    133. TheLastEdge := 520
    134. for k := 1; k < nums; k++ {
    135. temp := arc[0][k] + d[k][LengthOfd-1-int(math.Pow(2, float64(k-1)))]
    136. if temp < TheLastEdge {
    137. TheLastEdge = temp
    138. d[0][LengthOfd-1] = TheLastEdge
    139. front[0][LengthOfd-1] = k
    140. }
    141. }
    142. fmt.Print("TSP最短的路径是:", "0")
    143. //打印路径
    144. for i, j, count := LengthOfd-1, 0, 0; ; {
    145. j = front[j][i]
    146. i = i - j - count
    147. fmt.Print("->", j)
    148. count++
    149. if i <= 0 {
    150. fmt.Println("->0")
    151. break
    152. }
    153. }
    154. //画表
    155. for i := 0; i < len(subset); i++ {
    156. str := fmt.Sprint(subset[i])
    157. fmt.Printf("%10s", str)
    158. //fmt.Print(subset[i], "\t\t")
    159. }
    160. fmt.Println()
    161. for i := 0; i < nums; i++ {
    162. for j := 0; j < LengthOfd; j++ {
    163. bye := "-"
    164. if d[i][j] == 2004 {
    165. fmt.Printf("%10s", bye)
    166. } else {
    167. fmt.Printf("%10d", d[i][j])
    168. }
    169. }
    170. fmt.Println()
    171. }
    172. fmt.Println("最短路径是:", d[0][LengthOfd-1])
    173. }
    174. /*
    175. 4
    176. 0 3 6 7
    177. 5 0 2 3
    178. 6 4 0 2
    179. 3 7 5 0
    180. */

    五、参考文献

     算法分析与设计课程实验——TSP问题与01背包问题的动态规划算法实现-CSDN博客


    Go的主函数不需要写return

  • 相关阅读:
    一条 SQL 语句是如何执行的
    Skywalking全部
    【MySQL】视图操作
    一文理解 Docker 的 ENTRYPOINT、CMD 和 k8s 的 command、args
    部署LVS-DR群集
    浅析MVC、MVP、MMVM 架构
    用C++ Thread实现简单的socket多线程通信
    ADB命令
    产品经理应具备的能力
    DVWA-XSS(DOM)Low/Medium/High低中高级别
  • 原文地址:https://blog.csdn.net/FellAveal/article/details/139202813