目录

题目描述

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

题意分析

给定两个等长的字符串 startend,它们只由 LRX 三种字符组成。允许的操作只有两种:把子串 XL 替换成 LX,或者把子串 RX 替换成 XR。问能否经过任意多次操作把 start 变成 end

这两条替换规则要翻译成物理含义才好用。XL → LX 意味着一个 L 可以与它左边紧邻的 X 交换,也就是 L 只能向左移动RX → XR 意味着一个 R 可以与它右边紧邻的 X 交换,也就是 R 只能向右移动X 本身没有独立的移动能力,它只是被 LR 挤开的空位。把规则从「字符串替换」重述成「棋子在轨道上单向滑动」,整道题的结构立刻清晰。

由此推出第一条不变量:LR 永远无法互相穿越L 只能与 X 换位、R 也只能与 X 换位,任何一次操作都不会让一个 L 跨过一个 R,也不会让两个同类互换先后。所以去掉所有 X 之后,剩下的字符序列在整个变换过程中恒定不变。这是判定的第一道关口:两串去 X 后的序列必须完全相同,否则直接无解。

第二条不变量来自移动方向的单向性:每个 Lend 中的位置必须不大于它在 start 中的位置(只能左移),每个 Rend 中的位置必须不小于它在 start 中的位置(只能右移)。这里的「每个」是按去 X 序列一一对应的第 $k$ 个字符来说的。

反过来,这两条必要条件也是充分的——只要相对顺序一致且每个棋子的移动方向合法,就一定能构造出一串操作把 start 变成 end(因为棋子之间不会互相阻挡:需要左移的 L 从左往右依次处理,需要右移的 R 从右往左依次处理,中间的 X 总是够用)。所以判定可以只检查这两条。

约束是 $1 \le n \le 10^4$,两串等长。规模不大,但也足以排除「枚举所有操作序列做搜索」的方向——状态空间是指数级的。$O(n)$ 的一次扫描是自然目标。

边界方面:两串完全相同时显然可行;一串全是 X 而另一串含有非 X 字符时不可行(去 X 序列长度不同);某一侧的非 X 字符先耗尽而另一侧还有剩余,同样不可行。这三种情况都要被同一套指针逻辑覆盖。

解法:双指针匹配非 X 字符

核心思路

暴力思路是把字符串当作状态做广度优先搜索,每一步枚举所有可执行的替换。$n = 10^4$ 时状态数是天文数字,完全不可行。瓶颈很明确:我们在模拟过程,而实际上只需要验证两个不变量——过程本身是可构造的,不必真的走一遍。

于是转向判定。要同时验证「去 X 序列相同」和「每个字符的位移方向合法」,最省事的办法是用两个指针分别在两串上跳过 X,逐个配对非 X 字符。这样一次扫描就能同时拿到两份信息:配对上的字符是否相同(顺序一致性),以及它们各自的下标(位移方向)。

显式写下不变量:任意时刻,指针 $i$ 与 $j$ 分别指向 startend 中第 $k$ 个非 X 字符($k$ 为已配对的个数),且前 $k-1$ 对字符已经通过了「字符相同」与「移动方向合法」两项检查。

每一轮做三件事:

  • 对齐:让 $i$ 向右跳过 start 中连续的 X,让 $j$ 向右跳过 end 中连续的 X。跳完之后,两个指针要么指向各自的第 $k$ 个非 X 字符,要么已经越过末尾。
  • 终止判定:如果两个指针都到了末尾,说明所有非 X 字符都成功配对,返回真;如果只有一个到了末尾,说明两串的非 X 字符个数不同,去 X 序列必然不同,返回假。这两条判断的顺序不能交换——先判「都到末尾」才能正确接受合法输入。
  • 配对检查:先比较 $start[i]$ 与 $end[j]$ 是否相同,不同则去 X 序列不同,返回假。相同则按字符类型检查位移方向:若是 L,要求 $i \ge j$(它在 end 中的位置不能比在 start 中更靠右),所以 $i < j$ 时返回假;若是 R,要求 $i \le j$,所以 $i > j$ 时返回假。

三件事都通过后,两个指针各前进一步,进入下一轮。

这里最容易想歪的一点是方向判断的不等号。记住物理含义就不会错:L 向左走,所以它的终点下标只能更小或相等,即 $j \le i$;R 向右走,终点下标只能更大或相等,即 $j \ge i$。取等是允许的——一个字符完全可以原地不动。

外层循环的条件写成「$i < n$ 或 $j < n$」而不是「且」,是为了让「一侧先耗尽」这种情况也能进入循环体,被里面的终止判定捕获并返回假。如果写成「且」,一侧耗尽时循环直接退出并返回真,漏掉了非 X 字符个数不等的情形。

解题步骤

  • 第一步,令 $i = j = 0$,进入循环,条件是 $i < n$ 或 $j < n$。 为什么用「或」:需要让「$i$ 已到末尾但 $j$ 还没」这种不合法情形也进入循环体接受检查。用「且」会让这类输入直接跳出并返回真,漏判。
  • 第二步,让 $i$ 跳过 start 中的连续 X,让 $j$ 跳过 end 中的连续 X 为什么可以无条件跳过:X 只是空位,它的具体位置由 LR 的落点唯一决定,不携带独立信息。跳过 X 之后剩下的比较,正是「去 X 序列」的逐位比较。
  • 第三步,若 $i$ 与 $j$ 都等于 $n$,返回真。 为什么这是成功的唯一出口:两串的非 X 字符同时耗尽,说明个数相同、逐位相同、方向全部合法,三项条件齐备。
  • 第四步,若 $i$ 与 $j$ 中只有一个等于 $n$,返回假。 为什么必须放在第三步之后:先判「都到末尾」再判「只有一个到末尾」,顺序反了会把合法输入误判为失败。
  • 第五步,比较 $start[i]$ 与 $end[j]$,不同则返回假。 为什么这一步就等价于「去 X 序列相同」:两个指针始终指向各自的第 $k$ 个非 X 字符,逐位比较即是序列比较,无需真的构造出两个新字符串。
  • 第六步,若字符是 L 且 $i < j$,返回假。 为什么:L 只能左移,它在 end 中的下标 $j$ 不能大于在 start 中的下标 $i$。$i < j$ 意味着这个 L 需要向右移动,而没有任何操作能做到。
  • 第七步,若字符是 R 且 $i > j$,返回假。 为什么:对称地,R 只能右移,$i > j$ 意味着它需要向左移动,同样不可能。
  • 第八步,两个指针各加一,进入下一轮。 为什么必须同时加一:它们是配对推进的,任何一方单独前进都会打乱「第 $k$ 个对第 $k$ 个」的对应关系。
  • 第九步,循环因两侧都耗尽而自然退出时返回真。 实践中这条路径通常不会被走到(第三步已经返回),但保留它能让函数在逻辑上完整。

start = "RXXLRXRXL"end = "XRLXXRRLX" 走一遍($n = 9$)。

两串的下标与字符对照:start0:R 1:X 2:X 3:L 4:R 5:X 6:R 7:X 8:Lend0:X 1:R 2:L 3:X 4:X 5:R 6:R 7:L 8:X。先手算预期:startXRLRRLendX 也得 RLRRL,顺序一致,接下来只需逐个检查方向。

第 1 轮:$i = 0$ 处是 R,不需跳过;$j = 0$ 处是 X,跳到 $j = 1$(字符 R)。两者都未越界。字符都是 R,相同。检查方向:R 要求 $i \le j$,这里 $0 \le 1$ 成立——这个 R 从下标 0 右移到下标 1,合法。指针推进到 $i = 1$、$j = 2$。

第 2 轮:$i = 1$ 与 $i = 2$ 都是 X,跳到 $i = 3$(字符 L);$j = 2$ 处是 L,无需跳。字符都是 L,相同。检查方向:L 要求 $i \ge j$,这里 $3 \ge 2$ 成立——这个 L 从下标 3 左移到下标 2,合法。推进到 $i = 4$、$j = 3$。

第 3 轮:$i = 4$ 处是 R,无需跳;$j = 3$、$j = 4$ 都是 X,跳到 $j = 5$(字符 R)。字符都是 R。方向检查 $4 \le 5$ 成立,右移一格,合法。推进到 $i = 5$、$j = 6$。

第 4 轮:$i = 5$ 是 X,跳到 $i = 6$(字符 R);$j = 6$ 处是 R,无需跳。字符相同。方向检查 $6 \le 6$ 成立——取等,说明这个 R 原地不动,这正是为什么不等号必须允许相等。推进到 $i = 7$、$j = 7$。

第 5 轮:$i = 7$ 是 X,跳到 $i = 8$(字符 L);$j = 7$ 处是 L。字符相同。方向检查 $8 \ge 7$ 成立,左移一格,合法。推进到 $i = 9$、$j = 8$。

第 6 轮:循环条件「$i < 9$ 或 $j < 9$」中,$j = 8 < 9$ 成立,进入循环体。$i$ 已是 9,跳 X 的内层循环不执行;$j = 8$ 处是 X,跳到 $j = 9$。此时 $i = 9$ 且 $j = 9$,返回真。

这最后一轮很关键:end 末尾还剩一个 X 没被消费,若外层循环条件写成「且」,第 5 轮结束后 $i = 9$ 就直接退出了——本例仍返回真所以不暴露,但换成 start = "X"end = "L" 就会出问题:$i$ 跳过 X 后变成 1(越界),$j = 0$ 指向 L;用「且」时循环根本不会进入,返回真,而正确答案是假。用「或」则进入循环体,被「只有一个到末尾」的判断拦下,正确返回假。

再补两个失败用例。start = "LX"end = "XL":第 1 轮 $i = 0$ 指向 L,$j$ 跳过 X 到 1 指向 L,字符相同但 $i = 0 < j = 1$,这个 L 需要右移,返回假。start = "XXRXXLXXXX"end = "XXXXRXXLXX":第 1 轮 R 从下标 2 到下标 4,$2 \le 4$ 合法;第 2 轮 L 从下标 5 到下标 7,$5 < 7$ 意味着要右移,返回假。

代码实现

class Solution {
    public boolean canTransform(String start, String end) {
        int n = start.length();
        int i = 0;
        int j = 0;

        while (i < n || j < n) {
            while (i < n && start.charAt(i) == 'X') {
                i++;
            }
            while (j < n && end.charAt(j) == 'X') {
                j++;
            }

            if (i == n && j == n) {
                return true;
            }
            if (i == n || j == n) {
                return false;
            }

            char a = start.charAt(i);
            char b = end.charAt(j);
            if (a != b) {
                return false;
            }
            if (a == 'L' && i < j) {
                return false;
            }
            if (a == 'R' && i > j) {
                return false;
            }

            i++;
            j++;
        }

        return true;
    }
}
func canTransform(start string, end string) bool {
    n := len(start)
    i, j := 0, 0

    for i < n || j < n {
        for i < n && start[i] == 'X' {
            i++
        }
        for j < n && end[j] == 'X' {
            j++
        }

        if i == n && j == n {
            return true
        }
        if i == n || j == n {
            return false
        }

        if start[i] != end[j] {
            return false
        }
        if start[i] == 'L' && i < j {
            return false
        }
        if start[i] == 'R' && i > j {
            return false
        }
        i++
        j++
    }

    return true
}

复杂度分析

  • 时间复杂度:$O(n)$。两个指针各自单调向右移动,永不回退,所以 $i$ 与 $j$ 加起来最多走 $2n$ 步。内层跳 X 的循环虽然嵌套在外层里,但它推进的正是同一个指针,总步数受同一个上界约束——这是典型的双指针均摊分析,不能被嵌套的表象误导成 $O(n^2)$。
  • 空间复杂度:$O(1)$。只用了两个下标变量和两个临时字符。特别注意我们没有真的构造出两个「去 X 后的字符串」——那样是 $O(n)$ 空间;靠指针跳过 X 就地比较,把空间压到了常数。

关键点总结

  • 把替换规则翻译成物理运动,不变量自然浮现XL → LX 读作「L 只能左移」、RX → XR 读作「R 只能右移」之后,「相对顺序不变」和「位移方向单向」两条不变量几乎是显然的。凡是给出一组局部替换规则的题,第一步都应该问:这些规则保持了什么量不变?
  • 可达性判定往往等价于「若干个不变量同时成立」。不必模拟操作序列,只需找齐必要条件并论证它们也是充分的。本题的两条条件合起来既必要又充分,所以一次线性扫描就能定论。
  • 「跳过无关字符后逐位比较」用双指针实现,不要真的构造新串。这个手法在比较含退格的字符串、判断子序列、长按键入等题里反复出现,共同点是:某类字符不携带独立信息,可以在扫描时就地忽略。
  • 循环条件用「或」还是「且」,取决于你想让哪些非法情形进入检查。这里必须用「或」,才能让「一侧先耗尽」被循环体内的判断捕获。写双指针时要专门想一想:两个指针不同步结束时,我的循环还能不能发现问题?
  • 方向判断的不等号必须允许取等。字符原地不动是合法的,L 用 $i \ge j$、R 用 $i \le j$。把等号漏掉会让所有「不需要移动的字符」被判为非法,几乎所有正例都会挂掉。
  • 面试视角:这题的全部分数都在「你能不能说出两条不变量」。开口先讲「L 只能左移、R 只能右移,所以去掉 X 后的序列恒定」,再讲「每个字符的位移方向受限」,最后说明这两条既必要又充分——讲完之后代码只是十几行的直接翻译。面试官常见的追问是「为什么充分」,要能答出「棋子之间不会互相阻挡,按合适顺序逐个移动即可构造出操作序列」。另一个追问是「不用双指针行不行」,可以答:把两串各自压缩成「非 X 字符 + 其下标」的列表再逐项比较,逻辑等价但多花 $O(n)$ 空间,双指针是它的原地版本。

易错点总结

  • 错误写法:外层循环条件写成 i < n && j < n。以 start = "X"end = "L" 为例,$i$ 跳过 X 后越界,循环条件不成立直接退出并返回真,而正确答案是假(X 无法变出一个 L)。必须用「或」,让一侧耗尽的情形进入循环体接受检查。
  • 错误写法:L 的方向判断写成 i > j 返回假。以 start = "XL"end = "LX" 为例,L 从下标 1 左移到下标 0,$i = 1 > j = 0$ 会被误判为非法,返回假,而正确答案是真。方向搞反会让所有本该成功的左移全部失败。
  • 错误写法:R 的方向判断写成 i < j 返回假。以 start = "RX"end = "XR" 为例,R 从下标 0 右移到下标 1,$i = 0 < j = 1$ 被误判为非法,返回假,而正确答案是真。
  • 错误写法:方向判断漏掉取等,写成 L 要求 $i > j$、R 要求 $i < j$。以 start = "L"end = "L" 为例,字符原地不动,$i = j = 0$ 两个条件都不满足,返回假,而正确答案是真。任何不需要移动的字符都会被这个 bug 判死。
  • 错误写法:先判「只有一个到末尾」再判「都到末尾」。以 start = "L"end = "L" 为例,第 2 轮时 $i = j = 1 = n$,若先执行 if (i == n || j == n) return false,会直接返回假,而正确答案是真。两个判断的顺序不可交换。
  • 错误写法:只比较去 X 后的序列是否相同,不检查位移方向。以 start = "LX"end = "XL" 为例,两串去 X 后都是 L,序列一致,会返回真;但这个 L 需要向右移动,实际不可能,正确答案是假。顺序一致只是必要条件之一。
  • 错误写法:只检查位移方向,不比较字符是否相同。以 start = "RL"end = "LR" 为例,第一对下标都是 0,按 start 的字符 R 检查 $0 \le 0$ 通过;第二对下标都是 1,按 L 检查 $1 \ge 1$ 通过,最终返回真,而两串去 X 后分别是 RLLR,顺序不同,正确答案是假。两项检查缺一不可。
  • 错误写法:跳过 X 的内层循环忘记边界判断,写成 while (start.charAt(i) == 'X') i++。以 start = "XX"end = "XX" 为例,$i$ 一路加到 2 之后继续访问 charAt(2),Java 抛字符串越界异常,Go 直接 panic。跳过循环必须同时判 $i < n$。
  • 错误写法:只推进一个指针,比如比较通过后只写 i++。以 start = "RL"end = "RL" 为例,第一对匹配后 $i = 1$、$j = 0$,第二轮拿 start[1] = 'L'end[0] = 'R' 比较,字符不同返回假,而正确答案是真。两个指针必须成对推进。
  • 错误写法:先构造出两个去 X 的字符串再比较,但丢掉了原下标。以 start = "LX"end = "XL" 为例,压缩后两串都是 L,比较结果相同而返回真,正确答案是假。位移方向的判断依赖原始下标,压缩时必须把下标一起记下来,否则信息丢失。
  • 错误写法:以为 X 之间也需要一一对应。以 start = "RXXLRXRXL"end = "XRLXXRRLX" 为例,两串中 X 的分布完全不同,但答案是真。X 是被动的空位,它的位置由 LR 的落点决定,绝不能参与配对。
  • 错误写法:用广度优先搜索模拟所有可能的替换。以 $n = 10^4$ 的输入为例,状态空间随长度指数增长,必然超时甚至内存耗尽。可达性判定题应优先寻找不变量,而不是模拟过程。

相似题目

题目 难度 考察点
844. 比较含退格的字符串 简单 同样是「跳过无效字符后逐位比较」,但退格要从右往左扫且需计数抵消
925. 长按键入 简单 双指针配对时允许一侧出现重复字符,推进规则不对称
392. 判断子序列 简单 只要求顺序包含而不要求完全一致,一个指针可以停留不动
283. 移动零 简单 把「空位」挤到末尾的原地双指针,是本题 X 作为空位这一视角的最简形态
1247. 交换字符使得字符串相同 中等 交换的是任意两处而非相邻,问题从可达性判定变成最少交换次数的贪心配对
1202. 交换字符串中的元素 中等 可交换关系构成连通分量,分量内可任意重排,考察的是「哪些不变量被打破了」
1657. 确定两个字符串是否接近 中等 同为「给定操作下的可达性判定」,不变量是字符集合与频次的多重集
125. 验证回文串 简单 双指针跳过非字母数字字符后比较,练习「忽略无关字符」的指针推进模板