【问题标题】:Solving "string reduction" challenge解决“减少字符串”挑战
【发布时间】:2012-06-29 23:27:09
【问题描述】:

我在 interviewstreet.com 上看到过各种讨论和尝试解决"String reduction" 问题的代码,但没有一个是通过动态编程实现的。

列在Dynamic Programming部分下,问题描述如下:

给定一个由 a、b 和 c 组成的字符串,我们可以执行以下操作:取任意两个相邻的不同字符并将其替换为第三个字符。例如,如果“a”和“c”相邻,则可以将它们替换为“b”。

重复应用此操作可以得到的最小字符串是多少?

可以使用穷举蛮力搜索来解决问题,有效地创建所有可能替换的树:

// this is more or less pseudo code from my head
int GetMinLength(string s)
{
    // solve edge cases
    if (s.Length == 1) return 1;
    if (s.Length == 2) return ReduceIfPossible(s);

    // recursive brute force
    int min = s.Length;
    for (int i = 0; i<s.Length-1; i++)
    {
        if (s[i] != s[i+1])
        {
            var next = GetMinLength(
                  s.Substring(0, i) + 
                  Reduce(s[i], s[i + 1]) +
                  s.Substring(i + 2)
                  );

            if (next < min) min = next;
        }
    }
}

这对于较大的N (N &lt;= 100) 显然会失败,所以我试图将其分解为更小的子问题,记忆它们并合并结果。

问题是我无法确定具有"optimal substructure" 的状态,需要应用动态编程(或者换句话说,“合并”来自子问题的结果)。将字符串的一部分最小化并不能保证最终的字符串确实是最小的解决方案。

在这种情况下,什么是子问题“状态”,可以合并到最终解决方案中?

【问题讨论】:

    标签: algorithm dynamic-programming


    【解决方案1】:

    让这个问题变得棘手的是,您需要将其视为连续的 2 个动态规划问题。

    1. 建立一个表格,按您最终使用的字符、开始位置、所有可能的结束位置(可以简化为该字符的块)。
    2. 字符串的最后一个i 字符可以减少到的最小长度。 (您在步骤 1 中构建的表可用于将此问题递归地简化为已解决的子问题。)

    第二个提供你的答案。如果你已经解决了第一个问题,那就更简单了。

    【讨论】:

    • @Dilbert 没错。但是让它成为一个嵌套查找 - 你可以先查找一个东西然后另一个。 (这个表最容易生成,从字符串的末尾开始,然后向后工作。)
    • 您能否进一步说明第一步?我想你想让我建立一个长度小于或等于问题字符串的所有块的表,可以减少到三个字符之一。但是你是什么意思 - '按你结束的角色,按开始位置'?我是 DP 的菜鸟。
    • @gautam1168 我的意思正是我所说的。 :-) 给定一个最终字符和一个起始位置,您需要一个范围的所有端点的列表,您可以从该起始位置折叠到该端点并以该字符结束。这需要为每个起点解决一个 DP 问题,如果您已经为所有后续问题解决了相同的问题,则可以解决该问题。 (请注意,您将不得不对范围进行大量合并。查找“优先队列”以获取有用的信息。)
    【解决方案2】:

    剧透警报代码:

    public static int findMinReduction(String line){
        //Pseudocode:
        //Count the number of occurences of each letter in the input string.
        //If two of these counts are 0, then return string.length
        //Else if (all counts are even) or (all counts are odd), then return 2
        //Else, then return 1
    
        int len = line.length();
        String a_reduce = line.replaceAll("c", "").replaceAll("b", "");
        String b_reduce = line.replaceAll("a", "").replaceAll("c", "");
        String c_reduce = line.replaceAll("a", "").replaceAll("b", "");
    
        int a_occurances = a_reduce.length();
        int b_occurances = b_reduce.length();
        int c_occurances = c_reduce.length();
    
        if ((a_occurances == b_occurances && a_occurances == 0) || 
           (a_occurances == c_occurances && a_occurances == 0) || 
           (b_occurances == c_occurances && b_occurances == 0)){
            return len;
        }
        else{
            if (a_occurances % 2 == 0 && 
                b_occurances % 2 == 0 && 
                c_occurances % 2 == 0){
                return 2;
            }
            if (a_occurances % 2 != 0 
                && b_occurances % 2 != 0 && 
                c_occurances % 2 != 0){
                return 2;
            }
        }
        return 1;
    }
    

    复杂性:

    这是一个 O(n) 时间复杂度操作,随着输入大小的增加,要完成的工作量与输入大小成线性关系。这是闪电般的速度,我们可以处理兆字节大小的字符串,并且仍然可以在几分之一秒内处理它们。

    在此处找到的算法完整分析了该算法的工作原理:

    stumped on a Java interview, need some hints

    【讨论】:

    • @Alderath's answer you linked to runs in linear time,不是多项式,它与您的 cmets 中的伪代码相同。这不是“几乎零时间”,它很简单O(n)。因此,与他的答案和@Matteo 的答案相比,您只是用Java 编写了程序。同样,不确定您所说的“Java8”是什么意思,就好像您使用的是版本 8 中的任何内容一样。
    • 修复了它是通用的 java 并且 answer 没有 java8 特定的结构。
    【解决方案3】:

    您首先要创建一个描述解决方案理论的结构。它包括考虑的字符数,到目前为止的编码字符串,以及理论的最坏情况和最佳情况。

    一开始只有一个理论 - 没有处理任何字符。最好的情况是长度为 1 的字符串(例如,一条规则始终适用,字符串可以缩减为一个字符,最坏的情况是 N,没有适用的规则)

    encoded string = "";
    encoded index = 0;
    worst case = N; 
    best case = 1;
    

    现在开始将索引加一,并在编码字符串中添加一个字符。如果没有适用的规则,你就保持这个理论不变。如果某个规则确实适用,您必须做出决定——要么应用该规则,要么不应用该规则。因此,当您添加一个角色时,您会复制适用于最后一个角色的每条规则的理论,并为没有应用任何规则保留一个版本。并且您更新每个理论的最佳情况和最坏情况。

    起初,理论的数量会迅速增加。但最终你会遇到这样一种情况,即某些理论的最坏情况优于其他理论的最佳情况。

    因此,每当您推进索引时,您都会删除最佳情况比具有最佳最差情况的理论更差的理论。随着指数接近N,大部分理论都会退出。

    【讨论】: