目录

迷宫解题(回溯、贪心、蚁群)

题目来源 原博主做了一题多解,这里重写了其中的几个算法

1
2
3
4
5
6
7
8
9
var maze = [][]int{
	{0, 0, 0, 0, 0, 0, 0},
	{0, 0, 0, 1, 0, 1, 0},
	{0, 1, 0, 0, 0, 2, 0},
	{0, 0, 2, 0, 1, 0, 0},
	{0, 1, 1, 0, 0, 0, 0},
	{0, 2, 0, 0, 0, 0, 0},
	{0, 0, 0, 0, 0, 0, 0},
}

要求从(0,0)走到(5,5),值为1的点无法通过,值为2的点必须通过。

回溯算法

通过暴力递归,把所有解遍历出来,取其中的最优解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// Opt可选解
type Opt struct {
	res   [][]int
	coins int
}
// 回溯算法
// result记录所有的解
// minIdx最优解的index
func trackback(maze [][]int, begin, target [2]int, res Opt, result *[]Opt, minIdx *int, maxIter int) {
	// maxIter只用于aco生成部分初始解,回溯算法本身不需要
	if maxIter != -1 && len(*result) >= maxIter {
		return
	}
	// 1 basic case
	if begin[0] == target[0] && begin[1] == target[1] {
		if res.coins == 3 {
			// 这里必须复制
			var newRes Opt
			newRes.res = append([][]int{}, res.res...)
			newRes.coins = res.coins
			*result = append(*result, newRes)
			// 记录最小路径index
			minOpt := (*result)[*minIdx]
			if len(minOpt.res) > len(res.res) {
				*minIdx = len(*result) - 1
			}
		}
		return
	}
	// 2 dfs遍历
	for _, step := range [][]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} {
		nextX := begin[0] + step[0]
		nextY := begin[1] + step[1]
		// 2.1 跳过非法坐标
		if nextX < 0 || nextX >= len(maze) {
			continue
		}
		if nextY < 0 || nextY >= len(maze[nextX]) {
			continue
		}
		// 2.2 跳过无法通过的点
		if maze[nextX][nextY] == 1 {
			continue
		}
		// 2.3 跳过已经走过的点
		has := false
		for _, p := range res.res {
			if p[0] == nextX && p[1] == nextY {
				has = true
				break
			}
		}
		if has {
			continue
		}
		// 2.4 走到合法点并加入路径
		res.res = append(res.res, []int{nextX, nextY})
		// 记录走过值为2的个数
		if maze[nextX][nextY] == 2 {
			res.coins++
		}
		// 2.5 递归
		trackback(maze, [2]int{nextX, nextY}, target, res, result, minIdx, maxIter)
		// 2.6 恢复路径/取消加入路径
		res.res = res.res[:len(res.res)-1]
		if maze[nextX][nextY] == 2 {
			res.coins--
		}
	}
}

func main(){
	var result []Opt
	res := Opt{res: [][]int{{0, 0}}, coins: 0}
	minIdx := 0
	trackback(maze, [2]int{0, 0}, [2]int{5, 5}, res, &result, &minIdx, -1)
	fmt.Println("trackback:")
	fmt.Printf("all soluction: %d, best soluction index: %d\n", len(result), minIdx)
	fmt.Printf("best soluction:%v with steps %d\n", result[minIdx].res, len(result[minIdx].res))
}

结果

1
2
all soluction: 19673, best soluction index: 1874
best soluction:[[0 0] [1 0] [2 0] [3 0] [4 0] [5 0] [5 1] [5 2] [5 3] [4 3] [3 3] [3 2] [2 2] [2 3] [2 4] [2 5] [3 5] [4 5] [5 5]] with steps 19

贪心算法

  • 将起点、终点和三个必经点a\b\c之间分解开来,级联计算和选择每一段的最优解。
  • 从起点开始,计算到a\b\c三点中的哪个路径最短,选为第一跳X
  • 然后再从X开始计算到另外两点,最短的继续选为下一跳Y
  • 重复以上,直到最后一跳终点

注意以下几点:

  • 级联之间需要一个级联的访问矩阵来保证不会重复路径
  • 每次只会选择当前认为的最优解,最终结果可能是一个局部最优解,不是全局最优解
  • 时间复杂度会优于回溯
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
// 贪心算法,通过bfs实现最短路径选择
func bfs(maze [][]int, begin []int, target []int, casVisited [][]bool) ([][][]int, int) {
	var lastStep [][][]int
	// 内部访问矩阵,这个访问矩阵只能保证本次从begin-target之间不重复
	// casVisited级联的访问矩阵,使级联之间路径不重复
	var visited [][]bool
	for i := range maze {
		visited = append(visited, make([]bool, len(maze[i])))
		var line [][]int
		for j := 0; j < len(maze[i]); j++ {
			line = append(line, []int{-1, -1})
		}
		lastStep = append(lastStep, line)
	}
	q := [][]int{begin}
	visited[begin[0]][begin[1]] = true
	steps := 1
	// 出队
	for len(q) != 0 {
		size := len(q)
		for i := 0; i < size; i++ {
			node := q[0]
			x := node[0]
			y := node[1]
			q = q[1:]
			if x == target[0] && y == target[1] {
				return lastStep, steps
			}
			// 遍历
			for _, step := range [][]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} {
				nextX := x + step[0]
				nextY := y + step[1]
				if nextX < 0 || nextX >= len(maze) {
					continue
				}
				if nextY < 0 || nextY >= len(maze[x]) {
					continue
				}
				if visited[nextX][nextY] {
					continue
				}
				if casVisited[nextX][nextY] {
					continue
				}
				if maze[nextX][nextY] == 1 {
					continue
				}
				q = append(q, []int{nextX, nextY})
				visited[nextX][nextY] = true
				lastStep[nextX][nextY] = []int{x, y}
			}
		}
		// 完成一层遍历,步数+1
		steps++
	}
	return lastStep, steps
}

func trace(x, y int, lastStep [][][]int, casVisited [][]bool) {
	if x == -1 && y == -1 {
		return
	}
	now := lastStep[x][y]
	casVisited[x][y] = true
	trace(now[0], now[1], lastStep, casVisited)
}

func main(){
    // 2 贪心算法, lastStep矩阵记录每一步的上一跳,steps记录步数
	// 2.1 (0,0) => 三个金币点(2,5) (3,2) (5,1) 找到steps最短的下一跳,结果是(3,2)
	fmt.Println("greedy:")
	// 级联访问矩阵,保证每次级联不会重复上一次的路径
	var casVisited [][]bool
	for i := range maze {
		casVisited = append(casVisited, make([]bool, len(maze[i])))
	}
	lastStep, steps := bfs(maze, []int{0, 0}, []int{3, 2}, casVisited)
	for _, line := range lastStep {
		fmt.Println(line)
	}
	fmt.Println(steps)
	trace(3, 2, lastStep, casVisited)
	// 2.2 (3,2) => (2,5) (5,1) 找到steps最短的下一跳,结果是(2,5)
	lastStep, steps = bfs(maze, []int{3, 2}, []int{2, 5}, casVisited)
	for _, line := range lastStep {
		fmt.Println(line)
	}
	fmt.Println(steps)
	trace(2, 5, lastStep, casVisited)
	// 2.3 (2,5) => (5,1)
	lastStep, steps = bfs(maze, []int{2, 5}, []int{5, 1}, casVisited)
	for _, line := range lastStep {
		fmt.Println(line)
	}
	fmt.Println(steps)
	trace(5, 1, lastStep, casVisited)
	// 2.4 (5,1) => (5,5) 结束
	lastStep, steps = bfs(maze, []int{5, 1}, []int{5, 5}, casVisited)
	for _, line := range lastStep {
		fmt.Println(line)
	}
	fmt.Println(steps)
	trace(5, 5, lastStep, casVisited)
	for _, line := range casVisited {
		fmt.Println(line)
	}
	// 2.5 三个lastStep按从(5,5)回溯到(0,0)就是最短路径
}

结果,需要23步,但是一个局部最优解

蚁群算法

  • 通过回溯生成一定数量的初始解(不一定会包含全局最优解),这些初始解就作为蚂蚁自由选择的路径
  • 将这批初始解重叠起来,每个点的权重(信息素)每重叠一次加1,生成信息素矩阵
  • 最后从起始点开始选择附近权重(信息素)最大路径前进,得出最优解

最后的结果和初始解的数量和质量都有关系,所以选择初始解的策略需要重点优化

结果也不一定是全局最优解,是近似解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func ant() {
	// 1 先通过回溯算法生成一定数量的初始解(不一定含最优解)
	var result []Opt
	res := Opt{res: [][]int{{0, 0}}, coins: 0}
	minIdx := 0
	// 生成1000个初始解
	trackback(maze, [2]int{0, 0}, [2]int{5, 5}, res, &result, &minIdx, 1000)
	fmt.Printf("generate %d solves\n", len(result))
	// 2 将初始解重叠生成信息素矩阵
	var antInfo [][]int
	for i := range maze {
		antInfo = append(antInfo, make([]int, len(maze[i])))
	}
	// 2.1 随机挑选部分路径生成信息素矩阵
	used := make(map[int]struct{})
	for len(used) < 200 {
		idx := rand.Intn(len(result))
		if _, exists := used[idx]; exists {
			continue
		}
		used[idx] = struct{}{}
		for _, node := range result[idx].res {
			antInfo[node[0]][node[1]] += 1
		}
	}
	// 3 信息素矩阵按照数值大小遍历生成近似解
	for _, line := range antInfo {
		fmt.Println(strings.Join(func() []string {
			var tmp []string
			for _, d := range line {
				tmp = append(tmp, fmt.Sprintf("%d", d))
			}
			return tmp
		}(), "\t"))
	}
}

func main(){
    ant()
}

结果,这里从1000个初始解里面随机挑选了200个来生成信息素矩阵

1
2
3
4
5
6
7
8
9
generate 1000 solves
信息素矩阵:
200     68      144     144     160     79      79
200     68      144     0       97      0       79
200     0       200     74      155     200     133
200     0       200     200     0       167     142
200     0       0       188     137     146     164
200     200     199     185     160     200     139
145     200     136     121     125     114     96

总结

在可以穷举的情况下建议还是使用回溯、动态规划等暴力解法

在没办法穷举的时候,可以尝试蚁群、退火、遗传算法等

这些解法的基础仍然是暴力解法