当青训营遇上码上掘金 | 寻友之旅 (附带Golang实现代码)

101 阅读5分钟

码上掘金完整代码

寻友之旅 - 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)           //输出答案
}
​