当青训营遇上码上掘金 | 主题创作活动后端板块

131 阅读5分钟

当青训营遇上码上掘金,青训营中讲授的知识与代码得以在代码平台上实现和发布,我们对Go语言的理解和使用将会在掘金平台的交流与实践中不断加深。

作为青训营后端基础班的成员,我将完成两道算法题的代码创作和讲解,并且利用到在青训营中学习到的Go语言使用与性能调优的知识来提高代码性能,并讨论Go语言在算法方面使用的需要注意之处

1.主题 3:寻友之旅

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

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

本问题看似为一个简单的搜索问题,实际上可以抽象为一个多项式问题:

K=i=0tki2iK=\sum_{i=0}^tk_i*2^i 其中tt为一个正整数,kt=Nk_t=N, 求t+i=0tkit+\sum_{i=0}^tk_i的最小值

这个抽象问题中含有参数tt和参数ki这两个有相关性的变量,基本上是很难直接获取数学思路的,但是结合本题的背景,tt实际上代表了小青坐公交车的时间,而i=0tki\sum_{i=0}^tk_i代表了小青走路的时间,KK为小码家的地址,NN为小青家的地址,kik_i为小青坐了kik-i分钟公交车之后又步行的时间(向前为正,向后为负),例如小青家在3,小码家在22,小青从家先向前步行了2分钟,坐了1分钟公交,又步行了1分钟,坐了一分钟公交到达目的地

那么 22=322+222+12122=3*2^2+2*2^2+1*2^1

这个公式提示我们,一昧的向前也即贪心算法是没有用的,因为kik_i的增加只会导致tt的减少,反之亦然

因此对于这样的问题,我们还是采取基本的搜索思想,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在最后一层将会分配3/22n3/2*2^n大小的空间来存放所有情况下小青的状态,也就是说在第十分钟的时候我们需要分配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)
}

image.png

可以看到即使来到第20层,且K值达到题目要求的上限,代码也能够快速运行。

2.字节跳动第五届青训营主题4:攒青豆

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,
排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

image.png

这是一个经典的单调栈问题,因此在本问题中仅仅介绍单调栈的逻辑,并且讨论在高度数组类算法题中的应用:

单调栈,顾名思义,即为一个单调的栈,通过对数组元素比较后的出栈入栈操作,保证栈内永远单调。

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))
}