当青训营遇上码上掘金主题三"寻友之旅"(贪心)

108 阅读5分钟

主题 3:寻友之旅

小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)

请帮助小青通知小码,小青最快到达时间是多久?
输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)

作者:青训营官方账号
链接:juejin.cn/post/717498…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


小明看到这一题,非常高兴

小明:这个套路我熟,每次只能走三条路,那我就穷举这三条路的所有可能,走到目标的时候,我对已知最小的路径进行一次更新,美滋滋。

小明:我是算法小白,所以时间复杂度这些,我就不在乎了。

func main() {
   ops := make([]func(int) int, 3)
   ops[0] = func(i int) int {
      return i + 1
   }
   ops[1] = func(i int) int {
      return i - 1
   }
   ops[2] = func(i int) int {
      return 2 * i
   }
   recursive(5, 38, 0, ops)
   fmt.Println(path)
}

var path int

func recursive(N, K, step int, ops []func(int) int) {
   if N > K || N < 0 {
      return
   }

   if N == K {
      if path > step {
         path = step
      }
      return
   }

   for _, v := range ops {
      recursive(v(N), K, step+1, ops)
   }
}

小明的代码喜提StackOverflow,为什么这道题这么做不行呢?

小明总结出:可以使用递归穷举的,是有限状态机。递归本身是一个向更多贪心的算法,所以题目中可能存在这种情况:由于可以向前走,向后走,所以小明可能一直在终点附近转圈,也就是这个问题,有无数种从N到K的可能性,这些可能性构成的所有状态机数目是无限的,递归自然无法处理无限状态机。

如果是有限状态机,使用递归进行穷举后,可以进一步用记忆化搜索优化,最后还可以优化为非递归的动态规划问题。

所以,我们要对无限的情况进行剪枝。通过增加约束条件,我相信也会有递归的解法。但是如此,为什么不换一种思路呢?递归是向着"更多"贪心,那么我们的算法应该向着"更少"贪心才对。

小红的思路是用bfs,使用bfs也是穷举可能性,但是却有一个向 "更少" 贪心的思想。

这一步体现了贪心思想,也是寻路问题的松弛策略:只有迈出这一步后,我的步数更小,才更新,否则不更新。

如何保证贪心可以传递呢? 如果要使用贪心算法解题,必须要保证的是,小问题的贪心,可以推出大问题的最终结果正确,这意味着小的贪心要能够传递。

贪心算法往往难在严格的证明上,即如何证明贪心策略是传递的。对于这个问题,我们显然可以看出

  • 仅当第一次访问某个结点时,该节点路径最短。正是因为本算法引入了visited数组,所以可以避免上面递归的无限左右横跳导致StackOverflow的情况。
  • 如果从已经visited的结点出发,经过一个step(不管怎么走),下一个到达的地方,肯定也是最优的(这个显然,因为从一个已经最少步数的走一步直接到下一个位置,总比来来回回反复横跳的步数少)。
  • 我们把算法分为一层一层的看(本来就是bfs),首先N这个结点就是最优的,然后从N扩展到3N个结点,3N个结点扩展到9N,当然中途会经历剪枝过程。我们可以绘制出一颗有限状态机构成的树。
  • 因此,从第一层开始,到第N层,能保证每一次扩展都是从visited结点开始,这意味着,每一层的扩散是最优的,并且这个最优随着层的传递而传递,因此可以证明贪心是传递的。所以通过局部的贪心,可以推断出全局(从N开始,进行层层扩散,总能到达K,这个也很简单,因为题目中的扩散路径有向左/右走一步,就算一直向左/右走,最终也是可以走到的)的贪心。
dist[cur-1] > dist[cur]+1
import (
   "fmt"
   "math"
)

func main() {
   bfs := Bfs(3, 58)
   fmt.Println(bfs)
}
func Max(N, K int) int {
   if N > K {
      return N
   } else {
      return K
   }
}

type Queue struct {
   values []int
}

func (q *Queue) Push(elem int) {
   q.values = append(q.values, elem)
}

func (q *Queue) Pop() int {
   res := q.values[0]
   q.values = q.values[1:]
   return res
}

func (q *Queue) Length() int {
   return len(q.values)
}
func (q *Queue) Peek() int {
   return q.values[0]
}

func (q *Queue) isEmpty() bool {
   return q.Length() == 0
}
func Bfs(N, K int) int {

   max := Max(N, K) + 5
   vis := make([]bool, max+1)
   dist := make([]int, max+1)
   for i, _ := range dist {
      dist[i] = math.MaxInt32
   }
   que := &Queue{values: make([]int, 20)}

   dist[N] = 0
   que.Push(N)
   vis[N] = true

   for {
      if que.isEmpty() {
         break
      }
      cur := que.Peek()
      que.Pop()
      
      if cur > 0 && dist[cur-1] > dist[cur]+1 {
         dist[cur-1] = dist[cur] + 1
         if !vis[cur-1] {
            que.Push(cur - 1)
            vis[cur-1] = true
         }
      }

      if cur < max && dist[cur+1] > dist[cur]+1 {
         dist[cur+1] = dist[cur] + 1
         if !vis[cur+1] {
            que.Push(cur + 1)
            vis[cur+1] = true
         }
      }
  
      if 2*cur <= max && dist[2*cur] > dist[cur]+1 {
         dist[2*cur] = dist[cur] + 1
         if !vis[2*cur] {
            que.Push(2 * cur)
            vis[2*cur] = true
         }
      }
   }
   return dist[K]
}