最大UCC字串计算题解 | 2024 刷题 掘金 MarsCode 困难题

124 阅读5分钟

问题描述

小S有一个由字符 'U' 和 'C' 组成的字符串 𝑆S,并希望在编辑距离不超过给定值 𝑚m 的条件下,尽可能多地在字符串中找到 "UCC" 子串。

编辑距离定义为将字符串 𝑆S 转化为其他字符串时所需的最少编辑操作次数。允许的每次编辑操作是插入、删除或替换单个字符。你需要计算在给定的编辑距离限制 𝑚m 下,能够包含最多 "UCC" 子串的字符串可能包含多少个这样的子串。

例如,对于字符串"UCUUCCCCC"和编辑距离限制m = 3,可以通过编辑字符串生成最多包含3个"UCC"子串的序列。

测试样例

样例1:

输入:m = 3,s = "UCUUCCCCC"
输出:3

样例2:

输入:m = 6,s = "U"
输出:2

样例3:

输入:m = 2,s = "UCCUUU"
输出:2

解释

样例1:可以将字符串修改为 "UCCUCCUCC"(2 次替换操作,不超过给定值 m = 3),包含 3 个 "UCC" 子串。

样例2:后面插入 5 个字符 "CCUCC"(5 次插入操作,不超过给定值 m = 6),可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。

样例3:替换最后 2 个字符,可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。

求解思路

题目的要求是在编辑距离不超过 m 的条件下,长度为 length 的字符串最多可以形成的 "UCC"数量。 这题是一道典型的动态规划求解的问题。动态规划的求解步骤为:

定义状态

dp[i][j]表示当考虑前面长度为i的子字符串时,编辑j次的最大"UCC"字串个数。因此数组长度很明显0<=i<=s.length,0<=j<=m。

定义阶段

阶段数就是字符串的长度,即s.length。

设置初始状态

dp[0][j] = 0 长度为0的字符串,"UCC"字符串个数为0。

状态转移方程

状态转移的情况分以下几类:

  1. 不考虑第 i 个字符,则取前一个字符的最优解,即dp[i][j]=dp[i-1][j]
  2. 只考虑第 i 个字符, 在当前字符前面或后面添加 U 字符和 C 字符构成 UCC ,这种情况每次需要两次额外的编辑次数,而UCC字符串会加一。dp[i][j] = Math.max(dp[i-1][j-2]+1, dp[i][j]);
  3. 考虑第 i 符和第 i-1 字符,也就是假设 i-1 字符考虑的是第一种情况,没有构成 UCC ,如果当前第 i 个字符是 U ,那么需要进行一次替换编辑和一次插入编辑;如果第 i 个字符是 C ,那么只需要进行一次插入编辑。dp[i][j] = Math.max(dp[i-2][j-count]+1, dp[i][j]); 其中count是编辑次数。
  4. 考虑第 i 字符, i-1字符和 i-2 字符,即有三个字符组成的UCC,需要进行的是替换操作,将这三个字符通过替换编辑改成 UCC 字符串。 dp[i][j] = Math.max(dp[i-3][j-count]+1, dp[i][j]); 其中count是编辑次数。
  5. 当不考虑任何字符,直接在任何位置添加 UCC 字符串,需要三次编辑。dp[length][j] = Math.max(dp[length][j], dp[length][j-3] + 1);

这里我们并不需要考虑删除操作,因为不考虑的字符直接通过状态转移 1 不考虑就可以了,任何通过删除达到 UCC 字符串的编辑次数都不会小于状态转移 2。 例如如果 UCUC 通过删除第二个 U 构成 UCC,可以直接在后面添加一个C就可以达成目的。

完整代码如下

import java.util.Arrays;  
  
public class Main {  
  
    public static int solution(int m, String s) {  
        // write code here  
        int length = s.length(); // 字符串长度  
        // 定义状态  
        int dp[][] = new int[length+1][m+1]; // 当考虑到第i个字符, 编辑j次的最大子串长度  
        // 初始化状态 -- 初始化为0  
        for(int i=0; i<=length; i++){  
            Arrays.fill(dp[i], 0);  
        }  
        // 状态转移, 从 1 开始到 字符串长度 n  
        for(int i=1; i<=length; i++){  
            // 不考虑当前字符, 则取前一个字符的最优解  
            for(int j=0; j<=m; j++){  
                dp[i][j] = dp[i-1][j];  
        }  
        // 只考虑当前字符, 在当前字符前面或后面添加U字符和C字符构成UCC  
        for(int j=2; j<=m; j++){  
            // 需要两次编辑  
            dp[i][j] = Math.max(dp[i-1][j-2]+1, dp[i][j]); // 与当前最优解比较  
        }  
        // 考虑当前字符和前一个字符  
        if(i>=2){  
            int count = 0;  
            if(s.charAt(i-1) == 'U')  
                count = 2; //需要两个编辑,一次变化一个添加  
            else  
                count = 1;  
            for(int j = count; j<=m; j++){  
                // 需要count次编辑  
                dp[i][j] = Math.max(dp[i-2][j-count]+1, dp[i][j]); // 与当前最优解比较  
            }  
        }  
        // 考虑当前字符和前面两个字符,即有三个字符组成的UCC,需要进行的是替换操作  
        if(i>=3){  
            // 计算子字符串  
            String subString = s.substring(i-3, i);  
            int count = 0;  
            if(subString.charAt(0) != 'U')  
                count++;  
            if(subString.charAt(1) != 'C')  
                count++;  
            if(subString.charAt(2) != 'C')  
                count++;  
            for(int j = count; j<=m; j++){  
                // 需要count次编辑  
                dp[i][j] = Math.max(dp[i-3][j-count]+1, dp[i][j]); // 与当前最优解比较  
            }  
        }  
    }  
        for(int j=3; j<=m; j++){  
            dp[length][j] = Math.max(dp[length][j], dp[length][j-3] + 1); // 直接加一个CUU字符串  
        }  
        // 输出结果  
        return dp[length][m];  
    }  
      
    public static void main(String[] args) {  
        System.out.println(solution(3, "UCUC") == 3);  
        System.out.println(solution(3, "UCUUCCCCCU") == 3);  
        System.out.println(solution(6, "U") == 2);  
        System.out.println(solution(2, "UCCUUU") == 2);  
    }  
}

总结

当一个问题满足最优性原理,也就是无论过程的初始状态和初始决策是什么,其余的决策都必须相对于初始决策所产生的状态构成一个最优决策序列。就可以使用动态规划来求解。

动态规划的步骤主要就包括定义状态和阶段、设置边界条件(初始状态)、定义状态转移方程,状态转移需要具体分析,也是最重要和最难的一步,关键在于考虑到所有情况。

(个人总结,若存在错误请指正)