LeetCode 777. 在LR字符串中交换相邻字符

题目描述

在一个由 'L' , 'R' 和 'X' 三个字符组成的字符串(例如"RXXLRXRXL")中进行移动操作。一次移动操作指用一个"LX"替换一个"XL",或者用一个"XR"替换一个"RX"。现给定起始字符串start和结束字符串end,请编写代码,当且仅当存在一系列移动操作使得start可以转换成end时, 返回True。

示例

输入: start = "RXXLRXRXL", end = "XRLXXRRLX"
输出: True
解释:
我们可以通过以下几步将start转换成end:
RXXLRXRXL ->
XRXLRXRXL ->
XRLXRXRXL ->
XRLXXRRXL ->
XRLXXRRLX

提示

  • 1 <= len(start) = len(end) <= 10000。
  • startend中的字符串仅限于'L', 'R'和'X'。

题解

暴力解

首先,稍微画一下各种情况,可以发现一种需要特别警惕的情景:RRXX可以被转换成RXRXXRRXXRXRXXRR这些状态。
同理,L也存在类似的情况

将这种情景推广,用更为准确的情况描述,就是“X表示空位,L字符可以向左移动到空位,R字符可以向右移动到空位”

那么可以在考虑该移动策略的前提下,枚举所有可能。对于相同位置的字符,有:

起始字符 结束字符 结果
L L 可以转换
L R 无法转换
L X 无法转换
R L 无法转换
R R 可以转换
R X 可能可以转换
X L 可能可以转换
X R 无法转换
X X 可以转换

也即,实际上是需要考虑两种情况即可RXXL

RX为例,如果R存在右移的可能,那么必会留下一个X(空位)。因此只需要考虑是否可以移动即可。如果R想要移动,那么右侧必然存在一个X,且到这个X中间不能存在L。这样只需要将中间所有的R右移一位即可。

直接运行该算法,最坏情况下时间复杂度应该是 O(n2)O(n^2),空间复杂度为 O(1)O(1)

最坏时,可能会存在诸如RRRRRRRRRRRRRRRRRRRRRRRXXXXXX……的形式,每一个R都需要遍历后面所有的R

最优解

再次看上面的结论,如果我现在是RX的情况,我必须要确保找到下一个X之前不能有L。这时候去考虑另一个串的情况,由于在这个过程中,涉及的都是R的右移,因此至少要确保对应数目的R出现前,不能有L,否则无法一一对应。

也即,在不考虑X的情况下,将相邻的RL压缩,两个串将会得到相同的结果。
RXXLRXRXL压缩有应该是R(1)L(1)R(2)L(1)那么其可以转换成的目标串,压缩后必然也是R(1)L(1)R(2)L(1)
可以视为,去除X后,两个串相同。

接下来考虑LR的特点:R只允许右移,L只允许左移。
因此,对于串XRRRXR,尽管压缩后都是R(2)但是却无法转换成功。也即,起始串对应位置的R必须在目标串左侧。L同理。

总结下上述规律,只需要判断在两个串中,非X字符位置是否满足:

  • 字符相同
  • R 字符在起始串下标小于等于结束串
  • L 字符在结束串下表大于等于结束串

接下来要做的就是判定好边界情况

应该如何思考

如果多画一画,应该很容易发现L左移和R右移的规律。
可能再多手画几组数据,可能会有想到上面压缩的思路,但是很容易会被大量测试用例给否掉。

似乎如果将转换关系连线起来,会更易于理解,发现规律

RXXLRXRXLRRXXRLXXRRLXXRR的转换(可以看出,LR转换关系不交叉且拥有方向性)

%3 cluster_1 cluster_2 a_0 R a_1 X b_1 R a_0->b_1 a_2 X b_0 X a_1->b_0 a_3 L b_3 X a_2->b_3 a_4 R b_2 L a_3->b_2 a_5 X b_5 R a_4->b_5 a_6 R b_4 X a_5->b_4 a_7 X b_6 R a_6->b_6 a_8 L b_8 X a_7->b_8 a_9 R b_7 L a_8->b_7 a_10 R b_10 R a_9->b_10 a_11 X b_11 R a_10->b_11 b_9 X a_11->b_9

代码

暴力解

bool canTransform(char * start, char * end){
    char* a = start;
    char* b = end;
    while(*a != '\0') {
        // printf("%c %d\n", *a, *a);
        if (*a == 'R' && *b == 'X') {
            char* temp = a;
            while (*temp != '\0') {
                ++temp;
                if (*temp == 'L') return false;
                if (*temp == 'X') {
                    *temp = 'R';
                    break;
                }
            }
            if (*temp == '\0') return false;
            *a = 'X';
        } else if (*a == 'X' && *b == 'L') {
            char* temp = a;
            while (*temp != '\0') {
                ++temp;
                if (*temp == 'R') return false;
                if (*temp == 'L') {
                    *temp = 'X';
                    break;
                }
            }
            if (*temp == '\0') return false;
            *a = 'L';
        } else if (*a != *b) return false;
        ++a; ++b;
    }
    return true;
}

最优解

bool canTransform(char * start, char * end){
    int i = 0;
    int j = 0;
    while(start[i] != '\0' && end[j] != '\0') {
        // 找到第一个非 X 字符
        while (start[i] == 'X') ++i;
        while (end[j] == 'X') ++j;

        bool iend = start[i] == '\0';
        bool jend = end[j] == '\0';
        // 两者直接到末尾,说明全是 X 可以转换
        if (iend && jend) return true;
        // 存在一个到末尾,另一个未到末尾,说明个数不同,不能转换
        if (iend ^ jend) return false;

        // 如果对应的字符不同,说明无法转换
        // 如果起始串 R 位置大于目标串,说明无法转换
        // 如果起始串 L 位置小于目标串,说明无法转换
        if (
            start[i] != end[j] || 
            (start[i] == 'R' && i > j) || 
            (start[i] == 'L' && i < j)
        ) return false;
        
        i++; j++;
    }

    // 处理末尾部分
    while (start[i] != '\0') if (start[i++] != 'X') return false;
    while (end[j] != '\0') if (end[j++] != 'X') return false;

    return true;
}