题解:最少前缀操作问题 | 豆包MarsCode AI刷题

107 阅读6分钟

问题描述:

小U和小R有两个字符串,分别是S和T,现在小U需要通过对S进行若干次操作,使其变成T的一个前缀。操作可以是修改S的某一个字符,或者删除S末尾的字符。现在你需要帮助小U计算出,最少需要多少次操作才能让S变成T的前缀。

测试样例

样例1

输入:S = "aba", T = "abb"
输出:1

样例2

输入:S = "abcd", T = "efg"
输出:4

样例3

输入:S = "xyz", T = "xy"
输出:1

样例4

输入:S = "hello", T = "helloworld"
输出:0

样例5

输入:S = "same", T = "same"
输出:0

尝试解题:

第一眼是不是感觉特别熟悉?不知道有没有人跟我一样第一眼看到题目想到了经典的字符编辑问题。如果把T分解成不同的前缀,然后把S跟每个前缀做一次字符编辑貌似就能得到答案,然后我按照下面的代码试了一下:

def fun(s, t):
    dp = [[0 for _ in range(len(s) + 1)] for _ in range(len(t) + 1)]
    
    # 初始化 dp 数组
    for i in range(len(t) + 1):
        dp[0][i] = i
    for j in range(len(s) + 1):
        dp[j][0] = j
    
    # 填充 dp 数组
    for i in range(1, len(s) + 1):
        for j in range(1, len(t) + 1):
            if s[i-1] == t[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + 1
    
    return dp[len(s)][len(t)]
    
def solution(S: str, T: str) -> int:
    minn = float('inf')
    
    # 遍历 T 的所有前缀
    for i in range(len(T) + 1):
        prefix = T[:i]
        # 计算将 S 变成 prefix 所需的最少操作次数
        minn = min(minn, fun(S, prefix))
    
    return minn
    
if __name__ == '__main__':
    print(solution("aba", "abb") == 1)
    print(solution("abcd", "efg") == 4)
    print(solution("xyz", "xy") == 1)
    print(solution("hello", "helloworld") == 0)
    print(solution("same", "same") == 0)

看起来好像没什么问题,结果WA了。仔细看了下题目才发现问题:这道题跟字符编辑不是一回事。在字符编辑问题中我们能对字符串进行修改、增加和删除的操作,但是这些操作并没有任何限制,可以对任意字符执行,这也是为什么我们的状态转移方程可以写成:

if s[i-1] == t[j-1]:
    dp[i][j] = dp[i-1][j-1]
else:
    dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1

回到这道题中,题目要求的是对S进行操作使其变成T的前缀。我们模拟一下这个过程,假设S = 'abcd',对应当前匹配的前缀为pre = 'qwd',从后往前看,S[3]=pre[2]='d',满足 dp 的 if 语句条件,但是我们不能使用 dp[i][j] = dp[i-1][j-1] 进行状态转移,因为从S变成pre并不等价于从S[:4]变成pre[:3]

为什么呢?这是因为我们的字符删除操作必须从字符串末尾开始。而很明显,从S[:4]变成pre[:3]必须删掉一个字符,但是即使删掉的这个字符就是S[2],也需要先删掉S[3],但是我们的状态转移方程是将S[3]pre[2]当成匹配字符进行保留了的,因此这就出现了矛盾,所以字符编辑的思路并不适用于这题。

正确思路

其实这题跟dp没什么关系,不要想复杂了。因为字符串只有修改和尾部删除两种操作,所以,S要变成T,要想操作次数最少,所有的删除操作(如果需要的话)肯定是在修改操作完成之后进行的

这分成两种情况:

  1. S比T长:那很明显S必须删除一些多余的字符。至于删多少个,其实只需要删到跟T一样长即可,再删是没有意义的。还是举个例子:

S = 'abcd', T = 'qwc'

首先我们删掉S的最后一个字符让S和T长度持平,然后问题就变成了S_new = 'abc'T = 'qwc'的最小操作步骤。因为删除操作只能删除末尾字符,所以当S_new末尾字符跟T末尾字符相同时,最优方案其实就是跳过这个点继续处理前面的字符。比如这里,S_new末尾字符与T对应位置字符相等,那么直接跳过不处理,然后前面所有不匹配的字符全部进行替换为最优方案;但是如果S_new末尾字符与T对应位置字符不相等,比如一个'abc'一个'qwb',那么删除和修改必须二选一:删除之后就变成了'ab'去匹配'qw';而修改之后本质上相当于转化成了'ab(b)'去匹配'qw(b)'。两者的区别是,如果你选择删除,那么后续还可以继续执行删除操作(也就是可以选择匹配更短的前缀),但是如果你选择了修改,那么你最终匹配的前缀就是当前的S_new长度的T前缀,下一步也就转换成了S_new末尾字符与T对应位置字符相等这一情况。但是本质上,这两种选择对操作次数没有影响,只对最后匹配的前缀形式有影响。因此我们只需要在S比T长时使用删除操作对齐长度,然后剩下的全部交给修改操作,这就是该题的最优方案。

  1. S比T短:按照上面的分析,这时完全没必要执行删除操作,直接把不相同的字符修改即可。

捋清思路之后这题就很清晰了,直接用双指针把两个字符串扫一遍答案就出来了:

  • 具体步骤如下

    • 1、初始化计数器cnt存储最终答案

    • 2、初始化两指针 i, j 分别指向ST的开头

    • 3、依次向后扫描两字符串,当两指针指向字符不相等时计数器cnt+1,否则不进行任何处理,然后两个指针都向前移动一位

    • 4、如果步骤3结束后j指向了T的末尾,说明初始的ST长,需要额外删掉多出来的字符数,cnt加上这部分长度即可,当然这一步也可以在3之前通过直接比较ST的长度完成。

完整代码:

def solution(S: str, T: str) -> int:
    i, j, cnt = 0, 0, 0
    while i<len(S) and j<len(T):
        if S[i] != T[j]:
            cnt += 1
        i += 1
        j += 1
    if j == len(T):
        cnt += len(S) - len(T)
    return cnt

if __name__ == '__main__':
    print(solution("aba", "abb") == 1)
    print(solution("abcd", "efg") == 4)
    print(solution("xyz", "xy") == 1)
    print(solution("hello", "helloworld") == 0)
    print(solution("same", "same") == 0)
    print(solution("jnjsmjgcyplywiwmb", "eefgegffege") == 17)