当青训营遇上码上掘金
寻友之旅
题意
小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
请帮助小青通知小码,小青最快到达时间是多久?
输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)
案例1:
输入:10 11
输出:1
解析:从地点为10的位置步行一分钟到11
案例2:
输入:100 205
输出:4
解析:从地点为100的位置步行两分钟到102,再坐公交到204,再步行一分钟到达205
案例3:
输入:100 195
输出:4
解析:从地点为100的位置步行两分钟到98,再坐公交到196,再步行一分钟到达195
题解
从题意可以看到需要求最短时间,可以想到一些求最值的一些算法,如深度优先搜索,广度优先搜索,动态规划,贪心等思想。本题解将采用广度优先搜索的算法模型来求解。
广度优先搜索
广度优先搜索也称宽度优先搜索,缩写BFS,是连通图的一种遍历策略。因为它的思想是从一个顶点开始,辐射状地优先遍历其周围较广的区域,因此得名。按照据开始状态由远及近的顺序进行搜索,因此可以很容易用来求最短路径,最少操作之类的问题。
题意解析
这题就是一维的BFS,由于题目要求从小码的位置坐标作为起点N,从起点N出发,将从该点的三种可能路径进行枚举,并判断是否为小青为位置K,也就是终点,是的话就将直接跳出枚举循环。由于该题目的背景一定能够有最终解(一步一步走也一定能到小青的位置)。因此在循环中暴搜所有情况即可。这里枚举的情况有限都在[0,100000]里,所以最多搜索100000次,不会TLE。
实现细节
- 为了避免重复枚举(比如从1步行到4,和从2坐公交到4),我们将采用set数据结构来进行标记(示例代码使用go语言,用map实现set),保证不重复枚举。
- BFS按照顺序从小到大枚举,符合先进先出的特点,代码上使用队列数据结构来枚举从小到大的坐标点。
- 在枚举的大循环上,采用一个time变量来记录分钟数,将递增的某一分钟数的所有可到达的坐标点进行枚举,并将下一分钟可到达的坐标点添加到队列中,形成新的队列元素,直到到达小码的坐标点位置,结束循环。
- 确定边界情况,由于题目要求N和K的范围为[0,100000],因此要去除掉超过边界的坐标点。例如从坐标0的位置步行到-1的位置是不可行的,从坐标70000坐公交到140000也是不可行的。
go代码
package main
import (
"fmt"
)
func main() {
var N, K, time int
const MAX = 100000
//获取小青和小码的位置坐标
if _, err := fmt.Scanf("%d%d", &N, &K); err != nil {
fmt.Println(err)
return
}
//map中占位
obj := struct{}{}
//小青和已经到小码家
if N == K {
fmt.Println(0)
return
}
//记录已经能够到达的坐标点
mm := make(map[int]interface{})
//使用切片模拟队列
queue := make([]int, 0, 50000)
mm[N] = obj
queue = append(queue, N)
flag := false
for {
//时间加一分钟
size := len(queue)
time++
for i := 0; i < size; i++ {
now := queue[0]
queue = queue[1:]
//三种选择
temp := [3]int{now - 1, now + 1, now + now}
for _, next := range temp {
//越界抛弃
if next > MAX || next < 0 {
continue
}
//在更短时间内已经到达过该点了
if _, ok := mm[next]; ok {
continue
}
//加入队列枚举
queue = append(queue, next)
//标记到达过的点
mm[next] = obj
//第一次到达小码家
if next == K {
flag = true
break
}
}
}
if flag {
break
}
}
fmt.Println(time)
}
主题进阶
在上面的步骤中可以求出从起点N到终点K的最短时间了,那么,为了使小青能够更加清楚如何在每一分钟选择合适的交通方式和请进方向,你能够帮助小青找到每一分钟到达的坐标点吗?这样能够方便小青在前往小码的路上能够准确无误的采取时间最短的交通方式。
题解要点
- 需要记录到达某一坐标点的是从一分钟前哪一个坐标点移动过来的
- 需要能够从终点倒推回起点坐标并输出,这是一个倒序输出的过程,符合先进后出的数据结构特点,采用栈来记录(go没有栈的具体实现,使用切片来模拟实现一个线性栈)
- 为了记录某一坐标点是从哪一个坐标点移动过来的,将上一部分题解中的set改为map,键为坐标点,值为上一坐标点。
代码修改要点
mm := make(map[int]interface{}) --> mm := make(map[int]int)mm[next] = obj --> mm[next] = now- 从终点倒推回起点,并使用切片记录起来
- 将切片中的内容输出
go代码
package main
import (
"fmt"
)
func main() {
var N, K, time int
const MAX = 100000
//获取小青和小码的位置坐标
if _, err := fmt.Scanf("%d%d", &N, &K); err != nil {
fmt.Println(err)
return
}
//小青和已经到小码家
if N == K {
fmt.Println(0)
return
}
//记录已经能够到达的坐标点
mm := make(map[int]int)
//使用切片模拟队列
queue := make([]int, 0, 50000)
mm[N] = N
queue = append(queue, N)
flag := false
for {
//时间加一分钟
size := len(queue)
time++
for i := 0; i < size; i++ {
now := queue[0]
queue = queue[1:]
//三种选择
temp := [3]int{now - 1, now + 1, now + now}
for _, next := range temp {
//越界抛弃
if next > MAX || next < 0 {
continue
}
//在更短时间内已经到达过该点了
if _, ok := mm[next]; ok {
continue
}
//加入队列枚举
queue = append(queue, next)
//标记到达过的点
mm[next] = now
//第一次到达小码家
if next == K {
flag = true
break
}
}
}
if flag {
break
}
}
fmt.Println(time)
temp := time - 1
//切片模拟顺序栈
stack := make([]int, time)
for K != N {
stack[temp] = K
temp--
K = mm[K]
}
//输出路线
fmt.Printf("%v", N)
for i := 0; i < time; i++ {
fmt.Printf(" --> %v", stack[i])
}
}