VBA 中的最长公共子序列给出 #VALUE!错误

Longest Common Subsequence in VBA Giving #VALUE! Error

我一直在 Excel (365) 中组合一个 UDF 来计算两个字符串之间的最长公共子序列(基于 python https://www.geeksforgeeks.org/printing-longest-common-subsequence/ 中的实现)。

当我 运行 UDF 时,我得到一个#Value!工作表上的错误。我已经完成了一些基本的调试,但我是 VBA 和 运行 撞墙的新手。代码中的消息框语句就是为了粗略的调试。

我认为问题出在我对 L 数组的操作上。它似乎进入了第一组 for 循环中的第一种情况,然后在评估 L(i, j,) = 0 时退出。任何关于我哪里出错的指针?

在工作表中,我使用 =ClosestMatch("aabbaaaa", "aaaabbaa") 并得到 #VALUE! 作为结果。

这是我正在尝试的 UDF 的 VBA 代码:

Function ClosestMatch(ByVal x As String, ByVal y As String, Optional ByVal return_String As Boolean = False) As Variant
    Dim xLen As Integer
    Dim yLen As Integer
    
    xLen = Len(x)
    yLen = Len(y)
    
    MsgBox "x = " & x & " y = " & y
    
    'Create Zeroed Array of xLen+1 x yLen+1 dimensions (intentional extra space).
    ReDim L((xLen + 1), (yLen + 1)) 'indexing starts at 0.
    For i = 0 To (xLen + 1)
        For j = 0 To (yLen + 1)
            L(i, j) = 0
        Next j
    Next i
    
    MsgBox "Created 0'ed array L"
    
    'Build dynamic programming table from the bottom up.
    'Note that L[xLen][yLen] will contain an integer equal to the length
    'of the complete LCS.
    'Note that L[i][j] contains the length of the lcs of x[0..i] and y[0..j]
    For i = 0 To (xLen + 1)
        For j = 0 To (yLen + 1)
            If i = 0 Or j = 0 Then
                L(i, j) = 0
            ElseIf Mid(x, i - 1, 1) = Mid(x, i - 1, 1) Then
                L(i, j) = L(i - 1, j - 1) + 1
            Else
                L(i, j) = WorksheetFunction.Max(L(i - 1, j), L(i, j - 1))
            End If
        Next j
    Next i
    
    'Length of LCS
    Dim LCSlen As Integer
    LCSlen = L(xLen, yLen)
    
    MsgBox "Length of the LCS is " & LCSlen
    
    'Start from the right-most-bottom-most corner and store chars
    'one by on in LCS
    Dim LCS As String
    
    LCS = ""
    i = xLen
    j = yLen
    
    While i > 0 And j > 0
            'If current character in x and y are same, then current char
            'is part of the LCS. The L[xLen][yLen] is the location of the
            'fist charachter we will PUSH onto the front of the LCS string
            If Mid(x, i - 1, 1) = Mid(x, i - 1, 1) Then
                LCS = Mid(x, i - 1, 1) & Right(LCS, Len(LCS))
            
            'If not same, then find the larger of the two lengths in L[][]
            'then go in the direction of the larger value
            ElseIf L(i - 1, j) > L(i, j - 1) Then
                i = i + 1
            Else
                j = j + 1
            End If
    Wend
    
    If return_String Then
        ClosestMatch = LCS
    Else
        ClosestMatch = LCSlen
    End If
    
End Function

错误是因为 ElseIf Mid(x, i - 1, 1) = Mid(x, i - 1, 1) Then 行上的 i= 0 的值导致 Mid(x, i - 1, 1) 失败,因此函数崩溃。

我的推荐:

  1. 用户始终正确处理错误。
  2. 使用 Line Numbers 为您的代码编号并使用 ERL 获取有问题的行号。

这是一个例子

Option Explicit

Function ClosestMatch(ByVal x As String, ByVal y As String, Optional ByVal return_String As Boolean = False) As Variant
          Dim xLen As Integer
          Dim yLen As Integer
          Dim i As Long, j As Long, k As Long
          
10        On Error GoTo Whoa
          
20        xLen = Len(x)
30        yLen = Len(y)
          
40        MsgBox "x = " & x & " y = " & y
          
          'Create Zeroed Array of xLen+1 x yLen+1 dimensions (intentional extra space).
50        ReDim L((xLen + 1), (yLen + 1)) 'indexing starts at 0.
60        For i = 0 To (xLen + 1)
70            For j = 0 To (yLen + 1)
80                L(i, j) = 0
90            Next j
100       Next i
          
110       MsgBox "Created 0'ed array L"
          
          'Build dynamic programming table from the bottom up.
          'Note that L[xLen][yLen] will contain an integer equal to the length
          'of the complete LCS.
          'Note that L[i][j] contains the length of the lcs of x[0..i] and y[0..j]
120       For i = 0 To (xLen + 1)
130           For j = 0 To (yLen + 1)
140               If i = 0 Or j = 0 Then
150                   L(i, j) = 0
160               ElseIf Mid(x, i - 1, 1) = Mid(x, i - 1, 1) Then
170                   L(i, j) = L(i - 1, j - 1) + 1
180               Else
190                   L(i, j) = WorksheetFunction.Max(L(i - 1, j), L(i, j - 1))
200               End If
210           Next j
220       Next i
          
          'Length of LCS
          Dim LCSlen As Integer
230       LCSlen = L(xLen, yLen)
          
240       MsgBox "Length of the LCS is " & LCSlen
          
          'Start from the right-most-bottom-most corner and store chars
          'one by on in LCS
          Dim LCS As String
          
250       LCS = ""
260       i = xLen
270       j = yLen
          
280       While i > 0 And j > 0
                  'If current character in x and y are same, then current char
                  'is part of the LCS. The L[xLen][yLen] is the location of the
                  'fist charachter we will PUSH onto the front of the LCS string
290               If Mid(x, i - 1, 1) = Mid(x, i - 1, 1) Then
300                   LCS = Mid(x, i - 1, 1) & Right(LCS, Len(LCS))
                  
                  'If not same, then find the larger of the two lengths in L[][]
                  'then go in the direction of the larger value
310               ElseIf L(i - 1, j) > L(i, j - 1) Then
320                   i = i + 1
330               Else
340                   j = j + 1
350               End If
360       Wend
          
370       If return_String Then
380           ClosestMatch = LCS
390       Else
400           ClosestMatch = LCSlen
410       End If
          
LetsContinue:
          
420       Exit Function
Whoa:
430       ClosestMatch = Err.Description & " on line " & Erl
End Function

正在行动:

其他提示:

  1. 使用Option Explicit.
  2. 我用 MZ Tools 代替 Excel。如果你是一个认真的程序员,我绝对推荐使用它。

Excel “吞下”用户定义的函数错误并将它们包装到 Variant/Error 值中,这样任何抛出 VBA 运行 时间错误的函数return 调用工作表会出现 #VALUE! 错误。

诀窍是删除包装器并自己直接调用函数。

在VBIDE中,按Ctrl+G调出立即工具窗口,然后键入 ? 后跟函数名称及其参数:

?ClosestMatch("aabbaaaa", "aaaabbaa")

?PRINT 的 shorthand 所以如果一切顺利的话,函数 returns 是一个打印在下面的值:

?ClosestMatch("aabbaaaa", "aaaabbaa")
aa

但是如果出现任何问题并且函数抛出错误,您将得到 VBA 运行 次错误提示,并将直接带到负责 [=13] 的指令=] 工作表出现错误,通过使用您可以随意使用的调试器工具,您将能够:

  • 将鼠标悬停在任何变量上以查看其值
  • 调出 locals 工具窗口以查看所有变量及其值
  • 当前语句(黄色箭头)设置为函数中的任何其他语句
  • 逐步执行 (F8) 代码并一次执行一条语句
  • 放置和删除断点 (F9) 以在特定语句处停止执行
  • 恢复执行 (F5),即使在即时修改代码后

考虑使用 Debug.Print 语句而不是 MsgBox,打印到 即时 工具窗口,而不是弹出一个破坏性的消息框。

然后考虑编写几个测试方法,用各种参数组合调用你的函数,并对结果断言:如果函数returns 预期输出,测试通过,否则测试失败。当所有测试都通过时,您就知道您的函数将按预期适用于所有涵盖的案例。 Rubberduck(我几年前开始的一个免费开源 VBIDE 插件项目)为您提供了轻松编写和 运行 此类单元测试的工具,及其 静态代码分析 可以帮助您避免许多陷阱、初学者陷阱和过时的代码结构(例如 While...Wend 可以写成更标准的 Do While...Loop 结构)。

在盯着手表看太多时间之后 window...我有很多错误,我在应该是 y 的地方复制 x,然后在应该是 j 的地方输入 i .

因为我没能找到 VBA 寻找最长公共子序列的例子,这里是...

Public Function LCSMatch(ByVal x As Range, ByVal y As Range, Optional ByVal return_String As Boolean = False) As Variant
    Dim xLen As Integer
    Dim yLen As Integer
    
    xLen = Len(x)
    yLen = Len(y)
    
    
    'Create Zeroed Array of xLen+1 x yLen+1 dimensions (intentional extra space).
    ReDim L((xLen), (yLen)) 'indexing starts at 0.
    For i = 0 To (xLen)
        For j = 0 To (yLen)
            L(i, j) = 0
        Next j
    Next i
    
    'Build dynamic programming table from the bottom up...
    'Note that L[xLen][yLen] will contain an integer equal to the length
    'of the complete LCS.
    'Note that L[i][j] contains the length of the lcs of x[0..i] and y[0..j]
    For j = 0 To (yLen)
        For i = 0 To (xLen)
            If i = 0 Or j = 0 Then
                L(i, j) = 0
            ElseIf Mid$(x, i, 1) = Mid$(y, j, 1) Then
                L(i, j) = L(i - 1, j - 1) + 1
            Else
                L(i, j) = WorksheetFunction.Max(L(i - 1, j), L(i, j - 1))
            End If
        Next i
    Next j
    
    'Length of LCS
    Dim LCSlen As Integer
    LCSlen = L(xLen, yLen)

    
    'Start from the right-most-bottom-most corner and store chars
    'one by on in LCS
    Dim LCS As String
    
    LCS = ""
    i = xLen
    j = yLen
        
        
        
        While i > 0 And j > 0
            'If current character in x and y are same, then current char
            'is part of the LCS. The L[xLen][yLen] is the location of the
            'fist charachter we will PUSH onto the front of the LCS string
            If Mid$(x, i, 1) = Mid$(y, j, 1) Then
                LCSPart = Right$(LCS, Len(LCS))
                LCS = Mid$(x, i, 1) & LCSPart
                i = i - 1
                j = j - 1
                'GoTo Match
            'If not same, then find the larger of the two lengths in L[][]
            'then go in the direction of the larger value
            ElseIf L(i - 1, j) > L(i, j - 1) Then
                i = i - 1
            Else
                j = j - 1
            End If
'Match:
    Wend

    MsgBox "Length of the LCS is " & LCSlen
    MsgBox "LCS is " & LCS

    If return_String Then
        LCSMatch = LCS
    Else
        LCSMatch = LCSlen
    End If
    
End Function