当青训营遇上码上掘金
题目简述
主题 3:寻友之旅
小青要找小码去玩,他们的家在一条直线上,当前小青在地点 ,小码在地点 (),并且小码在自己家原地不动等待小青。
小青有两种交通方式可选:步行和公交。
- 步行:小青可以在一分钟内从任意节点 移动到节点 或
- 公交:小青可以在一分钟内从任意节点 移动到节点 (公交不可以向后走)
请帮助小青通知小码,小青最快到达时间是多久? 输入: 两个整数 N 和 K 输出: 小青到小码家所需的最短时间(以分钟为单位)
题目分析
这道题目如下图所示:
对于 从上一个时刻位置 到下一步位置 ,有如下关系
题目要求最短时间,即要求最少操作步数能够从 到达 。
首先分析 在这些操作步数中可能移动的范围和情况:
- 首先,如果 移动到负数,显然不可能会有最少操作步数能够到达(这是由 数据情况决定)。
- 然后,分 和 的大小进行讨论:
- 如果 ,显然最少步数为 .(只能往回走)。
- 如果 ,这里是求解的难点:
此时,最小移动步骤中,N肯定不会移动到的范围有。
这是因为 到达 进行返回步骤 必然要小于 直接到达 的步骤,即: 。
因此,我们确定我们主要求解的情况是 ,并且在这些最少操作步数中,的移动范围只有可能在 。
下面我们通过两种思路进行优化求解:
问题求解
-
广度优先遍历(BFS)
一个最简单的思路就是遍历,将 的所有可能移动范围(看作是图的节点)进行遍历搜索即可得到最少时间。
其中我们注意到 BFS 首次到达该节点时刻必然是最少移动次数。所以我们在 BFS 和 DFS 中选择了 BFS 方法。
我们只需要将首次遍历到的节点压入队列中遍历,不停弹出处理完的节点即可。
Go 语言代码如下:type Node struct { pos int minute int } // Brute-force method for the problem -> BFS func bf_method(n int, k int) int { if k <= n { return n - k } steps := make([]int, (k<<1)+2) for i := 0; i <= (k<<1)+1; i += 1 { steps[i] = MAX_STEPS } queue := list.New() queue.PushBack(Node{n, 0}) for queue.Len() > 0 { node := queue.Front() cur_pos, cur_minute := node.Value.(Node).pos, node.Value.(Node).minute queue.Remove(node) // move next steps next_minute := cur_minute + 1 next_pos := cur_pos + 1 if next_pos >= 0 && next_pos <= (k<<1)+1 && steps[next_pos] == MAX_STEPS { steps[next_pos] = next_minute queue.PushBack(Node{next_pos, next_minute}) } next_pos = cur_pos - 1 if next_pos >= 0 && next_pos <= (k<<1)+1 && steps[next_pos] == MAX_STEPS { steps[next_pos] = next_minute queue.PushBack(Node{next_pos, next_minute}) } next_pos = cur_pos * 2 if next_pos >= 0 && next_pos <= (k<<1)+1 && steps[next_pos] == MAX_STEPS { steps[next_pos] = next_minute queue.PushBack(Node{next_pos, next_minute}) } } return steps[k] }复杂度分析:
- 时间复杂度:由于本代码遍历中会除去之后遍历到的节点(不进入队列遍历),实际最坏只将 到 个节点依次遍历求解,所以时间复杂度为 。
- 空间复杂度:由于本代码需要保存之前遍历结果和一个队列维护,并且该队列只会出现之前遍历结果(即:每一个节点最多出现一次),所以空间复杂度为 。
- P.S:
- 如果没有保存之前遍历结果进行去重剪枝,时间复杂度将会是 ,这是因为将之后抵达节点的时间重复遍历了。
- 空间复杂度没有经过优化也会变为 ,这是因为加入了重复节点的抵达时间。
-
动态规划(DP)
- 这道题根据1中加入之前保存结果(进行简化),很容易转化为一个动态规划的优化方法。
- 这里我们依然保存之前计算结果(不同位置到达时间)。遍历对象进行改变(之前遍历对象是时间从小到大遍历)
- 考虑遍历对象为从 位置,如果我们保证 位置所用的时间最少,那么它的下一个步骤移动的位置 所用时间必然最少。
- 这里还需要注意如果 使用 方式移动超出 的位置时候,那么如果往回走超过2步 (设为 步) 才到达 ,我们一定能找到一种少的步骤比这种方式少。
- 所以即使超过,我们只需要计算超过 1 步的位置进行推演。
- 直观理解最后两点表述:我们只需要在进行该步骤前做 步骤前移,就能够比上述方案使用步骤(时间)更短。
设到达位置的最短时间为
边界条件:
转移方程:Go 代码如下:
func dp_method(n int, k int) int { if k <= n { return n - k } // define min function min := func(a int, b int) int { if a < b { return a } return b } steps := make([]int, (k<<1)+2) for i := 0; i <= (k<<1)+1; i += 1 { if i <= n { steps[i] = n - i } else { steps[i] = steps[i-1] + 1 } } for i := 0; i <= (k<<1)+1; i += 1 { if i%2 == 0 { steps[i] = min(steps[i], steps[i>>1]+1) } else { steps[i] = min(steps[i], min(steps[(i+1)>>1]+2, steps[(i-1)>>1]+2)) } } return steps[k] }复杂度分析:
时间复杂度: 。具体分析1中已经进行过。
空间复杂度: 。
实际运行
这里我们编写了一个随机对数器对两个运行时间进行检查(检查次数为1000次),如下:
得到运行时间(可能有一定不同):
虽然两个时间复杂度都是,但是实际实现以及每一个对象操作次数有所不同,导致实际效果不同。
- 方法1使用一个 container\list 实现队列操作,并且其中有大量操作需要对队列进行pop,并且检查判断次数相比方法2也多。
- 方法2使用一个 切片进行实现,访问过程顺序进行,并且操作次数比1少,所以相对会更快一些。