7/2, 动态规划,字符串类


字符串 DP 类的很多典型问题,用这一张图都能说明白:

两个 String 放一起,从某个位置开始看看是否 match; 要是不 match 了,就得错开一位看;要是 match 了,就都往后挪一位看。在 Edit Distance 里面,稍微特殊一点,因为我们还有一个 replace 操作,可以直接修正当前 mismatch,让两个字符串都挪动一格位置。

于是对于每一个 String,就有了一个对于其位置敏感的维度 dp[i],代表从最左面开始的 i 个字符构成的字符串,也是子问题的结构定义。因为我们有两个 String 并且需要枚举位置之间可能的各种交错穿插,我们就需要两个维度 dp[i][j],代表第一个字符串中的 i 个字符和第二个字符串中的 j 个字符匹配情况。


Longest Common Subsequence(CLRS)

研究一个新问题时,最好还是从算法导论开始。

给定长度为 m 的字符串 {1,2,3...m} ,其 subsequences 的数量总数为 2^m,即对每个字符选择 “取 / 不取”。

于是有了下面的可视化版本~

这个版本的做法非常适合存储和输出 optimal path,也可以应用到 Longest Increasing Subsequence 中,用于 follow-up 情况下输出 sequence.

比如输入数组为 X = [x1, x2 .. xn],其排序后的数组为 X' = [x'1, x'2 .. x'n]

  • LIS 即 X 与 X' 的 LCS,判断条件完全一样,元素相等向右下走。如果每一步上都存行进方向,那么最后从右下角往左上出发,每一步指向左上角的箭头都是 LIS 的元素。

滚动数组优化版的代码.

最大值肯定在 dp[][] 的最右下角。

public class Solution {
    /**
     * @param A, B: Two strings.
     * @return: The length of longest common subsequence of A and B.
     */
    public int longestCommonSubsequence(String A, String B) {
        // write your code here
        if(A == null || B == null) return 0;
        int n = A.length();
        int m = B.length();
        if(n == 0 || m == 0) return 0;

        int[][] dp = new int[2][m + 1];

        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(A.charAt(i - 1) == B.charAt(j - 1)){
                    dp[i % 2][j] = dp[(i - 1) % 2][j - 1] + 1; 
                } else {
                    dp[i % 2][j] = Math.max(dp[(i - 1) % 2][j], dp[i % 2][j - 1]); 
                }

            }
        }

        return dp[n % 2][m];
    }
}

One Edit Distance

一开始未经思考尝试以 LCS 长度判断,但是这个思路是错的,因为 S 和 T 的 LCS 长度可以满足条件,但是 mismatch 的字符却不一定是在同一个位置。况且, LCS 的时间复杂度是 O(mn), 这题应该很明显有 O(n) 的解法。

正解借鉴了论坛的代码,其实非常简洁。

核心思想是,既然有且只有 1 个位置 mismatch,我们可以直接在找到 mismatch 位置上判断:

  • 让 n, m 为两个 String 的长度

  • 如果 m == n ,mismatch 之后的子字符串一定完全相等;

  • 否则长的那段在 i 之后的 substring 一定与短的以 i 开始的 substring 全等;

  • 如果在循环末尾还没有发现 mismatch,两个字符串末尾必然会有 1 的长度差,否则 S == T.

在字符串 DP 中,原始字符串上的 substring,等价于原始区间内的 subproblem.

public class Solution {
    public boolean isOneEditDistance(String s, String t) {
        int n = s.length();
        int m = t.length();

        if(Math.abs(n - m) > 1) return false;

        for(int i = 0; i < Math.min(m,n); i++){
            if(s.charAt(i) != t.charAt(i)){
                if(n == m) return s.substring(i + 1).equals(t.substring(i + 1));
                if(n > m) return s.substring(i + 1).equals(t.substring(i));
                if(n < m) return s.substring(i).equals(t.substring(i + 1));
            }
        }

        return Math.abs(n - m) == 1;
    }
}

Edit Distance

结合上一道题的错误来看,可以很容易看到只依靠 LCS 长度解决这道题的乏力;因为 LCS 长度和 edit distance 对字符串结构的利用是不一样的,mismatch 的字符可以出现错位,而 edit distance 不支持一次操作进行修正。算法导论上也写的非常清楚,鉴定两条 DNA 序列的相似度,substring 是一种思路(KMP),LCS是一种思路,而 edit distance 是另一种思路。

关于这个问题最好的slides, by Stanford

然而相同的是,这道题思考并寻找 optimal substructure 的思路是非常接近的,都是直接从最终答案 String Z 的结构 top-down 往回看。因为对于每个 string,我们对其任意操作都会构造出来一个只与它自己相差一个字符的新 string.

  • S[1,2,3..n] 为 String S;

  • T[1,2,3..m] 为 String T;

  • Z[1,2...k] 为最少修改之后的 String Z;

    • 注意这里 Z 可以有多个正解,因为可以有多个最优的正确操作。

    • 类似于 LCS,Z 也可以是多条路径。

于是;

  • 若 S[n] == T[m] ,则Z[1,2.. k - 1] 为 S[n - 1] 和 T[m - 1] 的解;

  • 若 S[n] != T[m]:

    • 最优解为 S[1,2 ..n] 与 T[1,2 .. m - 1] 构造而成,

      • 删掉 S[n]

      • OR 增加 T[m];

    • 最优解为 S[1,2 .. n - 1] 与 T[1,2 .. m] 构造而成

      • 删掉 T[m]

      • OR 增加 T[n];

    • 最优解为 S[1,2 .. n - 1] 和 T[1,2 .. m - 1] 构造而成

      • 直接把 S[n] 和 T[m] replace 成一样的。

来自 Stanford 课件,bottom - up 的 String 构造法。我们定义 D(i, j - 1) 为 insertion 是因为 i 是外循环位置,j 是内循环位置;因此当 i 固定,相对于 j 取了前一个位置 + 新字符的时候是 j 的 insertion; 而 D(i - 1, j) 代表 j 在新循环中并没有增加长度,反而从 i 里删除了。

考虑到所有操作都在双循环内完成,调换循环位置并不会影响算法正确性,但是会赋予每次操作相反的意义,即调换等价互补操作 "S + 1" 和 "T - 1".

其实和算法导论的图是一个意思,方向不同而已。

如果这题每种操作附带了 cost ,要求最小的 cost 怎么办?

不难看出,add 和 delete 作为互补操作,其 cost 是一样的;即使不一样,我们也可以在两个 String 的构造过程中,总是选择更小的那个。

public class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();

        int[][] dp = new int[n + 1][m + 1];

        for(int i = 1; i <= n; i++){
            dp[i][0] = i;
        }
        for(int i = 1; i <= m; i++){
            dp[0][i] = i;
        }

        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(word1.charAt(i - 1) == word2.charAt(j - 1)){
                // 注意这步考虑 dp[i - 1][j - 1]同时再考虑 
                // min(dp[i - 1][j], dp[i][j - 1]) + 1 也合乎逻辑,一样可以 AC 
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j - 1], 
                               Math.min(dp[i - 1][j], 
                                        dp[i][j - 1])) + 1; 
                }
            }
        }
        return dp[n][m];
    }
}

(Google, Snapchat面经)

Delete Operation for Two Strings 7/1

很久没看,纠结在同时允许删除两个String的元素怎么dp。复习完edit distance,迅速开窍,在字符串2删除实际上就是在字符串1里面插入,和edit distance一样,一次AC。

public class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(); int n=word2.length();
        int[][] dp = new int[m+1][n+1];
        for(int[] d :dp) Arrays.fill(d, Integer.MAX_VALUE); 
        for(int i=0;i<=m;i++)
            dp[i][0] = i;
        for(int j=0;j<=n;j++)
            dp[0][j] = j;
        dp[0][0] = 0;
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++){
                if(word1.charAt(i-1)==word2.charAt(j-1))
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = Math.min(dp[i][j], dp[i][j-1]+1);
                    dp[i][j] = Math.min(dp[i][j], dp[i-1][j]+1);
                }
            }
        return dp[m][n];
    }
}

Minimum insertions to form a palindrome

和 Edit distance 非常像,考虑到 add/delete 在构造 string 上的等价性质,问题的 optimal substructure 即为

  • dp[i][j] = substring(i,j) 范围内,构造 palindrome 的最小编辑次数

    • 如果 s[i] == s[j]

      • dp[i][j] = dp[i + 1][j - 1] (不需要操作)

      • 同时考虑 i , j 相邻的情况

    • 如果 s[i] != s[j],那么我们可以经 add/delete 操作构造出当前的 s(i, j)

      • s(i + 1, j) + ADD

      • s(i, j - 1) + ADD

注意这题不支持 replace,如果支持的话,dp[i][j] 还要看一个新状态,dp[i + 1][j - 1]

public class PalindromeInsert {

    public static int getCost(String s){
        int len = s.length();
        int[][] dp = new int[len][len];
        char[] arr = s.toCharArray();
        for(int i=0;i<len;i++){
            dp[i][i] = 0;
            if(i<len-1){
                if(arr[i]==arr[i+1]) dp[i][i+1] = 0;
                else dp[i][i+1] = 1;
            }
        }

        for(int l=3;l<=len;l++)
            for(int i=0;i+l-1<len;i++){
                if(arr[i]==arr[i+l-1])
                    dp[i][i+l-1] = dp[i+1][i+l-2];
                else
                    dp[i][i+l-1] = Math.min(dp[i+1][i+l-1], dp[i][i+l-2])+1;
            }
        return dp[0][len-1];
    }

    public static void main(String[] args){
        String s = "geeks";
        int cost = getCost(s);
        System.out.println(cost);
    }
}

WildCard Matching

Hard 难度题,字符串 DP.

首先我们可以观察到最终结果只需要返回 true / false ,而不是很在乎具体到哪个位置的时候 wildcard 匹配了什么字符,所以是一个用 DP 的信号。

顺着这个思路想,这题具有这章其他 DP 类问题都非常相似的结构和 optimal substructure ,即都是 1 / 2 个 string 从小往大“生长”,每一步需要做 match 的判断构造出更长 string 的正解;以这题的结构,不难想出 dp 的数组结构就是 dp[i][j] 代表 s 的前 i 个字符串能否和 p 的前 j 个字符串成功匹配,以 dp[n][m] 为最终解。

剩下的问题就是,如何正确识别所有情况。

s[i] , p[j] 为 s , p 的第 i / j 个字符,略去 off-by-one 的 index 问题

  • 当 p[j] = 常规字母时;

    • 如果 s[i] == p[j],当前位置 match;

    • 同时如果之前的字符串 dp[i - 1][j - 1] 也 match ,则 dp[i][j] match;

  • 当 p[j] 为 '?' 时;

    • 当前位置一定 match,只看 dp[i - 1][j - 1] 是否也 match;

  • 当 p[j] 为 '*' 时;

    • 只 match 当前一个字符,看 dp[i - 1][j - 1];

      • LeetCode 测试了下,其实这行拿掉也是正确的

    • 不 match 任何字符,看 dp[i][j - 1] (忽略 p[j] 的存在)

    • match 多个字符,看 dp[i - 1][j]

      • 注意这里由于循环结构的关系,其实对于每一个 j ,我们会去考虑 [0 , i-1] 所有的可能性,所以可以用一个状态指代所有前面的 match.

最后要注意一下 dp[0][0], dp[0][i] 的初始化。

public boolean isMatch(String s, String p) {
        if(s == null || p == null) return false;
        int n = s.length();
        int m = p.length();

        boolean[][] dp = new boolean[n + 1][m + 1];

        dp[0][0] = true;

        for(int i = 1; i <= m; i++){
            if(p.charAt(i - 1) == '*') dp[0][i] = true;
            else break;
        }

        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(p.charAt(j - 1) == '?' || 
                   s.charAt(i - 1) == p.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1];
                } else if(p.charAt(j - 1) == '*'){
                    dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
                }
            }
        }

        return dp[n][m];
    }

Regular Expression Mathcing

也是 Hard 题,长得还和 Wildcard Matching 特别像。

最大的不同是,在这里 '*' 号是和其前一个字符有联系的,和其前一个字符一起,代表着“多个或0个星号前面的字符”。 这里我们需要假设 p 不会以星号开始,也不会有连续多个星号出现,不然现有题意描述是无法解决这些问题的。

  • 遇到常规字母和 '.' 的处理和上一题没有任何区别;

  • 遇到 p[j] 为星号时:

    • 如果 p[j - 1] 是 '.', 那么这个星号

      • dp[i - 1][j] 以当前星号匹配一个或多个多个字符;

      • dp[i][j - 1] 只让 p[j - 1] 匹配,当前星号不匹配字符;

      • dp[i][j - 2] 同下。

    • 否则,星号位置能否匹配取决于 dp[i][j - 2],即让(p[j - 1] + 当前星号)都不匹配任何字符。

以这两道题看,dp[i - 1][j] 都代表着 “以 p 当前 * 字符,匹配 s 的[1,n]长度字符”

这题dp[0][i]初始化和 Wildcard 不太一样,因为会有 c*a*b 这种情况,多个星号跳着出现,不要立刻 break 掉,而要扫到底,dp[0][i] 要看 dp[0][i - 2];

  public boolean isMatch(String s, String p) {

    if (s == null || p == null) {
        return false;
    }
    boolean[][] dp = new boolean[s.length()+1][p.length()+1];
    dp[0][0] = true;
    for (int i = 0; i < p.length(); i++) {
        if (p.charAt(i) == '*' && dp[0][i-1]) {
            dp[0][i+1] = true;
        }
    }
    for (int i = 0 ; i < s.length(); i++) {
        for (int j = 0; j < p.length(); j++) {
            if (p.charAt(j) == '.') {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == s.charAt(i)) {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == '*') {
                if (p.charAt(j-1) != s.charAt(i) && p.charAt(j-1) != '.') {
                    dp[i+1][j+1] = dp[i+1][j-1];
                } else {
                    dp[i+1][j+1] = (dp[i+1][j] || dp[i][j+1] || dp[i+1][j-1]);
                    /*
                     If p.charAt(j) == '*': 
                     here are two sub conditions:
               1   if p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2]  //in this case, a* only counts as empty
               2   if p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':
                              dp[i][j] = dp[i-1][j]    //in this case, a* counts as multiple a 
                           or dp[i][j] = dp[i][j-1]   // in this case, a* counts as single a
                           or dp[i][j] = dp[i][j-2]   // in this case, a* counts as empty
                    */
                }
            }
        }
    }
    return dp[s.length()][p.length()];
}

递归

    public boolean isMatch(String s, String p) {
        if(p.length()==0)
            return s.length()==0;
        if(p.length()==1)
            return (s.length()==1 && (s.charAt(0)==p.charAt(0)||p.charAt(0)=='.'));
        if(p.charAt(1)=='*'){
            if(isMatch(s, p.substring(2)))
                return true;
            else if(s.length()>0 && (s.charAt(0)==p.charAt(0)||p.charAt(0)=='.'))
                return isMatch(s.substring(1), p);
        }
        if(s.length()>0 && (s.charAt(0)==p.charAt(0)||p.charAt(0)=='.'))
            return isMatch(s.substring(1),p.substring(1));
        return false;
    }

Word Break

这是一道很像 rod-cutting 还有 palindrome paritioning 的题,利用的结构也一样。

要保证算法的正确性,我们要迭代考虑题目中所有的 substring 组合,不然可能会漏掉正解。考虑到题目可能的 substring 数量,这题的时间复杂度至少为 O(n^2),因此我们能做的就是利用好 substring 相互覆盖的地方避免重复计算。

public class Solution {
    public boolean wordBreak(String s, Set<String> wordDict) {
        int n = s.length();
        boolean[][] canBreak = new boolean[n][n];
        for(int i = 0; i < n; i++){
            for(int j = i; j >= 0; j--){
                String str = s.substring(j, i + 1);
                if(wordDict.contains(str)) canBreak[i][j] = canBreak[j][i] = true;
                if(j > 0 && canBreak[j][i] && canBreak[0][j - 1]) canBreak[0][i] = canBreak[i][0] = true;
            }
        }

        return canBreak[0][n - 1];
    }
}

在如上代码 AC 之后,思考之后可以发现两个很明显的优化:

optimal substructure 是,如果 [0, j] 可以 break 并且 [j + i , i] 在字典里,那么 [0 , j] 就可以 break.

  • 我们不需要存储所有区间是否可以 break,只取从 index = 0 开始到 i 为止是否可以就行了;空间优化成了 O(n);

  • 因此如果回头扫到了当前位置可以 break 的时候,我们就可以做 early termination.

  • 同时对于每个考虑的中间位置 j ,如果在 j 上不能 break,那么取 substring 和检查字典的必要都没有。

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        if(s==null || s.length()==0) return true;
        int n = s.length();
        boolean[] dp= new boolean[n+1]; Arrays.fill(dp, false);dp[0] = true;
        for(int i=1;i<=n;i++){
            for(String word : wordDict){
                if(i-word.length()<0) continue;
                if(word.equals(s.substring(i-word.length(),i)))
                    dp[i] = dp[i] || dp[i-word.length()];
            }
        }
        return dp[n];
    }
}

Word Break II

这题有简单暴力的 dfs + backtracking,然而为了优化计算时间,也要利用备忘录法和记忆化搜索。

这题做出来容易,AC 有点难,因为用时卡的厉害,有坑爹的 test case :

【s ="aaaaaaaaaaaaaaaaa...aaaa" , dict = "a", "aa", "aaa", "aaaa"】

硬搜的时间复杂度为 O(2^n),搜索树结构见算法导论的 rod-cutting.

最容易想到的几种优化方式:

  • 预处理,建 boolean[][] 表示所有可能区间内是不是 word,方便dfs 时判断要不要进入下一层循环;

  • 记录字典里最长单词长度作为 step size,dfs 每次循环寻找位置时限制最远的搜素距离;(用于优化 s 相对于 word 非常长的情况)

然而单独靠以上两种优化都还不够。。

https://leetcode.com/discuss/80266/java-dfs-memoization-dfs-and-pruning-solutions-with-analysis

事实证明在这题里,直接用 HashSet + substring 检查字典,要比预处理计算 boolean[][] 快。 (8ms vs. 22ms)

O(len(wordDict) ^ len(s / minWordLenInDict))

Because there're len(wordDict) possibilities for each cut,同时空间占用可能会比较大。

public class Solution {

HashMap&lt;String, List&lt;String&gt;&gt; map = new HashMap&lt;String, List&lt;String&gt;&gt;\(\);

public List&lt;String&gt; wordBreak\(String s, List&lt;String&gt; wordDict\) {

    return dfs\(s, wordDict\);

}



public List&lt;String&gt; dfs\(String s, List&lt;String&gt; wordDict\){

    if\(map.containsKey\(s\)\) return map.get\(s\);

    ArrayList&lt;String&gt; cur = new ArrayList&lt;String&gt;\(\);

    if\(s.length\(\)==0\){

        cur.add\(""\);

        return cur;

    }

    for\(String word : wordDict\){

        if\(s.startsWith\(word\)\){

            List&lt;String&gt; sublist = dfs\(s.substring\(word.length\(\)\), wordDict\);

            for\(String sub : sublist\){

                cur.add\(word + \(sub.length\(\)==0?"":" "\) +sub\);

            }

        }

    }

    map.put\(s, cur\);

    return cur;

}

}

Interleaving String

首先从题目结构来看,和这章的前几道字符串 DP 非常相似,都是一个字符串 “构造问题”,即用小的 substring 通过生长和拼接,构造出更大的目标 string. 一般这类问题有天然的 bottom-up 思路,当然,top-bottom 的递归思路也是完全可行的。

首先贴一个自己写的暴力解法,三维 dp,dp[i][j][k] 代表着“s1 的前 i 个字符和 s2 的前 j 个字符,能否构造成 s3 的前 k 个字符”。

下面代码时间复杂度 O(s1.len * s2.len * s3.len),可以 AC,但是显然还有优化的空间。

简单观察之后发现正确答案的限制条件: k = i + j,换句话说,k 可以用 i + j 表示出来,因此不需要以 k 作为单独维度遍历。

同时我们也要处理好 i = 0 和 j = 0 的初始化条件。

时间空间复杂度 O(s1.len * s2.len)

自己写的时候用了S3和S1下标i j做dp,这样 s2下标是i-j. 不方便使用

public class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        if(s1 == null || s2 == null || s3 == null) return false;
        if(s3.length() != s1.length() + s2.length()) return false;
        if(s1.length() == 0) return s2.equals(s3);
        if(s2.length() == 0) return s1.equals(s3);

        int s1Len = s1.length();
        int s2Len = s2.length();
        // dp[i][j][k] if the first i chars from s1 and first j chars from s2
        // can interleave to produce first i + j chars of s3

        boolean[][] dp = new boolean[s1Len + 1][s2Len + 1];

        dp[0][0] = true;
        for(int i = 0; i < s1Len; i++){
            if(s1.charAt(i) == s3.charAt(i) && dp[i][0]) dp[i + 1][0] = true;
        }
        for(int i = 0; i < s2Len; i++){
            if(s2.charAt(i) == s3.charAt(i) && dp[0][i]) dp[0][i + 1] = true;
        }

        for(int i = 1; i <= s1Len; i++){
            for(int j = 1; j <= s2Len; j++){
                char char1 = s1.charAt(i - 1);
                char char2 = s2.charAt(j - 1);
                char char3 = s3.charAt(i + j - 1);

                if(char1 == char3 && dp[i - 1][j])  dp[i][j] = true;
                if(char2 == char3 && dp[i][j - 1])  dp[i][j] = true;
            }
        }

        return dp[s1Len][s2Len];
    }
}

Student Attendance Record II

先用DFS剪支,length 13的时候超时。

public class Solution {
    int count =0;
    public int checkRecord(int n) {
        dfs(n, new StringBuilder());
        return count;
    }

    public void dfs(int n, StringBuilder sb){
        if(!isR(sb.toString())) return;
        if(sb.length()==n && isR(sb.toString())){
            count++;return;
        }
        char[] arr = new char[]{'A','L','P'};
        for(int i=0;i<arr.length;i++){
            sb.append(arr[i]);
            dfs(n, sb);
            sb.deleteCharAt(sb.length()-1);
        }
    }

    public boolean isR(String s){
        return !s.matches(".*LLL.*") && !s.matches(".*A.*A.*");
    }
}

DFS不行,上DP。纠结在怎么表示连续的L,写了几个式子后觉得都太繁琐,似对似错。论坛解法

dp[i][j][k]表示i个字符,最多(at most)有j个‘A’,k个‘L’,这个比较好想。dp[i]与dp[i-1]的关系如何递推,需要琢磨。这里可以看出,dp[i-1][j-1][k-1] 和dp[i-1][j][k] 是被包含与包含的关系,状态不独立。常见的dp状态都是独立的,不独立时会采用local(以当前字符结尾) global(不一定以当前字符结尾)。而这道题巧就巧在可以dp。

dp[i-1][j][k] 后面接'P',可以形成dp[i][j][k];

dp[i-1][j-1][k] 后面接‘A’,可以形成dp[i][j][k];

dp[i-1][j][k-1] 后面接‘L’,可以形成dp[i][j][k];

诚然dp[i-1][j][k-1] 和dp[i-1][j][k] 有overlap,然而后面接了不同的字符,dp[i][j][k]没有重复计算(overcount)

public class Solution {
    public int checkRecord(int n) {
        int MOD = 1000000007;
        int[][][] dp = new int[n+1][2][3];
        dp[0] = new int[][]{ {1,1,1},{1,1,1}};
        for(int i=1;i<=n;i++)
            for(int j=0;j<2;j++)
                for(int k=0;k<3;k++){
                    int val = dp[i - 1][j][2]; // ...P
                    if (j > 0) val = (val + dp[i - 1][j - 1][2]) % MOD; // ...A
                    if (k > 0) val = (val + dp[i - 1][j][k - 1]) % MOD; // ...L
                    dp[i][j][k] = val;
                }

        return dp[n][1][2];
    }
}

论坛上后续解法将递推转化为vector矩阵相乘,最后将复杂度化为O(logn), 有空再研究。

results matching ""

    No results matching ""