AI 刷题答疑 | 动态规划 | 豆包MarsCode AI刷题

158 阅读5分钟

最近刷算法题的时候遇到了这么一道题,自己一直发现不了错误,然后题解思路和我的也不一样,需要用KMP算法,看不明白,通过MARSCODE AI帮助我读懂了题解,找到了问题所在!!

首先来读题:

小秋很喜欢在某平台上看一些商家推荐。

一天小秋看到有一家商铺的好评非常多,想去体验一下,于是他高高兴兴地去了那家商铺。不出意外的话还是出意外了。那家商铺对小秋来说简直是噩梦,和平台上的好评完全相反。小秋认为这个商家的评论有问题,很可能是通过某种刷评论软件刷的。

在反复查看商家的评论后,小秋发现,在后面的评论中,经常出现和前面一模一样的评论。

因此,小秋做了如下推断:假设把商家现在的评论看成一个只包含小写字母的字符串 s,s 是通过一个空字符串 t,经过若干次下列操作得到的:

  1. 在 t 末尾添加任意一个小写字母;
  2. 在 t 末尾添加 t 任意长度的前缀;

小秋现在想知道,对于一个商家的评论 s,由一个空字符串得到最少需要多少次上述操作。

如果计算得到的操作数量过小,那么商家的评论肯定就是刷的,就可以去举报了。你只需要帮小秋求出这个最小操作数量。

输入格式

输入仅一行,一个只包含小写字母的字符串 s。

输出格式

输出仅一行,一个整数表示由空字符串得到 s 的最少操作数量。

样例输入

ababa

样例输出

4

说明

  • 第 1 次操作,在字符串末尾添加一个字符 a,得到字符串 a;
  • 第 2 次操作,在字符串末尾添加一个字符 b,得到字符串 ab;
  • 第 3 次操作,在字符串末尾添加前缀 ab,得到字符串 abab;
  • 第 4 次操作,在字符串末尾添加一个字符 a,得到字符串 ababa。

评测数据规模

对于 20% 的评测数据,1≤∣s∣≤103

对于 40% 的评测数据,1≤∣s∣≤105

对于 100% 的评测数据,1≤∣s∣≤106

我一开始的思路:

一个个遍历字符,然后找前缀的第0个字符中是否为该字符。
如果是,继续找遍历位置的下一字符是否出现在第二个字符处...。
这样依次往后找,直到找到不相等处或者找到前缀的最后一个字母处。
这些相等的字符采用操作2来一次完成。

import os
import sys

x=input()
n=len(x)
cnt=1
i=1
while i<n:
    if x[i] != x[0]:
        cnt+=1
        i+=1
    else:
        temp=1
        while x[i+temp] == x[temp]:
                temp+=1
                if i+temp>=n:
                    break
                if temp>=i:
                    break
        cnt+=1
        i=i+temp
    #print(cnt,i-1)
print(cnt)

但是这样只能通过60%的样例,让我们来看一下MARSCODE AI的说明:

你的代码逻辑是正确的,但是在处理字符串索引时可能会出现越界的情况。在Python中,字符串的索引是从0开始的,因此当你访问x[i+temp]时,如果i+temp的值大于等于字符串x的长度n,就会导致IndexError

为了避免这种情况,你可以在访问字符串索引之前检查索引是否越界。

MARSCODE AI的讲解一开始并不正确,但是在学习了题解后,它又对回复进行了改进:

在 KMP 算法中,next 数组用于记录字符串中每个位置之前的最长相等前缀和后缀的长度。在这个问题中,我们可以利用 next 数组来找到字符串中可以通过复制前缀得到的最长重复子串。

接下来,我们考虑动态规划(DP)的方法来解决这个问题。我们定义一个数组 dp,其中 dp[i] 表示得到字符串 s 的前 i 个字符的最小操作次数。

对于每个位置 i,我们有两种可能的操作:

  1. 在字符串 t 的末尾添加一个任意小写字母,这将使操作次数增加 1。因此,dp[i] = dp[i-1] + 1
  2. 复制字符串 t 的某个前缀到末尾。为了找到这个前缀,我们需要查看 next 数组。假设 next[i] 表示字符串 s 的前 i 个字符中最长相等前缀和后缀的长度,那么我们可以从位置 i - next[i] 复制前缀到位置 i。但是,我们需要注意一个问题:如果复制的前缀长度小于等于 i 的一半,那么我们可能无法通过一次复制操作得到长度为 i 的字符串。因此,我们需要找到一个最长的不重叠的相等前缀和后缀,即 min(⌊i/2⌋, next[i])。这样,我们的转移方程为 dp[i] = min(dp[i-1], dp[i - min(⌊i/2⌋, next[i])]) + 1

最终,我们的答案是 dp[n],其中 n 是字符串 s 的长度。

最后,我理解到了我的代码哪里出问题了。

我的思路是贪心方法,但是动态递归才是正解。
比如对于aakakaaakak字符串,
若利用我最开始的代码(贪心):是a a k a k aa a k a k这样分割,
而利用动态规划:是a a k a k a aakak这样分割!!
次数很明显是dp的更少

下面给出蓝桥云课的官方AC Code:

import sys

def main():
    s = input()
    n = len(s)
    s = " " + s
    ne = [0] * (n + 1)
    for i in range(2, n + 1):
        j = ne[i - 1]
        while s[i] != s[j + 1] and j:
            j = ne[j]
        if s[j + 1] == s[i]:
            j += 1
        ne[i] = j
    
    f = [1000000000] * (n + 1)
    f[0] = 0
    for i in range(1, n + 1):
        f[i] = f[i - 1] + 1
        length = min(i // 2, ne[i])
        f[i] = min(f[i], f[i - length] + 1)
    
    print(f[n])

if __name__ == "__main__":
    main()