经典dp--股票买卖系列之Go写法

本文探讨了使用Go语言解决一系列动态规划问题,包括编辑距离、最长公共子序列、正则表达式匹配和通配符匹配。通过二维数组保存状态并进行状态转移,解释了不同操作和边界条件对解题的影响。建议读者画出矩阵图以加深理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里我目前碰到了一些比较像的字符串dp处理的问题,这些问题结构很相似,都是借助二维数组来保存状态,根据某种状态来转移更新。其中第一和二道题很类似,三和四类似

PS:画个矩阵出来比一比更加形象生动!!!

Edit Distance

编辑距离这道题连接和题目描述见原链接(标题)

首先,我们定义一个dp数组,大小为m = len(word1), n = len(word2)。dp[i][j]的含义表示子串word1[0:i]word2[0:j]的最小编辑距离。

然后,我们可知操作主要有四种,也就是插入、删除、替换和什么都不搞,这四种操作我们可以看做获得最优解的一个状态,也就是说我们当前状态是通过这四种操作而获得。

所以我们有情况,当word1[i] == word2[j]时,dp[i][j]= dp[i-1][j-1], 因为两个相等了,就不需要操作了;当word1[i] != word2[j]时, 这是我们有三种操作可以选择,当然,我们求最小编辑距离,就是从三种操作中选择最小的那个:min(dp[i-1][j], dp[i][j-1],dp[i-1][j-1]) + 1

dp[i][j] = dp[i-1][j-1]      ,if word1[i] == word2[j]
dp[i][j] = minValues(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1   , otherwise

在这里特别说明一下dp[i][j-1]表示对word1进行插入,dp[i-1][j]表示对word1进行删除,dp[i-1][j-1]表示对word1进行替换

最后,我们需要明确一下边界,也就是初始值,我们需要对dp数组进行初始化,也就是dp数组大小是(m+1) x (n+1)的,初始化的值为对应的长度。举个简单的例子就是,假定word2长度为0,对于子串word1[0:i]对应的初始值dp[0][i] = i, 因为这个匹配是要不断删除word1字母,同理dp[i][0] = i

func minDistance(word1 string, word2 string) int {
    len1 := len(word1)
    len2 := len(word2)

    dp := make([][]int, len1+1)
    for i:=0; i<=len1; i++ {
        dp[i] = make([]int, len2+1)
    }

    // init 边界
    for i:=1; i<=len2; i++ {
        dp[0][i] = i
    }

    for i:=1; i<=len1; i++ {
        dp[i][0] = i
    }

    // finish init 

    for i:=1; i<=len1; i++ {
        for j:=1; j<=len2;j++ {
            // 根据状态转移方程计算dp[i][j]
            if word1[i-1] == word2[j-1] {
                dp[i][j] = dp[i-1][j-1]
            } else {
                dp[i][j] = minValues(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
            }
        }
    }
    return dp[len1][len2]
}

func minValues(values ...int) int {
    res := math.MaxInt32
    for _, v := range values {
        if v < res {
            res = v
        }
    }
    return res
}

Longest Common Subsequence

有了上面那个例子的,我们这个题也是相对比较类似的

首先,我们定义一个dp数组,大小为m = len(text1), n = len(text2)dp[i][j]表示子串text1[0:i]text2[0:j]的之间最大的公共子序列(LCS)。

然后我们需要确定状态,在这里状态有三种,当text1[i] == text2[j]时,我们的LCS比dp[i-1][j-1]要长一个单位;当text1[i] != text2[j]时, 我们可以根据dp[i-1][j]或者 dp[i][j-1]这两个状态中选一个最大的作为我们的值:

dp[i][j] = 1 + dp[i-1][j-1]      ,if text1[i] == text2[j]
dp[i][j] = max(dp[i-1][j], dp[i][j-1])    ,if text1[i] != text2[j]

边界问题则相对来说比较简单,某个text为空,其dp数组都为0,因为没有LCS。

func longestCommonSubsequence(text1 string, text2 string) int {
    len1 := len(text1)
    len2 := len(text2)

    dp := make([][]int, len1 + 1)
    for i:=0; i<=len1; i++ {
        dp[i] = make([]int, len2+1)
    } //finish init
    for i:=1; i<=len1; i++ {
        for j :=1; j<=len2;j++ {
            // 根据状态转移方程计算dp[i][j]
            if text2[j-1] == text1[i-1] {
                dp[i][j] = 1 + dp[i-1][j-1]
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
            }
        }
    }
    return dp[len1][len2]
}


func max(a, b int) int {
    if a < b {
        return b
    }
    return a
}
  • PS 还是那句话,自个画个矩阵图来体会一下效果更加好

剑指 Offer 19. 正则表达式匹配

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2GX836M-1612234029872)(C:\Users\conliwho\AppData\Roaming\Typora\typora-user-images\image-20210201235818430.png)]

同时,这里需要考虑了到三种字符串匹配模式:1. 正常字符;2. ?字符;3. *字符。其中 *字符对其前面的字符出现次数又分为三种情况:0次,1次和多次。针对这几种情况,我们对应获得其状态转移方程:(这里定义了一个 m + 1 x n + 1大小的dp矩阵(m = len(s), n = len(p)),因此j - 1 表示当前p指向的字符)

状态转移方程:

  1. p[j - 1] != '*'时,dp[i][j] == true的前提是dp[i - 1][j - 1] == true,这是确保前面字符串能匹配上,如果不能匹配,后面就是全都匹配上也没有用了。在dp[i - 1][j - 1] == true前提下,dp[i][j] == true成立只有s[i-1] == p[j - 1] 或者p[j - 1] == '.'

  2. p[j - 1] == '*'时,有三种情况:

    • 0次,这里就相当于去掉了p[j-1:j]字符串,因此这里我们可以用dp[i][j-2]来表示

    • 1次,这里就是相当于p[j-1]不存在,因此这里我们可以用dp[i][j-1]来表示

    • 多次,这种情况dp[i][j] == true的前提是dp[i - 1][j] == true,这里是保证了p[j]s[:i-1]要匹配上,也就是当前'*'的前面字符取一次的情况是否匹配上。只有在匹配上才进行下一步考虑。因此在dp[i - 1][j] == true前提下,dp[i][j] == true成立只有s[i-1] == p[j - 2] 或者p[j - 2] == '.' (这里就是取当前'*'的前面字符是否匹配上当前s[i-1]

边界条件为:

  • sp都为空时候为真:dp[0][0] = true
  • s不为空,p为空:dp[i][0] = false
  • s为空,p不为空:只有偶数位的字符都为*时为真,比如说"u*9*",所以有dp[0][j] = dp[0][j - 2] && p[j - 1] == '*'

所以有对应的代码:

func isMatch(s string, p string) bool {
    m, n := len(s), len(p)
    dp := make([][]bool, m + 1)
    for i := 0; i < m + 1; i++ {
        dp[i] = make([]bool, n + 1)
    }
    dp[0][0] = true
    for j := 2; j < n+1; j += 2 { // 边界条件
        dp[0][j] = dp[0][j - 2] && p[j - 1] == '*'
    }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if p[j -1] != '*' {
                if dp[i - 1][j - 1] { // 转移状态1
                    dp[i][j] = s[i - 1] == p[j - 1] || p[j - 1] == '.'
                    
                }
            } else { // 转移状态2
                dp[i][j] = dp[i][j - 2] || dp[i][j - 1] // 0次或者1次
                if dp[i - 1][j] { // 多次
                    dp[i][j] = dp[i][j] || s[i - 1] == p[j - 2] || p[j - 2] == '.'
                }
            }
        }
    }
    return dp[m][n]
}

44. 通配符匹配

这个题跟上面的题很像,思路大致都是一样的,不同的是状态转移方程和边界条件的区别而已(题目都不一样当然不同了),这里的区别就是*可以匹配任何东西,而且跟前面字符完全无关了,?匹配任何单个字符。

因此这里按照上面的思路,也是分为两种情况。

状态转移方程:

  1. p[j - 1] != '*'时,dp[i][j] == true的前提是dp[i - 1][j - 1] == true,这是确保前面字符串能匹配上,如果不能匹配,后面就是全都匹配上也没有用了。在dp[i - 1][j - 1] == true前提下,dp[i][j] == true成立只有s[i-1] == p[j - 1] 或者p[j - 1] == '?'
  2. p[j - 1] != '*'时, 这题只有两种情况了:
    • 不匹配任何字符(空字符),可用dp[i][j-1] 表示
    • 匹配任何字符,可用dp[i - 1][j] 表示

边界条件为:

  • sp都为空时候为真:dp[0][0] = true

  • s不为空,p为空:dp[i][0] = false

  • s为空,p不为空:只有当前p[j - 1] == '*'并且前面的字符串能匹配上才是为真,也就是dp[0][j] = dp[0][j-1] && p[i-1] == '*'

因此有代码:

func isMatch(s string, p string) bool {
    m, n := len(s), len(p)
    dp := make([][]bool, m + 1)
    for i := 0; i <= m; i++ {
        dp[i] = make([]bool, n + 1)
    }
    dp[0][0] = true
    for j := 1; j <= n; j++ {  // 边界条件
       dp[0][j] = dp[0][j-1] && p[j-1] == '*'
    }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if p[j-1] != '*' { // 转移状态1
                if dp[i - 1][j - 1] {
                    dp[i][j] = p[j - 1] == s[i - 1] || p[j - 1] == '?'
                }
                
            } else { // 转移状态2
                dp[i][j] = dp[i-1][j] || dp[i][j-1]
            }
        }
    }
    return dp[m][n]
}


// 矩阵反过来的形式也是可以的,初始化就可以不用单独一个for循环了

func isMatch(s string, p string) bool {
    m, n := len(p), len(s)
    dp := make([][]bool, m + 1)
    for i := 0; i <= m; i++ {
        dp[i] = make([]bool, n + 1)
    }
    dp[0][0] = true
    for i := 1; i <= m; i++ {
        dp[i][0] = dp[i-1][0] && p[i-1] == '*' // 边界条件
        for j := 1; j <= n; j++ {
            if p[i-1] == '*' { // 转移状态2
                dp[i][j] = dp[i-1][j] || dp[i][j-1]
            } else { // 转移状态1
                if dp[i - 1][j - 1] {
                    dp[i][j] = p[i - 1] == s[j - 1] || p[i - 1] == '?'
                }
                
            }
        }
    }
    return dp[m][n]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值