码上掘金完整代码
寻友之旅 - Golang实现:code.juejin.cn/pen/7200106…
C++实现请参考文末
题目描述
小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。 步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1 公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
请帮助小青通知小码,小青最快到达时间是多久? 输入: 两个整数 N 和 K 输出: 小青到小码家所需的最短时间(以分钟为单位)
题目拆解
可以抽象成一维线上的两个点A、B(A < B)
A点每次可以向前或向后走一步(+1/-1),同时也可以到当前自己下标的2倍(2A)
问最少经过多少次A可以抵达B
初始代码
struct Node
{
int cnt; // 当前步数
int x; // 当前位置
Node(int cnt, int x):cnt(cnt),x(x){};
};
queue<Node> q;
int n, k;
int ans = INF;
void bfs()
{
while(!q.empty())
{
Node now = q.front();
q.pop();
// 恰好
if(now.x == k)
{
ans = min(ans, now.cnt);
continue;
}
// 回退
if(now.x > k)
{
ans = min(ans, now.cnt + now.x - k);
continue;
}
q.push(Node(now.cnt + 1, now.x * 2));
q.push(Node(now.cnt + 1, now.x + 1));
}
}
void solve()
{
cin >> n >> k;
q.push(Node(0, n));
bfs();
printf("%d\n", ans);
return;
}
我使用了广度优先搜索(BFS)来做这道题,这是第一版代码。
其中,Node结构体中的cnt和x分别表示当前步数和当前位置,我们的队列将会使用Node结构体来完成算法操作。
我们不断的从队列中取出点,判断一下是否刚好是目标/超出目标,是的话则进行答案更新,不是的话则加入两个新的点:向前走或向后走。
优化
逻辑纠错
可以发现,我在代码里面,对队列push进去的新的点永远比旧的点来的大
那么我们来考虑一下这个例子:N = 5, K = 8,你觉得最优解是多少呢?
是2!,我们首先退回去一步,然后再坐公交车!
我们原先的代码已经出现了大问题,这份代码是不会AC的!
我们只好加上去这个代码:q.push(Node(now.cnt + 1, now.x - 1));
但是我们发现,加上去后代码要跑完花了很久很久
合并“恰好”与“回退”
参考我的第一版代码,我们可以注意到我有两个if,分别判断恰好到达终点与超过终点的情况。由于超过终点不能再坐公交车,我们只能步行回去,
但是请注意这两个if里面的代码,我们以“回退”为例,由于只能往后退,那么我们答案只需要改成now.cnt + now.x - k,也就是步数从当前步数再加上回退的距离就是我们的答案
那么,“恰好”的时候,now.x == k是不是也成立?
所以我们实际上只需要把两个if合并为这个if:
// 计算答案
if(now.x >= k)
{
ans = min(ans, now.cnt + now.x - k);
continue;
}
剪枝 - 去除对答案无贡献的点
搜索相当于展开一个树,我们可以考虑对一些没必要的“枝条”进行剪枝处理
例如,如果当前这个Node的cnt已经比ans来的大了或者刚好,那么我们就可以直接pop掉这个Node。
因为,即使留着它,它也对ans没有任何贡献:不论是计算答案(ans不变),还是继续加入新的点(cnt > ans,后续计算答案的时候ans也不会更新),ans都是不会变的
所以,我们在原始代码中的q.pop()后,加入下述代码
while(now.cnt >= ans && q.size())
{
now = q.front();
q.pop();
}
其中,q.size()是为了防止出现内存错误,我们总不可能让一个空的队列Front一个点吧?
剪枝 - 去除越走越长的点
我们可以注意到,公交车的这个两倍距离,是一把达摩克利斯之剑:我们可以让自己前进很多,也可以让自己坐过站很多
我们考虑下面这个情况:N = 6, K = 8
这时候,我们还需要往队列里加上坐公交车后的点吗?
完全不需要!因为坐上公交车后,需要往回退四步!
我们可以经过一些样例推测,可以发现,当回退距离大于前进距离的时,也就是now.x * 2 >= 2 * (k - now.x)时,我们不需要再坐公交车了
化简后可以得2 * now.x <=k
也就是说,实际上我们两倍距离只要大于k,我们就不需要再加进队列了
ps:这与我第一次看到这道题的猜测一致!因为实际上当两倍距离大于k后,我们总是可以找到不用超过k且用上2倍距离的办法——自己先往后走点
最终代码
struct Node
{
int cnt; // 当前步数
int x; // 当前位置
Node(int cnt, int x):cnt(cnt),x(x){};
};
queue<Node> q;
int n, k;
int ans = INF;
void bfs()
{
while(!q.empty())
{
Node now = q.front();
q.pop();
while(now.cnt >= ans && q.size())
{
now = q.front();
q.pop();
}
// 计算答案
if(now.x >= k)
{
ans = min(ans, now.cnt + now.x - k);
continue;
}
q.push(Node(now.cnt + 1, now.x - 1));
q.push(Node(now.cnt + 1, now.x + 1));
if(now.x * 2 <= k) q.push(Node(now.cnt + 1, now.x * 2));
}
}
void solve()
{
cin >> n >> k;
q.push(Node(0, n));
bfs();
printf("%d\n", ans);
return;
}
Golang的实现
由于目前意向后端,这里贴一份我自己的golang实现,其中Queue模板参考了网上资料
package main
import (
"fmt"
)
/* ——————Queue模板—————— */
type (
// Queue 队列
Queue struct {
top *node
rear *node
length int
}
// 双向链表节点
node struct {
pre *node
next *node
value interface{}
}
)
// Create a new queue
func NewQueue() *Queue {
return &Queue{nil, nil, 0}
}
// 获取队列长度
func (this *Queue) Size() int {
return this.length
}
// 返回true队列不为空
func (this *Queue) Empty() bool {
return this.length <= 0
}
// 返回队列顶端元素
func (this *Queue) Front() interface{} {
if this.top == nil {
return nil
}
return this.top.value
}
// 入队操作
func (this *Queue) Push(v interface{}) {
n := &node{nil, nil, v}
if this.length == 0 {
this.top = n
this.rear = this.top
} else {
n.pre = this.rear
this.rear.next = n
this.rear = n
}
this.length++
}
// 出队操作
func (this *Queue) Pop() interface{} {
if this.length == 0 {
return nil
}
n := this.top
if this.top.next == nil {
this.top = nil
} else {
this.top = this.top.next
this.top.pre.next = nil
this.top.pre = nil
}
this.length--
return n.value
}
/* ——————Queue模板—————— */
var (
q *Queue
n, k, ans int
)
type Node struct {
cnt int // 当前步骤数
x int // 当前位置
}
func bfs() {
for {
// 如果队列为空则退出
if q.Empty() {
break
}
now := q.Pop().(Node) // 取出队首元素
// 优化:如果当前步数大于等于答案,那么后面的步数也不会比答案小,因此可以直接弹出
for now.cnt >= ans && !q.Empty() {
now = q.Pop().(Node)
}
// 优化:对答案无贡献
if now.cnt >= ans {
break
}
// 计算答案
if now.x >= k {
if ans > now.cnt+now.x-k {
ans = now.cnt + now.x - k
}
continue
}
q.Push(Node{cnt: now.cnt + 1, x: now.x + 1}) // 向前走
q.Push(Node{cnt: now.cnt + 1, x: now.x - 1}) // 向后走
// 优化:乘坐公交车超过答案则步数不是最优解
if now.x*2 <= k {
q.Push(Node{cnt: now.cnt + 1, x: now.x * 2}) // 坐公交车
}
}
}
func Init() {
n = 2
k = 5
}
func main() {
ans = 1061109567 // 将答案设置为极大值
q = NewQueue() // 初始化队列对象
Init() // 初始化n和k,由于码上掘金不支持fmt.Scanf()操作,只能在程序启动时设置n和k
q.Push(Node{0, n}) // 将起点入队
bfs() // BFS搜索
fmt.Println(ans) //输出答案
}