目录

  • 一.假设有排成一排的N个格子,记为1~N,机器人在除开端点外的格子可以往左或往右走一格,左端点只能右走一格,右端点只能左走一格,给定一定的行动步数,在某一个格子作为起点,另一个格子作为终点,问在规定的行走步数走到终点的方式有几种?
  • 二.给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,每次只能拿最左或者最右的牌,现规定玩家A先拿,请返回最后获胜者的分数。(范围尝试模型)
  • 三.背包问题(一件商品有其相应的重量和价值,在不超过背包容量的情况下能获得的最大价值是多少?)
  • 四.规定1和A对应、2和B对应、3和C对应......26和Z对应,那么一个数字字符串比如“111”就可以转化为“AAA”、“KA”和“AK”。现给定一个只有数字字符组成的字符串str,返回有多少种转化结果?
  • 五.给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文,arr每个字符串代表一张贴纸,你可以把单个字符剪开使用,拼出str需要至少几张贴纸?
  • 六.最长公共子序列(样本对应模型:特别注重讨论两个样本的结尾处情况)
  • 七.字符串的最长回文子序列长度(注意子序列是可以不连续的,子串是一定要连续的)
  • 八.10*9大小的象棋跳马,从棋盘左下角位置(设为(0,0)),然后设定一个目标点和一定的步数,问最多能有几种方式到达目标点?
  • 九.返回N个人从泡咖啡到洗干净咖啡杯的最短时间(有三台咖啡机和一台洗咖啡杯机,泡咖啡的时间分别是1分钟、3分钟和7分钟,洗咖啡杯机的清洗时间是5分钟,每台咖啡机和洗咖啡杯机每次只能一个人用,假设咖啡泡好即喝完,待洗杯子可以选择放到洗咖啡杯机,或自然挥发8分钟也能干净)

一.假设有排成一排的N个格子,记为1~N,机器人在除开端点外的格子可以往左或往右走一格,左端点只能右走一格,右端点只能左走一格,给定一定的行动步数,在某一个格子作为起点,另一个格子作为终点,问在规定的行走步数走到终点的方式有几种?

//N:总的格数 Cur:机器人当前位置 Aim:目标位置 Rest:剩余的可行走步数 wayNum:不同的行走方式数量
func process1(N, Cur, Aim, Rest int) (wayNum int) {if Rest == 0 {if Cur == Aim {return 1} else {return 0}}//如果还剩余行走步数,可分成三种情况if Cur == 1 {return process1(N, 2, Aim, Rest-1)}if Cur == N {return process1(N, N-1, Aim, Rest-1)}//在中间位置return process1(N, Cur-1, Aim, Rest-1) + process1(N, Cur+1, Aim, Rest-1)
}//N:总的格数 Start:机器人起始位置 Aim:目标位置 Step:可行走的总步数 wayNum:不同的行走方式数量
func way(N, Start, Aim, Step int) (wayNum int) {if N < 1 || Start < 1 || Start > N || Aim < 1 || Aim > N || Step < 1 {return -1}return process1(N, Start, Aim, Step)
}func main() {//1 2 3 4 fmt.Println(way(4, 2, 4, 4))
}

接下来考虑优化的问题,对于上面的递归函数,显然返回值只和当前位置和剩余步数有关,这两个值只要确定了,返回值就确定了,而递归函数中有可能会多次出现参数相同的的递归函数,那是否可以考虑将第一次计算得到的返回值存储起来,第二次遇到时就可以不调用递归直接获得返回值呢?
这种空间换时间,其实就是记忆化搜索

//N:总的格数 Cur:机器人当前位置 Aim:目标位置 Rest:剩余的可行走步数 wayNum:不同的行走方式数量
func process2(N, Cur, Aim, Rest int, dp [][]int) (ans int) {//当缓存表的值不为-1时,表示这种参数组合的递归函数已经算过了if dp[Cur][Rest] != -1 {return dp[Cur][Rest]}if Rest == 0 {if Cur == Aim {ans = 1} else {ans = 0}} else {//如果还剩余行走步数,可分成三种情况if Cur == 1 {ans = process2(N, 2, Aim, Rest-1, dp)} else if Cur == N {ans = process2(N, N-1, Aim, Rest-1, dp)} else {//在中间位置ans = process2(N, Cur-1, Aim, Rest-1, dp) + process2(N, Cur+1, Aim, Rest-1, dp)}}//记录缓存表dp[Cur][Rest] = ansreturn ans
}//N:总的格数 Start:机器人起始位置 Aim:目标位置 Step:可行走的总步数 wayNum:不同的行走方式数量
func way(N, Start, Aim, Step int) (wayNum int) {if N < 1 || Start < 1 || Start > N || Aim < 1 || Aim > N || Step < 1 {return -1}//对于递归函数来说:当前位置Cur的取值为1~N,剩余步数的取值为0~Step,所以设定以下这个二位数组dp := make([][]int, N+1)for i := range dp {dp[i] = make([]int, Step+1)}for i := 0; i <= N; i++ {for j := 0; j <= Step; j++ {dp[i][j] = -1}}return process2(N, Start, Aim, Step, dp)
}func main() {//1 2 3 4fmt.Println(way(4, 2, 4, 4))
}

还有哪里可以优化呢?dp表的填写有一定的规律,当前格子依赖于左上和左下格子之和,只要给出了第一列,后面那就可以直接填写。
这就是状态转移,状态转移是结果,这样优化后其实就是靠表的规律来直接得到递归的值,而不用递归来算

//N:总的格数 Start:机器人起始位置 Aim:目标位置 Step:可行走的总步数 wayNum:不同的行走方式数量
func way(N, Start, Aim, Step int) int {if N < 1 || Start < 1 || Start > N || Aim < 1 || Aim > N || Step < 1 {return -1}//对于递归函数来说:当前位置Cur的取值为1~N,剩余步数的取值为0~Step,所以设定以下这个二位数组dp := make([][]int, N+1)for i := range dp {dp[i] = make([]int, Step+1) //初始化默认为0}dp[Aim][0] = 1 //将剩余0步且在目标位置的dp格子设置为1,其他就按默认为0//而我们所想要得到的是(Start,Step)位置的值,只要把dp表填好即可//填表的顺序:先按列填,注意第0行不用填,第一列已经填好了for Rest := 1; Rest <= Step; Rest++ { //这是列dp[1][Rest] = dp[2][Rest-1] //注意第一行的格子只依赖左下格子for Cur := 2; Cur <= N-1; Cur++ {dp[Cur][Rest] = dp[Cur-1][Rest-1] + dp[Cur+1][Rest-1]}dp[N][Rest] = dp[N-1][Rest-1] //注意第N行的格子只依赖左上格子}return dp[Start][Step]
}func main() {//1 2 3 4fmt.Println(way(5, 2, 4, 6))
}

二.给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,每次只能拿最左或者最右的牌,现规定玩家A先拿,请返回最后获胜者的分数。(范围尝试模型)

//先手递归函数,返回当前取值以及剩下部分后手取值的最小值
func first(arr []float64, L, R int) float64 {if L == R {return arr[L]} else {return math.Max(arr[L]+last(arr, L+1, R), arr[R]+last(arr, L, R-1))}
}//后手递归函数,返回先手取值后剩下部分的最小值
func last(arr []float64, L, R int) float64 {if L == R {return 0} else {return math.Min(first(arr, L+1, R), first(arr, L, R-1))}
}func win(arr []float64) float64 {if len(arr) == 0 || arr == nil {return -1}firstScore := first(arr, 0, len(arr)-1)secondScore := last(arr, 0, len(arr)-1)return math.Max(firstScore, secondScore)
}
func main() {arr := make([]float64, 0)arr = append(arr, 5, 7, 4, 5, 8, 1, 6, 0, 3, 4, 6, 1, 7)fmt.Println(arr)fmt.Println("获胜者分数为:", win(arr))
}

接下来考虑优化问题,随便举个实例,分析依赖关系,找到重复解,可以先弄一个缓存表的优化版本

//先手递归函数,返回当前取值以及剩下部分后手取值的最小值
func first(arr []float64, L, R int, fmap, lmap [][]float64) (ans float64) {if fmap[L][R] != -1 {return fmap[L][R]} else {if L == R {ans = arr[L]} else {ans = math.Max(arr[L]+last(arr, L+1, R, fmap, lmap), arr[R]+last(arr, L, R-1, fmap, lmap))}}fmap[L][R] = ansreturn ans
}//后手递归函数,返回先手取值后剩下部分的最小值
func last(arr []float64, L, R int, fmap, lmap [][]float64) (ans float64) {if lmap[L][R] != -1 {return lmap[L][R]} else {if L != R {ans = math.Min(first(arr, L+1, R, fmap, lmap), first(arr, L, R-1, fmap, lmap))}}lmap[L][R] = ansreturn ans
}func win(arr []float64) float64 {if len(arr) == 0 || arr == nil {return -1}//弄两张缓存表,分别给两个递归函数使用fmap := make([][]float64, len(arr))lmap := make([][]float64, len(arr))for i := range fmap {fmap[i] = make([]float64, len(arr))lmap[i] = make([]float64, len(arr))}for i := 0; i < len(arr); i++ {for j := 0; j < len(arr); j++ {fmap[i][j] = -1lmap[i][j] = -1}}firstScore := first(arr, 0, len(arr)-1, fmap, lmap)secondScore := last(arr, 0, len(arr)-1, fmap, lmap)return math.Max(firstScore, secondScore)
}
func main() {arr := make([]float64, 0)arr = append(arr, 5, 7, 4, 5, 8, 1, 6, 0, 3, 4, 6, 1, 7)fmt.Println(arr)fmt.Println("获胜者分数为:", win(arr))
}

同样也可以根据依赖关系得到缓存表的填写规律,从而直接完成两张表的填写,而不用去递归计算,经过分析可知fmap和lmap的左下部分是不用填的,而fmap里的格子依赖于这个格子在gmap的对称点的左边格子和下边格子,这样我们可以先填好fmap和lmap边的斜对角线的值,然后相互参考填写剩余格子。

func win(arr []float64) float64 {if len(arr) == 0 || arr == nil {return -1}//弄两张缓存表,分别给两个递归函数使用fmap := make([][]float64, len(arr))lmap := make([][]float64, len(arr))for i := range fmap {fmap[i] = make([]float64, len(arr))lmap[i] = make([]float64, len(arr))}//先填好斜对角线的数值,gmap初始化时已经为0,for i := 0; i < len(arr); i++ {fmap[i][i] = arr[i]}//然后斜着填写剩余格子,而基准就是第一行的每个格子,每次都要填写好平行于斜对角线的格子for startCol := 1; startCol < len(arr); startCol++ {Row := 0Col := startColfor Col < len(arr) {fmap[Row][Col] = math.Max(arr[Row]+lmap[Row+1][Col], arr[Col]+lmap[Row][Col-1])lmap[Row][Col] = math.Min(fmap[Row+1][Col], fmap[Row][Col-1])Row++Col++}}return math.Max(fmap[0][len(arr)-1], lmap[0][len(arr)-1])
}
func main() {arr := make([]float64, 0)arr = append(arr, 5, 7, 4, 5, 8, 1, 6, 0, 3, 4, 6, 1, 7)fmt.Println(arr)fmt.Println("获胜者分数为:", win(arr))
}

三.背包问题(一件商品有其相应的重量和价值,在不超过背包容量的情况下能获得的最大价值是多少?)

默认版本就是暴力递归

func process(w []int, v []int, bag int, index int) int {if bag < 0 {return 0}if index == len(w) {return 0}//如果上诉条件都没满足,说明当前还有商品选择,并且背包还有容量//注意这里要先判断一下是否超出背包容量if bag-w[index] >= 0 {//情况一:要当前index位置的商品,valueSum1 := v[index] + process(w, v, bag-w[index], index+1)//情况二:不要当前index位置的商品valueSum2 := process(w, v, bag, index+1)return int(math.Max(float64(valueSum1), float64(valueSum2)))} else { //背包容量不足,必不能要当前商品return process(w, v, bag, index+1)}
}
func maxValue(w []int, v []int, bag int) int {if w == nil || v == nil || len(w) <= 0 || len(v) <= 0 || len(w) != len(v) {return -1}return process(w, v, bag, 0)
}
func main() {weights := make([]int, 0)values := make([]int, 0)weights = append(weights, 3, 2, 4, 7)values = append(values, 5, 6, 3, 19)setBag := 11fmt.Println("当前满足背包的最大价值:", maxValue(weights, values, setBag))
}

接下来举个例子找一下是否有出现重复解

缓存表版本不写了,直接动态规划表,经过递归函数分析可知,每一行的格子只依赖其底下一行,所以动态规划表的填写顺序是自底向上

func dp_MaxValue(w []int, v []int, bag int) int {if w == nil || v == nil || len(w) <= 0 || len(v) <= 0 || len(w) != len(v) {return -1}//index的取值范围0~len(w)      有效restBag的取值范围0~bagdp := make([][]int, len(w)+1)for i := range dp {dp[i] = make([]int, bag+1)}//最底下一行要填0,初始化时就是0了,就不用再填了for index := len(w) - 1; index >= 0; index-- {for restBag := 0; restBag <= bag; restBag++ {if restBag-w[index] >= 0 {valueSum1 := dp[index+1][restBag]                     //这是不要当前的商品valueSum2 := v[index] + dp[index+1][restBag-w[index]] //这是要当前的商品dp[index][restBag] = int(math.Max(float64(valueSum1), float64(valueSum2)))} else {//当前商品不能装入背包dp[index][restBag] = dp[index+1][restBag]}}}return dp[0][bag] //dp表的每个格子代表当前格子信息出发的获得的最大价值
}
func main() {weights := make([]int, 0)values := make([]int, 0)weights = append(weights, 3, 2, 4, 7, 3, 1, 7)values = append(values, 5, 6, 3, 19, 12, 4, 2)setBag := 15fmt.Println("当前满足背包的最大价值:", dp_MaxValue(weights, values, setBag))
}

四.规定1和A对应、2和B对应、3和C对应…26和Z对应,那么一个数字字符串比如“111”就可以转化为“AAA”、“KA”和“AK”。现给定一个只有数字字符组成的字符串str,返回有多少种转化结果?

//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}//递归函数的参数是字符数组和当前所在字符的位置索引,返回的是可转化结果的数量
func process(arr []byte, index int) int {if index == len(arr) { //如果来到了字符数组最后一个字符的后一位,证明前面的全部字符已经完成转化,代表这就是一种转化结果return 1}if arr[index] == '0' { //如果遇到一个字符是0,证明前面有决策出错了return 0}//如果上述条件不满足,证明当前位置还在字符数组中间且当前字符不为0//选择一:字符单转,这种是没有条件限制的,直接进入下一个字符就行methodSum := process(arr, index+1)//选择二:和后一个字符一起转,这种的条件限制是合起来的数字不能超过26if index+1 < len(arr) && (arr[index]-'0')*10+(arr[index+1]-'0') < 27 {methodSum += process(arr, index+2)}return methodSum
}
func numToStr(Str string) int {if len(Str) == 0 || &Str == nil {return -1}strArr := StringToBytes(Str)return process(strArr, 0)
}func main() {ss := "11111"fmt.Println(numToStr(ss))
}
func dp_numToStr(Str string) int {if len(Str) == 0 || &Str == nil {return -1}strArr := StringToBytes(Str)dp := make([]int, len(strArr)+1)dp[len(strArr)] = 1for i := len(strArr) - 1; i >= 0; i-- {//当strArr[i]为0时,表示之前决策错误,这时候的返回,即dp[i]=0,而初始化就已经默认为0if strArr[i] != '0' {methodSum := dp[i+1]if i+1 < len(strArr) && (strArr[i]-'0')*10+(strArr[i+1]-'0') < 27 {methodSum += dp[i+2]}dp[i] = methodSum}}return dp[0] //返回最后填写好的值
}
func main() {ss := "23151024661"fmt.Println(numToStr(ss))fmt.Println(dp_numToStr(ss))
}

五.给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文,arr每个字符串代表一张贴纸,你可以把单个字符剪开使用,拼出str需要至少几张贴纸?

(注:这道题下面实现的三个版本代码其实都有点小问题,就是最大值INT_MAX这个标志位设置成100000,原意是想要一个和Java那种系统最大值来当标志位,但是代码只用来测试逻辑是不是正确的,用的测试数据很小,所以就随便弄了个最大值,如果贴纸要用到100000以上这些情况时,结果就会有问题了,最好自己弄一个golang的系统最大值当标志位 )
简单的暴力递归版本

//const INT_MAX = int(^uint(0) >> 1)
const INT_MAX = 100000//标志位
func minusString(jianshu, beijianshu string) string {count := make([]int, 26) //创建一个26字母的字符频率统计数组,下面用两个for循环实现字符相减for _, i := range jianshu {count[i-'a']++}for _, i := range beijianshu {count[i-'a']--}rebuildStr := make([]byte, 0)for i := 0; i < 26; i++ {if count[i] > 0 {for j := 0; j < count[i]; j++ {rebuildStr = append(rebuildStr, byte('a'+i))}}}return string(rebuildStr)
}//递归函数的参数是贴纸字符串数组和剩余的目标字符串,返回的是需要贴纸的数量
func process(stickers []string, restTarget string) int {if len(restTarget) == 0 { //如果已经没有剩余目标字符,说明已经完成字符拼接了,就不再需要贴纸了return 0}minSticker := INT_MAX//主要流程:从贴纸数组按顺序取一张贴纸出来,用目标字符串减去第一个贴纸字符得到剩余字符串,剩余字符串相当于成为新的目标字符串,重复执行前面的操作直到剩余字符串为空//然后又回到一开始初始目标字符串,用用目标字符串减去第一个贴纸字符得到剩余字符串...,直到完成全部贴纸作为第一张的情况for _, sticker := range stickers {rest := minusString(restTarget, sticker)if len(rest) != len(restTarget) { //如果符合条件说明目标字符串的部分字符和贴纸的部分匹配,剩下的字符继续匹配其他minSticker = int(math.Min(float64(minSticker), float64(process(stickers, rest)))) //这里面递归到最后一层,如果符合条件的话是会返回0的}}//如果没执行上面的语句,minSticker就是INT_MAXif minSticker == INT_MAX {return minSticker} else { //如果执行了上面的语句,minSticker就不可能是最大值了,但由于最后一层是返回0,所以这里要加1return minSticker + 1}
}
func minStickers(stickers []string, target string) int {ans := process(stickers, target)if ans == INT_MAX {return -1} else {return ans}
}
func main() {str1 := make([]string, 0)str1 = append(str1, "abc", "bkb", "cd")target := "aaaakd"fmt.Println(minStickers(str1, target))
}

很显然上面的暴力递归做了很多多余的工作,接下来用一些剪枝优化一下,剪枝版本:

package mainimport ("fmt""math""unsafe"
)//const INT_MAX = int(^uint(0) >> 1)
const INT_MAX = 10000//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}//递归函数的参数是贴纸数组和剩余的目标字符串,返回的是需要贴纸的数量
//贴纸用一个N*26的二维数组表示,N表示有N张贴纸,26是26个字母,做这个数组的目的是提前将一张贴纸的字符出现频率做好
func process(stickers [][]int, restTarget string) int {if len(restTarget) == 0 { //如果已经没有剩余目标字符,说明已经完成字符拼接了,就不再需要贴纸了return 0}restArr := StringToBytes(restTarget)//将 剩余目标字符串 转成 目标词频表RTcount := make([]int, 26)for _, i := range restArr {RTcount[i-'a']++}minSticker := INT_MAX//关键优化二:剪枝,之前每一种贴纸都作为第一个对比初始目标字符串的对象,现在只要出现过目标字符串的第一个字符的贴纸作为初始目标串的对比对象for i := 0; i < len(stickers); i++ {sticker := stickers[i]           //取出第一张贴纸,如果含有目标字符串的首字符,就拿来用,没有就不用if sticker[restArr[0]-'a'] > 0 { //选出贴纸rebuildStr := make([]byte, 0) //构建重构后的数组for m := 0; m < 26; m++ {     //然后按每个字母来对比目标串和贴纸的词频表,相减if RTcount[m] > 0 {for j := 0; j < RTcount[m]-sticker[m]; j++ {rebuildStr = append(rebuildStr, byte('a'+m))}}}minSticker = int(math.Min(float64(minSticker), float64(process(stickers, string(rebuildStr)))))}}//如果没执行上面的语句,minSticker就是INT_MAXif minSticker == INT_MAX {return minSticker} else { //如果执行了上面的语句,minSticker就不可能是最大值了,但由于最后一层是返回0,所以这里要加1return minSticker + 1}
}
func minStickers(stickers []string, target string) int {//关键优化一:用词频统计表替换贴纸数组counts := make([][]int, len(stickers))for i := range counts {counts[i] = make([]int, 26)}for i := 0; i < len(stickers); i++ {for _, j := range stickers[i] {counts[i][j-'a']++}}ans := process(counts, target)if ans == INT_MAX {return -1} else {return ans}
}
func main() {str1 := make([]string, 0)str1 = append(str1, "zbc", "mg", "nx")target := "zcczxxx"fmt.Println(minStickers(str1, target))
}

经过举一些特殊的例子可知这个贴纸拼词的过程也有重复解,但是可变参数是字符串类型,没办法找到准确的临界点,所以弄不了严格的dp表结构,那就只剩缓存表(哈希表实现)来记录重复解

package mainimport ("fmt""math""unsafe"
)//const INT_MAX = int(^uint(0) >> 1)
const INT_MAX = 10000//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}//递归函数的参数是贴纸数组和剩余的目标字符串,返回的是需要贴纸的数量
//贴纸用一个N*26的二维数组表示,N表示有N张贴纸,26是26个字母,做这个数组的目的是提前将一张贴纸的字符出现频率做好
func process(stickers [][]int, restTarget string, dp map[string]int) (ans int) {if i, ok := dp[restTarget]; ok { //从dp哈希表中取值,如果目标字符串已经计算过,直接返回return i}if len(restTarget) == 0 { //如果已经没有剩余目标字符,说明已经完成字符拼接了,就不再需要贴纸了return 0}restArr := StringToBytes(restTarget)//将 剩余目标字符串 转成 目标词频表RTcount := make([]int, 26)for _, i := range restArr {RTcount[i-'a']++}minSticker := INT_MAX//关键优化二:剪枝,之前每一种贴纸都作为第一个对比初始目标字符串的对象,现在只要出现过目标字符串的第一个字符的贴纸作为初始目标串的对比对象for i := 0; i < len(stickers); i++ {sticker := stickers[i]           //取出第一张贴纸,如果含有目标字符串的首字符,就拿来用,没有就不用if sticker[restArr[0]-'a'] > 0 { //选出贴纸rebuildStr := make([]byte, 0) //构建重构后的数组for m := 0; m < 26; m++ {     //然后按每个字母来对比目标串和贴纸的词频表,相减if RTcount[m] > 0 {for j := 0; j < RTcount[m]-sticker[m]; j++ {rebuildStr = append(rebuildStr, byte('a'+m))}}}minSticker = int(math.Min(float64(minSticker), float64(process(stickers, string(rebuildStr), dp))))}}//如果没执行上面的语句,minSticker就是INT_MAXif minSticker == INT_MAX {ans = minSticker} else { //如果执行了上面的语句,minSticker就不可能是最大值了,但由于最后一层是返回0,所以这里要加1ans = minSticker + 1}//记录当前字符串对应的结果dp[restTarget] = ansreturn ans
}
func minStickers(stickers []string, target string) int {//关键优化一:用词频统计表替换贴纸数组counts := make([][]int, len(stickers))for i := range counts {counts[i] = make([]int, 26)}for i := 0; i < len(stickers); i++ {for _, j := range stickers[i] {counts[i][j-'a']++}}dp := make(map[string]int)dp[" "] = 0ans := process(counts, target, dp)if ans == INT_MAX {return -1} else {return ans}
}
func main() {str1 := make([]string, 0)str1 = append(str1, "zbc", "mg", "nx")target := "zccz"fmt.Println(minStickers(str1, target))
}

六.最长公共子序列(样本对应模型:特别注重讨论两个样本的结尾处情况)

暴力递归

package mainimport ("fmt""math""unsafe"
)//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}
func process(str1, str2 []byte, i, j int) int {if i == 0 && j == 0 {if str1[i] == str2[j] {return 1} else {return 0}} else if i == 0 {if str1[i] == str2[i] {return 1} else {return process(str1, str2, i, j-1)}} else if j == 0 {if str1[j] == str2[j] {return 1} else {return process(str1, str2, i-1, j)}} else {/*样本对应模型就以结尾处为讨论基础,在本题中有三种情况:1.可能考虑str1中i位置的字符作为结尾,不可能考虑str2中j位置的字符作为结尾2.不可能考虑str1中i位置的字符作为结尾,可能考虑str2中j位置的字符作为结尾3.考虑str1中i位置的字符作为结尾,考虑str2中j位置的字符作为结尾(这种情况就是要求字符str1的i位置和str2的j位置的字符必须相同)*/p1 := process(str1, str2, i-1, j)p2 := process(str1, str2, i, j-1)p3 := -1if str1[i] == str2[j] {p3 = process(str1, str2, i-1, j-1) + 1} else {p3 = 0}return int(math.Max(math.Max(float64(p1), float64(p2)), float64(p3)))}
}
func getLongest(str1, str2 string) int {if len(str1) == 0 || len(str2) == 0 || &str1 == nil || &str2 == nil {return 0}str1Arr := StringToBytes(str1)str2Arr := StringToBytes(str2)return process(str1Arr, str2Arr, len(str1Arr)-1, len(str2Arr)-1)
}
func main() {str1 := "abc12ef3d45"str2 := "mkluhg12345"fmt.Println(getLongest(str1, str2))
}

接下来是dp表版本,分析递归函数可知每个格子依赖于它左边格子、上面格子和左上格子,所以只要先填好0行0列的那个格子,然后填第0行格子,接下来在逐行填写即可完成dp表的填写:

package mainimport ("fmt""math""unsafe"
)//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}
func process(str1, str2 []byte, i, j int) int {if i == 0 && j == 0 {if str1[i] == str2[j] {return 1} else {return 0}} else if i == 0 {if str1[i] == str2[i] {return 1} else {return process(str1, str2, i, j-1)}} else if j == 0 {if str1[j] == str2[j] {return 1} else {return process(str1, str2, i-1, j)}} else {/*样本对应模型就以结尾处为讨论基础,在本题中有三种情况:1.可能考虑str1中i位置的字符作为结尾,不可能考虑str2中j位置的字符作为结尾2.不可能考虑str1中i位置的字符作为结尾,可能考虑str2中j位置的字符作为结尾3.考虑str1中i位置的字符作为结尾,考虑str2中j位置的字符作为结尾(这种情况就是要求字符str1的i位置和str2的j位置的字符必须相同)*/p1 := process(str1, str2, i-1, j)p2 := process(str1, str2, i, j-1)p3 := -1if str1[i] == str2[j] {p3 = process(str1, str2, i-1, j-1) + 1} else {p3 = 0}return int(math.Max(math.Max(float64(p1), float64(p2)), float64(p3)))}
}
func getLongest(str1, str2 string) int {if len(str1) == 0 || len(str2) == 0 || &str1 == nil || &str2 == nil {return 0}str1Arr := StringToBytes(str1)str2Arr := StringToBytes(str2)dp := make([][]int, len(str1Arr))for i := range dp {dp[i] = make([]int, len(str2Arr))}//先填dp[0][0]if str1Arr[0] == str2Arr[0] {dp[0][0] = 1} //如果不等就默认0//再填dp[0][...]for i := 1; i < len(str2Arr); i++ {if str1Arr[0] == str2Arr[i] {dp[0][i] = 1} else {dp[0][i] = dp[0][i-1]}}//再填dp[...][0]for i := 1; i < len(str1Arr); i++ {if str1Arr[i] == str2Arr[0] {dp[i][0] = 1} else {dp[i][0] = dp[i-1][0]}}//再逐行填写其他位置for i := 1; i < len(str1Arr); i++ {for j := 1; j < len(str2Arr); j++ {dp[i][j] = int(math.Max(float64(dp[i-1][j]), float64(dp[i][j-1])))if str1Arr[i] == str2Arr[j] {dp[i][j] = int(math.Max(float64(dp[i][j]), float64(dp[i-1][j-1]+1)))}}}return dp[len(str1Arr)-1][len(str2Arr)-1]
}
func main() {str1 := "abc12ef3d4"str2 := "mkluhg12345"fmt.Println(getLongest(str1, str2))
}

七.字符串的最长回文子序列长度(注意子序列是可以不连续的,子串是一定要连续的)

思路一:一个字符串和其逆序串的最长公共子序列就是最长回文子序列(套上一道题即可,这里不写)
思路二:范围尝试模型(特别注意讨论开头和结尾的不同情况)
暴力递归版本

package mainimport ("fmt""math""unsafe"
)//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}//返回strArr[L...R]的最长回文子序列
func process(strArr []byte, L, R int) int {if L < 0 || R >= len(strArr) || L > R || &strArr == nil {return -1}if L == R {return 1} else if L == R-1 {if strArr[L] == strArr[R] {return 2} else {return 1}} else {//情况一:最长回文子序列既不以L开头,也不以R结尾//情况二:最长回文子序列以L开头,不以R结尾//情况三:最长回文子序列不以L开头,以R结尾//情况四:最长回文子序列既以L开头,也以R结尾(只有strArr[L]和strArr[R]相等才有这种情况)p1 := process(strArr, L+1, R-1) //反正和LR无关,就直接缩小范围即可,下面同理p2 := process(strArr, L, R-1)p3 := process(strArr, L+1, R)p4 := -1if strArr[L] == strArr[R] {p4 = process(strArr, L+1, R-1) + 2}return int(math.Max(math.Max(float64(p1), float64(p2)), math.Max(float64(p3), float64(p4))))}
}
func Palindrome(str string) int {if len(str) == 0 || &str == nil {return 0}return process(StringToBytes(str), 0, len(str)-1)
}
func main() {str := "6afd&pygoykd#ae"fmt.Println(Palindrome(str))
}

接下来考虑优化,首先分析递归函数的可变参数,是int类型的L和R,都是可以确定下来的,所以可以向二维dp表考虑:

package mainimport ("fmt""math""unsafe"
)//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}
func Palindrome(str string) int {if len(str) == 0 || &str == nil {return 0}strArr := StringToBytes(str)dp := make([][]int, len(str))for i := range dp {dp[i] = make([]int, len(str))}//先把右下角格子填了dp[len(str)-1][len(str)-1] = 1//然后主对角线和上边一条对角线一起填for i := 0; i < len(str)-1; i++ {dp[i][i] = 1if strArr[i] == strArr[i+1] {dp[i][i+1] = 2} else {dp[i][i+1] = 1}}//最后从倒数第三行开始填,依次往上,每行再从左往右for L := len(str) - 3; L >= 0; L-- {for R := L + 2; R < len(str); R++ {p1 := dp[L+1][R-1] //反正和LR无关,就直接缩小范围即可,下面同理p2 := dp[L][R-1]p3 := dp[L+1][R]p4 := -1if strArr[L] == strArr[R] {p4 = dp[L+1][R-1] + 2}dp[L][R] = int(math.Max(math.Max(float64(p1), float64(p2)), math.Max(float64(p3), float64(p4))))}}return dp[0][len(str)-1]
}
func main() {str := "6afd&pygoykd#ae"fmt.Println(Palindrome(str))
}


接下来还有一部分细节点可优化:由之前的递归可知,当前格子依赖于其左下,左和下三个格子的最大值,如下图分析可知K格子其实可以不考虑的:

package mainimport ("fmt""math""unsafe"
)//字符串转单个字符数组的函数
func StringToBytes(data string) []byte {return *(*[]byte)(unsafe.Pointer(&data))
}
func Palindrome(str string) int {if len(str) == 0 || &str == nil {return 0}strArr := StringToBytes(str)dp := make([][]int, len(str))for i := range dp {dp[i] = make([]int, len(str))}//先把右下角格子填了dp[len(str)-1][len(str)-1] = 1//然后主对角线和上边一条对角线一起填for i := 0; i < len(str)-1; i++ {dp[i][i] = 1if strArr[i] == strArr[i+1] {dp[i][i+1] = 2} else {dp[i][i+1] = 1}}//最后从倒数第三行开始填,依次往上,每行再从左往右for L := len(str) - 3; L >= 0; L-- {for R := L + 2; R < len(str); R++ {dp[L][R] = int(math.Max(float64(dp[L][R-1]), float64(dp[L+1][R])))if strArr[L] == strArr[R] {dp[L][R] = int(math.Max(float64(dp[L][R]), float64(dp[L+1][R-1]+2)))}}}return dp[0][len(str)-1]
}
func main() {str := "6afd&pygoykd#ae"fmt.Println(Palindrome(str))
}

八.10*9大小的象棋跳马,从棋盘左下角位置(设为(0,0)),然后设定一个目标点和一定的步数,问最多能有几种方式到达目标点?

Jump是暴力递归版本,Jump2是三维dp版本

package mainimport ("fmt"
)//x,y表示当前位置,a,b表示目标位置,rest表示剩余步数(10*9棋盘)
func process(x, y, rest, a, b int) int {if x < 0 || x > 9 || y < 0 || y > 8 {return 0}if rest == 0 {if x == a && y == b {return 1} else {return 0}}//有八种跳法ways := process(x+2, y+1, rest-1, a, b)ways += process(x+1, y+2, rest-1, a, b)ways += process(x-1, y+2, rest-1, a, b)ways += process(x-2, y+1, rest-1, a, b)ways += process(x-2, y-1, rest-1, a, b)ways += process(x-1, y-2, rest-1, a, b)ways += process(x+1, y-2, rest-1, a, b)ways += process(x+2, y-1, rest-1, a, b)return ways}
func Jump(a, b, step int) int {return process(0, 0, step, a, b)
}//这个函数是为了解决越界问题,在之前的递归中越界就返回0,但是在数组越界会报错的
func pick(dp [][][]int, x, y, rest int) int {if x < 0 || x > 9 || y < 0 || y > 8 {return 0}return dp[x][y][rest]
}
func Jump2(a, b, step int) int {dp := make([][][]int, 10)for i := range dp {dp[i] = make([][]int, 9)for j := range dp[i] {dp[i][j] = make([]int, step+1)}}//三维dp表,每一个rest一层dp[a][b][0] = 1for rest := 1; rest <= step; rest++ {for x := 0; x < 10; x++ {for y := 0; y < 9; y++ {ways := pick(dp, x+2, y+1, rest-1)ways += pick(dp, x+1, y+2, rest-1)ways += pick(dp, x-1, y+2, rest-1)ways += pick(dp, x-2, y+1, rest-1)ways += pick(dp, x-2, y-1, rest-1)ways += pick(dp, x-1, y-2, rest-1)ways += pick(dp, x+1, y-2, rest-1)ways += pick(dp, x+2, y-1, rest-1)dp[x][y][rest] = ways}}}return dp[0][0][step]
}
func main() {x, y, step := 2, 1, 7fmt.Println(Jump(x, y, step))fmt.Println(Jump2(x, y, step))
}

九.返回N个人从泡咖啡到洗干净咖啡杯的最短时间(有三台咖啡机和一台洗咖啡杯机,泡咖啡的时间分别是1分钟、3分钟和7分钟,洗咖啡杯机的清洗时间是5分钟,每台咖啡机和洗咖啡杯机每次只能一个人用,假设咖啡泡好即喝完,待洗杯子可以选择放到洗咖啡杯机,或自然挥发8分钟也能干净)

其实这是两个问题,一开始调度所有人执行最优的排队策略是个问题,而洗咖啡杯又是一个问题
调度排队问题:
小根堆实现,而且堆里的每个数据是 咖啡机下一次空闲的时间点+咖啡机的冲泡时间
(咖啡机下一次空闲的时间点,咖啡机的冲泡时间)
小根堆的排序就是 咖啡机下一次空闲的时间点+咖啡机的冲泡时间

初始
(0,1)
(0,3)
(0,7)
第一个人来准备排队,弹出(0,1),然后生成(0+1,1),前面代表上次的空闲时间点+咖啡机的冲泡时间,后面代表咖啡机的冲泡时间,然后将这个数据放回堆里
(1,1)
(0,3)
(0,7)
第二个人来准备排队,弹出(1,1),然后生成(1+1,1),前面代表上次的空闲时间点+咖啡机的冲泡时间,后面代表咖啡机的冲泡时间,然后将这个数据放回堆里
(2,1)
(0,3)
(0,7)
第三个人来准备排队,弹出(2,1),然后生成(2+1,1),前面代表上次的空闲时间点+咖啡机的冲泡时间,后面代表咖啡机的冲泡时间,然后将这个数据放回堆里
(0,3)
(3,1)
(0,7)
第四个人来准备排队,弹出(0,3),然后生成(0+3,3),前面代表上次的空闲时间点+咖啡机的冲泡时间,后面代表咖啡机的冲泡时间,然后将这个数据放回堆里
(3,1)
(3,3)
(0,7)
后续依次类推…代码这里不附上(因为实现流程和动态规划无关的)

洗杯问题解决如下:

package mainimport ("fmt""math"
)/*
drinks表示待洗杯子的产生时间点,即数组里面的值表示一个时间点,数组大小就表示待洗杯子的数量
washTime表示洗杯机的清洗时间,固定时间
airTime表示杯子自然挥发干净的时间,固定时间
freeTime表示洗杯机空闲的时间点
index表示当前的咖啡杯,drinks[0...index-1]的杯子已经是干净的了
*/
func bestTime(drinks []int, washTime, airTime, index, freeTime int) int {if len(drinks) == index {return 0}//选择一:index号杯子用洗杯机清洗//情况一:洗杯机空闲,所以杯子开始洗的时间点是待洗杯子的产生时间点 + 洗杯子的时间//情况二:洗杯机忙碌,所以杯子开始洗的时间点是待洗杯子的产生时间点和洗杯机接下来空闲的时间点比较的最大值 + 洗杯子的时间selfClean1 := int(math.Max(float64(drinks[index]), float64(freeTime))) + washTime //当前杯子清洁完成后的时间点restClean1 := bestTime(drinks, washTime, airTime, index+1, selfClean1)            //剩余杯子的清洁时间/*类似木桶原理,我所需要的是所有杯子都变干净的时间,即在当前杯子清洁时间以内,后面的杯子都挥发干净了,总时间就取当前的杯子清洁时间;如果在当前杯子清洁时间以内,后面的杯子没有挥发干净,那总时间就要取后面杯子的清洁时间*/p1 := math.Max(float64(selfClean1), float64(restClean1))//选择二:index号杯子自然挥发selfClean2 := drinks[index] + airTimerestClean2 := bestTime(drinks, washTime, airTime, index+1, freeTime) //剩余杯子的清洁时间p2 := math.Max(float64(selfClean2), float64(restClean2))             //p2和p1同理return int(math.Min(p1, p2)) //那返回就是要最短的时间
}func dp_bestTime(drinks []int, washTime, airTime int) int {//想让全部杯子都去洗,得到之前freeTime(洗完全部杯子后的洗杯机空闲时间点)的最大值maxFree := 0for i := 0; i < len(drinks); i++ {maxFree = int(math.Max(float64(maxFree), float64(drinks[i]))) + washTime}//开始准备dp表,递归函数的可变参数是index和freeTimedp := make([][]int, len(drinks)+1)for i := range dp {dp[i] = make([]int, maxFree+1)}//经过分析可知,当前返回都依赖于index+1,所以是从下往上填//由上面的递归可知:dp[len(drinks)][...] = 0,初始化表时就是0了,接下来填好上面的dp//注意下面的restClean1不能填dp[index+1][selfClean1],因为selfClean1是max(drinks[index],freeTime)+wash,而这里的freeTime是逼近maxFree的,再加wash就可能越界了for index := len(drinks) - 1; index >= 0; index-- {for freeTime := 0; freeTime <= maxFree; freeTime++ {selfClean1 := int(math.Max(float64(drinks[index]), float64(freeTime))) + washTime//但其实是不会填到越界的情况的,这里就和之前的范围尝试模型的L>R的情况,这里算出来的最大值是所有杯子清洁完成后的时间点,而在//前面的那些杯子中要使用的洗杯机空闲时间是不可能到达这个时间点的,所以真实递归里是不可能出现这些越界的状态的if selfClean1 > maxFree {continue}restClean1 := dp[index+1][selfClean1]p1 := math.Max(float64(selfClean1), float64(restClean1))selfClean2 := drinks[index] + airTimerestClean2 := dp[index+1][freeTime]p2 := math.Max(float64(selfClean2), float64(restClean2))dp[index][freeTime] = int(math.Min(p1, p2))}}return dp[0][0] //index从开始,咖啡机空闲时间点是0
}func main() {drinks := make([]int, 0)drinks = append(drinks, 3, 14, 15)fmt.Println("暴力递归方法输出:", bestTime(drinks, 2, 5, 0, 0))fmt.Println("dp输出:", dp_bestTime(drinks, 2, 5))
}

其中可变参数freeTime不能直观地得到变化范围,比如说样本对应模型的两个参数就是下标,范围尝试模型的L和R都是下标,背包问题也是明确背包容量大小,都可以明确其变化范围,而本题的freeTime这里就应该假设所有的杯子都去洗,这样最坏情况得到的值作为边界,这样改出来的形式就是业务限制模型。

算法学习笔记:涉及动态规划的简单例题相关推荐

  1. 算法学习笔记----用动态规划解决钢管切割问题

    (说明:由于CSDN的博客中不能添加下标等特殊符号,所以部分内容使用截图的形式) 通过对问题进行高度抽象,现在我们的问题,就是要递归地求解r n 的最大值,下面采用的是一种自顶向下的递归方法: int ...

  2. Python最优化算法学习笔记(Gurobi)

    微信公众号:数学建模与人工智能 github地址:https://github.com/QInzhengk/Math-Model-and-Machine-Learning Python最优化算法学习笔 ...

  3. 基于MVS的三维重建算法学习笔记(四)— 立体匹配经典算法Semi-Global Matching(SGM)论文翻译及要点解读

    基于MVS的三维重建算法学习笔记(四)- 立体匹配经典算法Semi-Global Matching(SGM)论文翻译及要点解读 声明 SGM概述 Cost Calculation(像素代价计算)--M ...

  4. 输出dag的所有拓扑排序序列_算法学习笔记(53): 拓扑排序

    拓扑排序是对DAG(有向无环图)上的节点进行排序,使得对于每一条有向边 , 都在 之前出现.简单地说,是在不破坏节点 先后顺序的前提下,把DAG拉成一条链.如果以游戏中的科技树(虽然名字带树,其实常常 ...

  5. 两个字符串的最长公共子序列长度_算法学习笔记(58): 最长公共子序列

    (为什么都更了这么多篇笔记了,这时候才讲这么基础的内容呢?因为我本来以为LCS这种简单的DP不用讲的,结果CF不久前考了LCS的变式,然后我发现由于自己对LCS一点都不熟,居然写不出来 ,于是决定还是 ...

  6. 基于MVS的三维重建算法学习笔记(二)— 立体视觉的几何基础总结

    基于MVS的三维重建算法学习笔记(二)- 立体视觉的几何基础总结 声明 概述 1. 常见三维数据类型 2. 三维形状的几种表达形式 3. 三维空间刚体运动 4. 李群和李代数 5. 相机标定 6. 非 ...

  7. 数据结构与算法学习笔记——链栈

    数据结构与算法学习笔记(C语言) 链栈 在开始链栈的学习之前,我们先实现一下上一篇文章中提到的检查括号匹配的小程序,鉴于水平有限,本人就随便写一下代码好了,目标仅限于对功能的实现. /*用顺序栈这种数 ...

  8. 基于MVS的三维重建算法学习笔记(五)— 立体匹配经典算法PatchMatch论文翻译及要点解读

    基于MVS的三维重建算法学习笔记(五)- 立体匹配经典算法PatchMatch论文翻译及要点解读 声明 问题提出 问题建模 通过PatchMatch获取平面参数--Inference via Patc ...

  9. 数据结构与算法学习笔记——图 C++实现

    数据结构与算法学习笔记--图 C++实现 1 概念 2 图的表示方法 3 算法 3.1 拓扑排序 3.2 图的搜索算法 3.2.1 广度优先搜索(BFS) 3.2.2 深度优先搜索(DFS) 3.3 ...

  10. Parse算法学习笔记

    Parse算法学习笔记 Parse算法是一种自底向上的语法分析算法,其主要应用于编译器中的语法分析阶段.相比于其他语法分析算法,Parse算法具有简单.高效的特点.本篇文章将详细介绍Parse算法的原 ...

最新文章

  1. 宿主如何访问虚拟机中的web服务器
  2. teraterm 执行sql命令_tera term的ttl脚本使用方法 | 学步园
  3. linux 隐藏脚本运行,linux – 为什么在运行ls时隐藏此文件?
  4. 当程序发布特别慢的时候,如何高效使用Eclipse
  5. 转载:XPath基本语法
  6. 利用iMazing将iOS设备的录音文件拷贝到电脑
  7. asp.net中控制反转的理解
  8. 【图像分割】基于matlab分水岭算法图像分割【含Matlab源码 390期】
  9. Excel函数大全一《求和与统计函数》
  10. TreeView 右键菜单
  11. VS2015基于对话框的MFC倒计时器
  12. 【SSM框架】MyBatis
  13. 【金猿产品展】沃丰科技GaussMind——用技术提升客户体验
  14. Self-Attention with Relative Position Representations(2018)
  15. 多个迹象表明,瑞幸咖啡已进入新的发展阶段
  16. 进制之间快速转换技巧
  17. 点亮技能 I 人机对话系统全面理解
  18. Adobe Acrobat DC 2022 直装版
  19. 信息系统集成-辅助知识-知产/法律法规/标准化
  20. Mp4文件播放原理分析

热门文章

  1. Unity实现隐藏鼠标功能
  2. bzoj1779 [Usaco2010 Hol]Cowwar 奶牛战争(网络流)
  3. 二维平面上线段与直线位置关系的判定
  4. pandas.melt()详解
  5. 第四章 变形-学习笔记+练习题
  6. 怎么查询oracle归档模式,查看oracle数据库归档模式
  7. 【Redis】Redis常用命令
  8. Jetson TK1学习(二)安装无线网卡
  9. 计算2个GPS坐标的距离
  10. aardio - 范例搜索工具