主题 3: 寻友之旅 - 题解

133 阅读4分钟

当青训营遇上码上掘金

活动链接 juejin.cn/post/718775…

代码见文末

这是一道典型的 BFS 问题,由于小青有两种移动方式,所以我们需要在 BFS 的时候将两种移动方式都考虑进去。

我们可以把小青当前所在的位置看作一个点,用一个队列来存储已经被访问过的点,并且用一个数组来存储每个点到起点的时间。

在 BFS 的过程中,每次取出队列的第一个点,并且将两种移动方式能到达的下一个点加入到队列中,同时更新数组中存储的时间。

当取出的点正好是目标点时, 返回时间

考虑到有的同学还没有了解BFS, 可能不明白为什么这样能够得到正确答案, 这里用一个简单的例子帮助同学们理解

假设小青从n=200出发, 并且不会走同一个点两次

t=1时, 我们能到达的有199, 201, 400

t=2时, 能到达的没走过的点有198, 398, 202, 402, 399, 401, 800

假设t=x时, 到达的没走过点中正好有我们的目标点, 那个x不就是我们要找的最短时间吗?

为什么? 因为我们是从时间由短到长进行遍历的啊, 到达目标点的时间不可能小于x了, x就是到达目标点的最短时间

队列的使用

队列可以类比日常生活中的队列, 先进先出

代码中, 我们先将出发点加入队列

每轮循环从队列首部取出一个点

然后将时间上离该点最近的点都加入队列, 将即1分钟可以到达的点加入队列

这样, 保证了时间距离小的点先入队列, 先出队列, 先被访问到

保证了我们是按时间由短到长进行遍历

为什么不用递归

递归和 BFS 都可以用来搜索图上的路径,但是它们在实现和思想上有很大的不同。

递归的思想是 “自上而下”,它从当前点开始递归,每次调用递归函数都是在深度优先地搜索。递归的优点是简单易懂,缺点是可能会爆栈。

而 BFS 的思想是 “自下而上”,它从起点开始广度优先搜索,每次取出队列中距离最近的点来更新。BFS 的优点是可以保证找到的路径是最短路径,并且不会爆栈。

对于这道题来说,我们需要找到小青从起点到目标点的最短路径,那么 BFS 更适合这种情况。

如果我们使用递归,我们只能深度优先搜索,不能保证找到的路径一定是最短的,并且递归的深度可能会很大,导致爆栈。

代码 - 找到最短时间

⭐代码中dist存的不是空间距离, 而是时间距离, 即dist[i]表示N到i所需的最短时间. 怕同学们误会, 所以解释一下

 package main
 ​
 import "fmt"
 ​
 func minSteps(n int, k int) int {
    // 初始化队列
    var queue []int
    queue = append(queue, n)
 ​
    // 初始化距离数组
    var dist = make([]int, 100001)
    for i := range dist {
       dist[i] = -1
    }
    dist[n] = 0
 ​
    // BFS
    for len(queue) > 0 {
       // 取出队列的第一个点
       cur := queue[0]
       queue = queue[1:]
 ​
       // 步行
       if cur-1 >= 0 && dist[cur-1] == -1 {
          dist[cur-1] = dist[cur] + 1
          queue = append(queue, cur-1)
       }
       if cur+1 < 100001 && dist[cur+1] == -1 {
          dist[cur+1] = dist[cur] + 1
          queue = append(queue, cur+1)
       }
 ​
       // 公交
       if cur*2 < 100001 && dist[cur*2] == -1 {
          dist[cur*2] = dist[cur] + 1
 ​
          queue = append(queue, cur*2)
       }
       // 如果到达目标点,返回距离
       if cur == k {
          return dist[k]
       }
    }
 ​
    return -1
 }
 ​
 func main() {
    n := 3
    k := 11
    fmt.Println(minSteps(n, k))
 }

代码 - 找到最短时间, 并输出最短时间对应的那条路径

用parent数组存储节点的父节点, 这样, 最后就可以从目标点一直找回出发点, 得到路径

 package main
 ​
 import "fmt"
 ​
 func minSteps(n int, k int) (int, []int) {
     // 初始化队列
     var queue []int
     queue = append(queue, n)
 ​
     // 初始化距离数组
     var dist = make([]int, 100001)
     for i := range dist {
         dist[i] = -1
     }
     dist[n] = 0
 ​
     // 初始化父节点数组
     var parent = make([]int, 100001)
     for i := range parent {
         parent[i] = -1
     }
     parent[n] = n
 ​
     // BFS
     for len(queue) > 0 {
         // 取出队列的第一个点
         cur := queue[0]
         queue = queue[1:]
 ​
         // 步行
         if cur-1 >= 0 && dist[cur-1] == -1 {
             dist[cur-1] = dist[cur] + 1
             parent[cur-1] = cur
             queue = append(queue, cur-1)
         }
         if cur+1 < 100001 && dist[cur+1] == -1 {
             dist[cur+1] = dist[cur] + 1
             parent[cur+1] = cur
             queue = append(queue, cur+1)
         }
 ​
         // 公交
         if cur*2 < 100001 && dist[cur*2] == -1 {
             dist[cur*2] = dist[cur] + 1
             parent[cur*2] = cur
             queue = append(queue, cur*2)
         }
 ​
         // 如果到达目标点,返回距离
         if cur == k {
             var path []int
             cur := k
             for cur != n {
                 path = append(path, cur)
                 cur = parent[cur]
             }
             path = append(path, n)
 ​
             // 翻转路径
             for i := 0; i < len(path)/2; i++ {
                 path[i], path[len(path)-i-1] = path[len(path)-i-1], path[i]
             }
             return dist[k], path
         }
     }
     return -1, nil
 }
 ​
 func main() {
     n := 3
     k := 10000
     steps, path := minSteps(n, k)
     fmt.Println("steps:", steps)
     fmt.Println("path:", path)
 }