如何通过反转子字符串找到按字典顺序排列的最小字符串?

时间:2017-09-15 16:30:17

标签: string algorithm lexicographic

我有一个字符S,其中包含ab&#39}。执行以下操作一次。目的是获得按字典顺序排列的最小字符串。

操作:正好反转S

的一个子字符串

e.g。

  1. 如果S = abab然后Output = aabb(字符串ba的反向S
  2. 如果S = abba然后Output = aabb(字符串bba的反向S
  3. 我的方法

    案例1:如果输入字符串的所有字符都相同,那么输出将是字符串本身。

    案例2 如果S的格式为aaaaaaa....bbbbbb....,那么答案将为S本身。

    否则:b中找到S的第一次出现,说位置为i。字符串S看起来像

    aa...bbb...aaaa...bbbb....aaaa....bbbb....aaaaa...
         |
         i   
    

    为了获得按字典顺序排列的最小字符串,将被反转的子字符串从索引i开始。请参阅下文,了解可能的结尾j。

    aa...bbb...aaaa...bbbb....aaaa....bbbb....aaaaa...
         |           |               |               |
         i           j               j               j
    

    为每个j反转子串S[i:j]并找到最小的字符串。 算法的复杂性为O(|S|*|S|),其中|S|是字符串的长度。

    有没有更好的方法来解决这个问题?可能是O(|S|)解决方案。

    我在想,如果我们能够在线性时间内选择正确的j,那么我们就完成了。我们将选择a的数量最大的j。如果有一个最大值,那么我们就解决了问题但是如果不是这样的话怎么办?我已经尝试了很多。请帮忙。

2 个答案:

答案 0 :(得分:1)

所以,我想出了一个算法,它似乎比O(| S | ^ 2)更有效,但我不太确定它的复杂性。这是一个粗略的大纲:

  1. 前导a's的条带,存储在变量start
  2. 将字符串的其余部分分组为字母块。
  3. 查找序列最长a's
  4. 的组的索引
  5. 如果只剩下一个index,请转到10。
  6. 过滤这些索引,以便在逆转后[{1}}的[第一]组的长度最小。
  7. 如果只剩下一个b's,请转到10。
  8. 过滤这些索引,以便在逆转后index(不包括前导a's)的[第一]组的长度最小。
  9. 如果只剩下一个a's,请转到10。
  10. 回到5,除了此次检查indexa's的[second / third / ...]组。
  11. 返回b's,以及最多start的反转组以及剩余的组。
  12. 由于任何正在反转的子字符串都以index开头并以b结尾,因此没有两个假设的反转是回文,因此两次反转不会产生相同的输出,从而保证有一个独特的最优解决方案,算法将终止。

    我的直觉说这种方法可能是O(log(| S |)* | S |),但我不太确定。下面提供了Python中的一个示例实现(虽然不是很好的实现)。

    a

答案 1 :(得分:1)

TL; DR:这里的算法只迭代字符串一次(对于有限的字符串长度,使用O(| S |) - 复杂度)。我在下面解释它的例子有点啰嗦,但算法非常简单:

  
      
  • 迭代字符串,并将其值更新为解释为反向(lsb-to-msb)二进制数。
  •   
  • 如果您发现零序列的最后一个零长于当前最大值,则存储当前位置和当前反向值。从那时起,还要更新此值,将字符串的其余部分解释为转发(msb-to-lsb)二进制数。
  •   
  • 如果您发现零序列的最后一个零与当前最大值一样长,则将当前反向值与存储的终点的当前值进行比较;如果它更小,则将终点替换为当前位置。
  •   

所以你基本上比较字符串的值,如果它被反转到当前点,字符串的值如果它只被反转到(迄今为止)最佳点,并且更新这个最佳点即时。

这是一个快速的代码示例;毫无疑问,它可以更优雅地编码:



function reverseSubsequence(str) {
    var reverse = 0, max = 0, first, last, value, len = 0, unit = 1;
    
    for (var pos = 0; pos < str.length; pos++) {
        var digit = str.charCodeAt(pos) - 97;                   // read next digit
        if (digit == 0) {
            if (first == undefined) continue;                   // skip leading zeros
            if (++len > max || len == max && reverse < value) { // better endpoint found
                max = len;
                last = pos;
                value = reverse;
            }
        } else {
            if (first == undefined) first = pos;                // end of leading zeros
            len = 0;
        }
        reverse += unit * digit;                                // update reverse value
        unit <<= 1;
        value = value * 2 + digit;                              // update endpoint value
    }
    return {from: first || 0, to: last || 0};
}
var result = reverseSubsequence("aaabbaabaaabbabaaabaaab");
document.write(result.from + "&rarr;" + result.to);
&#13;
&#13;
&#13;

(每当找到零时,可以通过比较reversevalue来简化代码,而不仅仅是在遇到最大长零序列的末尾时。(

您可以创建一个只迭代输入一次的算法,并且可以通过跟踪两个值来处理未知长度的传入流:整个字符串的值被解释为反向(lsb到msb)二进制数,以及一个部分反转的字符串的值。只要反向值低于存储的最佳终点的值,就会找到更好的终点。

以此字符串为例:

aaabbaabaaabbabaaabaaab

或者,为了简单起见用零和1写的:

00011001000110100010001   

我们遍历前导零,直到找到第一个:

0001
   ^

这是我们想要逆转的序列的开始。我们将开始将零和1的流解释为反向(lsb-to-msb)二进制数,并在每个步骤后更新此数字:

reverse = 1, unit = 1  

然后在每一步,我们将单位加倍并更新反向数字:

0001        reverse = 1
00011       unit = 2; reverse = 1 + 1 * 2 = 3
000110      unit = 4; reverse = 3 + 0 * 4 = 3
0001100     unit = 8; reverse = 3 + 0 * 8 = 3

此时我们找到了一个,并且零序列结束了。它包含2个零,当前是最大值,因此我们将当前位置存储为可能的终点,并存储当前的反向值:

endpoint = {position = 6, value = 3} 

然后我们继续迭代字符串,但在每一步,我们更新可能的端点的值,但现在作为正常的(msb-to-lsb)二进制数:

00011001      unit = 16; reverse = 3 + 1 * 16 = 19
              endpoint.value *= 2 + 1 = 7
000110010     unit = 32; reverse = 19 + 0 * 32 = 19
              endpoint.value *= 2 + 0 = 14
0001100100    unit = 64; reverse = 19 + 0 * 64 = 19
              endpoint.value *= 2 + 0 = 28
00011001000   unit = 128; reverse = 19 + 0 * 128 = 19
              endpoint.value *= 2 + 0 = 56

此时我们发现我们有一个3个零的序列,它比当前最大值2更长,所以我们扔掉了到目前为止的终点并用当前位置和反向值替换它:

endpoint = {position = 10, value = 19}  

然后我们继续迭代字符串:

000110010001         unit = 256; reverse = 19 + 1 * 256 = 275
                     endpoint.value *= 2 + 1 = 39
0001100100011        unit = 512; reverse = 275 + 1 * 512 = 778
                     endpoint.value *= 2 + 1 = 79
00011001000110       unit = 1024; reverse = 778 + 0 * 1024 = 778
                     endpoint.value *= 2 + 0 = 158
000110010001101      unit = 2048; reverse = 778 + 1 * 2048 = 2826
                     endpoint.value *= 2 + 1 = 317
0001100100011010     unit = 4096; reverse = 2826 + 0 * 4096 = 2826
                     endpoint.value *= 2 + 0 = 634
00011001000110100    unit = 8192; reverse = 2826 + 0 * 8192 = 2826
                     endpoint.value *= 2 + 0 = 1268
000110010001101000   unit = 16384; reverse = 2826 + 0 * 16384 = 2826
                     endpoint.value *= 2 + 0 = 2536

在这里我们发现我们有另一个带有3个零的序列,因此我们将当前的反向值与终点值进行比较,并发现存储的端点值较低:

endpoint.value = 2536  < reverse = 2826  

所以我们将终点设置为位置10,然后我们继续迭代字符串:

0001100100011010001      unit = 32768; reverse = 2826 + 1 * 32768 = 35594
                         endpoint.value *= 2 + 1 = 5073  
00011001000110100010     unit = 65536; reverse = 35594 + 0 * 65536 = 35594
                         endpoint.value *= 2 + 0 = 10146
000110010001101000100    unit = 131072; reverse = 35594 + 0 * 131072 = 35594
                         endpoint.value *= 2 + 0 = 20292
0001100100011010001000   unit = 262144; reverse = 35594 + 0 * 262144 = 35594
                         endpoint.value *= 2 + 0 = 40584

我们找到另外一个3个零的序列,所以我们将这个位置与存储的终点进行比较:

endpoint.value = 40584 > reverse = 35594  

我们发现它的值较小,因此我们用当前位置替换可能的终点:

endpoint = {position = 21, value = 35594}  

然后我们迭代最后的数字:

00011001000110100010001   unit = 524288; reverse = 35594 + 1 * 524288 = 559882  
                          endpoint.value *= 2 + 1 = 71189

所以最后我们发现位置21给出了最低值,因此它是最佳解决方案:

00011001000110100010001  ->  00000010001011000100111
   ^                 ^
start = 3         end = 21

这是一个使用bool向量而不是整数的C ++版本。它可以解析超过64个字符的字符串,但复杂性可能是二次的。

#include <vector>

struct range {unsigned int first; unsigned int last;};

range lexiLeastRev(std::string const &str) {
    unsigned int len = str.length(), first = 0, last = 0, run = 0, max_run = 0;
    std::vector<bool> forward(0), reverse(0);
    bool leading_zeros = true;

    for (unsigned int pos = 0; pos < len; pos++) {
        bool digit = str[pos] - 'a';
        if (!digit) {
            if (leading_zeros) continue;
            if (++run > max_run || run == max_run && reverse < forward) {
                max_run = run;
                last = pos;
                forward = reverse;
            }
        }
        else {
            if (leading_zeros) {
                leading_zeros = false;
                first = pos;
            }
            run = 0;
        }
        forward.push_back(digit);
        reverse.insert(reverse.begin(), digit);
    }
    return range {first, last};
}