寻友之旅, dp写法, 附随机用例的单元测试验证

122 阅读2分钟

当青训营遇上码上掘金

寻友之旅

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

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

读题

  1. 首先读数据范围, 0 <= n , k <= 1e5, 由此可以简单判断时间复杂度应当控制在O(nlogn)以内
  2. 粗略读一下题目, 从起点n移动到终点k, 所花费的最少次数, 每次移动有三种方法
x{x+1x1x2x\begin{cases}&x+1\\&x-1\\&x*2\end{cases}

, 其中x-1仅当当前位置在终点右侧时使用.

解题一, 验证算法, O(n^2 )

刚开始为了偷懒往贪心上去想, 仔细想了想还是dp做法比较安全

首先, 是dp表示的内容, 因为 可能从k-1的地方开始做公交, 而且当x>k的时候, 只能通过步行, 因此将dp[i]表示为, 从终点k出发, 到达当前位置x的最小步数

dp:=make([]int,(k<<1)+10), 即将dp数组长度设置为终点的两倍, 因为有 所以dp数组需要设置的稍微大一点.

接着是状态转移方程

dp[x]={dp[x1]+1,x>kmin(dp[x+1],dp[x2],dp[y2]+(xa))+1,x<k,x/2<y<x0,x=kdp[x] = \begin{cases}dp[x-1]+1,&x>k\\min(dp[x+1],dp[x*2],dp[y*2]+(x-a))+1,&x<k,x/2<y<x\\0,&x=k\end{cases}

即当x>k时, 只能通过步行从k转移到x, eg: 从k -> k+1 -> k+2

当x<k时, 需要选择步行或是公交, 因为存在往回走的情况, 所以需要考虑三种, 不往回走分为公交和步行, 往回走一定为步行, 综上, 暂时可以写出O(n^2)级别的验证算法.

代码

寻友之旅&攒青豆-码上掘金链接

 package activity
 ​
 // findFriends 寻友之旅
 // n,k 范围[0,1e5]
 // dp, O(n2)
 func findFriends(n, k int) (ans int) {
     const INF int = 0x3f3f3f3f
     min := func(a, b int) int {
         if a < b {
             return a
         }
         return b
     }
     //需要注意的case, 题目未给出n,k的大小关系
     if n >= k {
         //在公交反方向, 只能步行
         //或者n=k,俩人家在一起
         return n - k
     }
     dp := make([]int, (k<<1)+10)
     for i := 0; i < len(dp); i++ {
         dp[i] = INF
     }
     dp[k] = 0
     //先处理从右侧到终点的情况
     for i := k + 1; i < len(dp); i++ {
         dp[i] = dp[i-1] + 1
     }
     for i := k - 1; i >= n; i-- {
         dp[i] = min(dp[i+1], dp[i<<1]) + 1
         for j := (i + 1) >> 1; j < i; j++ {
             dp[i] = min(dp[i], dp[j*2]+i-j+1)
         }
     }
     return dp[n]
 }
 ​

解题2, 线性dp, O(n)

想复杂了, 没有好好分析情况

dp[i]存储从n到i的最短步数, 则状态转移方程如下

dp[i]={dp[i+1]+1,i<n0,i=ndp[i>>1]+1,i>ni为偶数min(dp[(i+1)>>1],dp[(i1)>>1])+2,i>ni为奇数dp[i] = \begin{cases}dp[i+1]+1,&i<n\\0,&i=n\\dp[i>>1]+1,&i>n且i为偶数\\min(dp[(i+1)>>1],dp[(i-1)>>1])+2,&i>n且i为奇数\end{cases}

最终返回dp[k],

需要注意奇数时的情况, 可能会有两张情况, 需要格外注意

eg: 1,2,3,4,5,6,7,8,9,10

dp[5] = dp[3]+2, 3->6->5

dp[5] = dp[2]+2, 2->4->5

因为dp[k]的值仅在遍历时更新一次, 并且遍历到k时, 所需要的元素均已经更新过, 因此, 遍历范围为(n,k]

代码

 package activity
 ​
 // findFriends 寻友之旅
 // n,k 范围[0,1e5]
 // dp, O(n)
 func findFriends(n, k int) (ans int) {
     const INF int = 0x3f3f3f3f
     min := func(a, b int) int {
         if a < b {
             return a
         }
         return b
     }
     //需要注意的case, 题目未给出n,k的大小关系
     if n >= k {
         //在公交反方向, 只能步行
         //或者n=k,俩人家在一起
         return n - k
     }
     dp := make([]int, (k<<1)+10)
     for i := n - 1; i >= 0; i-- {
         dp[i] = dp[i+1] + 1
     }
     for i := n + 1; i < len(dp); i++ {
         dp[i] = INF
     }
     for i := n + 1; i <= k; i++ {
         dp[i] = min(dp[i-1]+1, dp[i])
         if i%2 == 0 {
             dp[i] = min(dp[i], dp[i>>1]+1)
         } else {
             dp[i] = min(dp[i], dp[(i+1)>>1]+2)
             dp[i] = min(dp[i], dp[(i-1)>>1]+2)
         }
     }
     return dp[k]
 }
 ​

测试代码

 package activity
 ​
 import (
     "math/rand"
     "testing"
     "time"
 )
 ​
 ​
 func TestFindFriends(t *testing.T) {
     cnt := 0
     for i := 0; i < 1e2; i++ {
         rand.Seed(time.Now().UnixNano() + int64(i))
         n := rand.Intn(1e5)
         rand.Seed(time.Now().UnixNano() + int64(i*100))
         k := rand.Intn(1e5)
         ans := findFriendsEval(n, k)
         test := findFriends(n, k)
         t.Log("====================================================")
         t.Logf("当前第%v轮, 当前的n为%v, k为%v, 答案为%v", i+1, n, k, ans)
         if ans != test {
             cnt++
             t.Errorf("当前轮次出现错误, 测试结果为%v,正确答案为%v", test, ans)
         }
     }
 }
 ​
 ​

对应目录结构

-->activity

-->activity-->findFriends.go

-->activity-->findFriends_test.go