当青训营遇上码上掘金,青训营中讲授的知识与代码得以在代码平台上实现和发布,我们对Go语言的理解和使用将会在掘金平台的交流与实践中不断加深。
作为青训营后端基础班的成员,我将完成两道算法题的代码创作和讲解,并且利用到在青训营中学习到的Go语言使用与性能调优的知识来提高代码性能,并讨论Go语言在算法方面使用的需要注意之处
1.主题 3:寻友之旅
小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
**请帮助小青通知小码,小青最快到达时间是多久?**
输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)
本问题看似为一个简单的搜索问题,实际上可以抽象为一个多项式问题:
其中为一个正整数,, 求的最小值
这个抽象问题中含有参数和参数ki这两个有相关性的变量,基本上是很难直接获取数学思路的,但是结合本题的背景,实际上代表了小青坐公交车的时间,而代表了小青走路的时间,为小码家的地址,为小青家的地址,为小青坐了分钟公交车之后又步行的时间(向前为正,向后为负),例如小青家在3,小码家在22,小青从家先向前步行了2分钟,坐了1分钟公交,又步行了1分钟,坐了一分钟公交到达目的地
那么
这个公式提示我们,一昧的向前也即贪心算法是没有用的,因为的增加只会导致的减少,反之亦然
因此对于这样的问题,我们还是采取基本的搜索思想,dfs算法在最短距离问题中表现并不稳定,取决于具体实例的情况,因此选择使用dfs算法以步数为层数进行广度优先搜索
考虑小青的移动情况
graph TB
A((位置在目标之前))
B((向前步行1min))
C((向后步行1min))
D((坐公交车1min))
J((向前步行1min))
K((向后步行1min))
L((坐公交车1min))
M((...))
N((...))
W((...))
E((位置在目标之后))
F((向后步行1min))
A-->B
A-->C
A-->D
B-->J
B-->K
B-->L
C-->M
D-->N
E-->F
F-->W
因此我们可以很容易得到每层的行动,然后将每层行动加入数组,并且作为下一层的参数传入,直到到达目的地返回即可。
但是具体操作上,考虑实际的问题,在最坏情况下,小青每次位置都在目标之前,一共走了n分钟才到达小码家,则dfs在最后一层将会分配大小的空间来存放所有情况下小青的状态,也就是说在第十分钟的时候我们需要分配1536*4字节的空间,这是有可能会造成堆栈溢出。
对于这种情况,我们可以做一方面的改进:
题目要求求出最短时间但是并未要求路径,因此对于小青的位置信息可以使用一个集合来存储:
posSet := make(map[int]struct{})
其中map的值部分使用青训营Go语言性能调优内容介绍的空结构体来尽可能减少内存占用,这样下来只会存储不同的位置信息,大大减少了空间占用问题。
到这个位置,题目基本已经分析完成,细节方面调整向后走的条件为当前位置不在0点即可,实现如下:
package main
import (
"fmt"
)
func findWay(pos2 int, step int, posList map[int]struct{}) int {
nextList := make(map[int]struct{}, 0)
for e, _ := range posList {
if e == pos2 {
return step
} else if e > pos2 {
nextList[e-1] = struct{}{}
} else {
if e > 1 {
nextList[e-1] = struct{}{}
}
nextList[e+1] = struct{}{}
nextList[e*2] = struct{}{}
}
}
return findWay(pos2, step+1, nextList)
}
func main() {
var pos1,pos2 int
fmt.Scan(&pos1,&pos2)
posList := make(map[int]struct{}, 0)
posList[pos1] = struct{}{}
step := findWay(pos2, 0, posList)
fmt.Println(step)
}
可以看到即使来到第20层,且K值达到题目要求的上限,代码也能够快速运行。
2.字节跳动第五届青训营主题4:攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,
排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
这是一个经典的单调栈问题,因此在本问题中仅仅介绍单调栈的逻辑,并且讨论在高度数组类算法题中的应用:
单调栈,顾名思义,即为一个单调的栈,通过对数组元素比较后的出栈入栈操作,保证栈内永远单调。
graph TD
A[开始]
B[第i个元素e准备入栈 栈顶元素为m]
C[e>m]
D[e<m]
E[m元素出栈]
F[i元素入栈 i++]
A-->B-->C-->E-->B
B-->D-->F
单调栈的性质为,栈中相邻两个元素之间的所有元素都小于他们,这个特性可以帮助我们解决本问题
但是仍然需要考虑另外一个问题:单调栈是有方向的,即单调栈并不能考虑到栈顶元素之后的情况,这是因为单调栈只会在出栈时体现性质
具体对于本题目而言,单调栈入栈时根据入栈值更新维护一个临时变量,再维护一个代表最大高度的数组,起初临时变量值为第一个入栈元素,同时数组对应位置赋值为该临时变量,当临时变量维护的元素出栈时,临时变量变为新的入栈元素,继续对最大高度数组赋值,之后为了解决栈顶元素之后元素失效问题,反向进行一次,则完成了最大高度数组。
具体代码实现如下:
package main
import "fmt"
func trap(height []int) int {
sinStack := make([]int, len(height))
res := 0
val := 0
for i, e := range height {
if val < e {
val = e
sinStack[i] = val
} else {
sinStack[i] = val
}
}
val = 0
for i := len(height) - 1; i >= 0; i-- {
if val < height[i] {
val = height[i]
sinStack[i] = val
} else {
if val < sinStack[i] {
sinStack[i] = val
}
}
}
for i, e := range sinStack {
res += e - height[i]
}
return res
}
func main() {
fmt.Println("输入容器长度:")
var len int
fmt.Scan(&len)
box := make([]int,len)
fmt.Println("输入容器高度:")
for i,_ := range box{
fmt.Scan(&box[i])
}
fmt.Println(trap(box))
}