LeetCode 854:相似度为 K 的字符串

LeetCode 854:相似度为 K 的字符串

问题背景与定义

在字符串处理问题中,经常会遇到需要通过某种操作将一个字符串转换为另一个字符串的场景。LeetCode 854题「相似度为 K 的字符串」就是这类问题的典型代表。题目要求我们计算将一个字符串 s1 转换为另一个字符串 s2 所需的最小交换次数 ,其中 s1s2 是互为异位词(anagram)的字符串。

示例:

  • 输入:s1 = "ab", s2 = "ba"

    输出:1

    解释:交换 s1 中的 ab,得到 "ba",只需 1 次交换。

  • 输入:s1 = "abc", s2 = "bca"

    输出:2

    解释:先交换 ab 得到 "bac",再交换 ac 得到 "bca",共需 2 次交换。

这类问题本质上是在寻找字符串转换的最短路径,而广度优先搜索(BFS)是解决这类问题的有效方法。

为什么选择 BFS?

广度优先搜索(BFS)是解决最短路径问题的理想选择,因为它具有以下特性:

  1. 逐层遍历:BFS 从初始状态开始,逐层扩展所有可能的状态,确保每一层的状态都对应相同的操作步数。
  2. 最短路径保证:当第一次到达目标状态时,所经过的路径一定是最短的(步数最少)。
  3. 避免重复计算:通过记录已访问的状态,可以避免重复处理相同的状态,提高效率。

在本题中,每个状态是一个字符串,每次操作是一次字符交换。BFS 可以确保我们在找到目标字符串时,使用的交换次数是最少的。

算法思路详解

我们的目标是通过最少的交换次数将 s1 转换为 s2。核心思路是使用 BFS 逐层扩展所有可能的交换,并通过剪枝策略减少不必要的计算:

  1. 定位差异 :每次找到第一个不匹配的位置 i,即 s1[i] ≠ s2[i]
  2. 筛选候选交换位置 :寻找位置 j > i,满足 s1[j] == s2[i]s1[j] ≠ s2[j]
  3. 优先处理最优交换 :如果存在位置 j 使得 s1[i] == s2[j],则交换 ij 可以同时修复两个位置,优先处理这种情况。
  4. 记录已访问状态:使用哈希集合记录已生成的字符串,避免重复处理。

代码实现与详细解释

以下是完整的 Java 代码实现:

java 复制代码
import java.util.*;

class Solution {
    public int kSimilarity(String a1, String a2) {
        // 如果两个字符串已经相同,无需交换
        if (a1.equals(a2)) {
            return 0;
        }
        
        // 初始化 BFS 队列和已访问集合
        Queue<String> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        queue.offer(a1);
        visited.add(a1);
        int step = 0; // 记录当前步数
        int n = a1.length();
        
        // BFS 主循环
        while (!queue.isEmpty()) {
            int size = queue.size();
            // 处理当前层的所有状态
            for (int k = 0; k < size; k++) {
                String s = queue.poll();
                // 如果找到目标字符串,返回当前步数
                if (s.equals(a2)) {
                    return step;
                }
                
                // 找到第一个不匹配的位置 i
                int i = 0;
                while (i < n && s.charAt(i) == a2.charAt(i)) {
                    i++;
                }
                if (i == n) continue; // 如果所有字符都匹配,跳过
                
                char[] arr = s.toCharArray();
                List<Integer> candidates = new ArrayList<>();
                
                // 寻找所有可能的交换位置 j
                for (int j = i + 1; j < n; j++) {
                    // 条件1:arr[j] 必须等于目标位置 i 的字符
                    // 条件2:位置 j 的字符不能已经在目标位置
                    if (arr[j] == a2.charAt(i) && arr[j] != a2.charAt(j)) {
                        // 最优情况:如果交换 i 和 j 可以同时解决两个位置的不匹配
                        if (arr[i] == a2.charAt(j)) {
                            swap(arr, i, j);
                            String t = new String(arr);
                            if (t.equals(a2)) {
                                return step + 1; // 提前返回,找到结果
                            }
                            if (visited.add(t)) {
                                queue.offer(t); // 将新状态加入队列
                            }
                            swap(arr, i, j); // 回溯,恢复数组
                            candidates.clear(); // 找到最优解,清空候选列表
                            break;
                        } else {
                            candidates.add(j); // 普通候选,稍后处理
                        }
                    }
                }
                
                // 处理所有普通候选
                for (int j : candidates) {
                    swap(arr, i, j);
                    String t = new String(arr);
                    if (visited.add(t)) {
                        queue.offer(t); // 将新状态加入队列
                    }
                    swap(arr, i, j); // 回溯,恢复数组
                }
            }
            step++; // 进入下一层,步数加1
        }
        return -1; // 理论上不会执行到这里,因为题目保证有解
    }
    
    // 交换数组中两个位置的字符
    private void swap(char[] arr, int i, int j) {
        char temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

关键步骤解析

1. 初始化与边界检查
java 复制代码
if (a1.equals(a2)) {
    return 0;
}

如果两个字符串已经相同,直接返回 0,无需任何交换。

2. BFS 队列与已访问集合
java 复制代码
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(a1);
visited.add(a1);
int step = 0;
  • 队列 queue:存储待处理的字符串状态。
  • 集合 visited:记录已访问的状态,避免重复处理。
  • 步数 step:记录当前处理的层数,即交换次数。
3. 定位第一个不匹配位置
java 复制代码
int i = 0;
while (i < n && s.charAt(i) == a2.charAt(i)) {
    i++;
}
if (i == n) continue;
  • 从左到右找到第一个 s.charAt(i) ≠ a2.charAt(i) 的位置 i
  • 如果所有字符都匹配(i == n),则跳过当前状态。
4. 筛选候选交换位置
java 复制代码
for (int j = i + 1; j < n; j++) {
    if (arr[j] == a2.charAt(i) && arr[j] != a2.charAt(j)) {
        // ...
    }
}
  • 条件1arr[j] == a2.charAt(i)
    确保交换后位置 i 的字符能被修复。
  • 条件2arr[j] != a2.charAt(j)
    避免交换一个已经在正确位置的字符,减少无效操作。
5. 最优交换优先处理
java 复制代码
if (arr[i] == a2.charAt(j)) {
    swap(arr, i, j);
    String t = new String(arr);
    if (t.equals(a2)) {
        return step + 1;
    }
    if (visited.add(t)) {
        queue.offer(t);
    }
    swap(arr, i, j);
    candidates.clear();
    break;
}
  • arr[i] == a2.charAt(j) 时,交换 ij 可以同时修复两个位置的不匹配。
  • 这种情况下,直接返回 step + 1,或加入队列继续处理。
6. 普通候选处理
java 复制代码
for (int j : candidates) {
    swap(arr, i, j);
    String t = new String(arr);
    if (visited.add(t)) {
        queue.offer(t);
    }
    swap(arr, i, j);
}
  • 对于无法同时修复两个位置的普通候选,逐个处理并加入队列。
  • 使用 visited.add(t) 确保只处理未访问过的状态。

剪枝策略详解

这段代码的核心优化在于候选交换位置的筛选,通过以下策略减少不必要的状态扩展:

  1. 定位第一个不匹配位置

    每次只关注当前未匹配的最左侧位置,避免重复处理已匹配的部分。

  2. 候选位置筛选条件

    java 复制代码
    if (arr[j] == a2.charAt(i) && arr[j] != a2.charAt(j)) {
        // ...
    }
    • 确保交换后位置 i 能被修复。
    • 避免交换一个已经在正确位置的字符。
  3. 最优候选优先处理

    java 复制代码
    if (arr[i] == a2.charAt(j)) {
        // 最优情况:交换后同时修复两个位置
    }

    当找到一个位置 j,使得 arr[i] 恰好等于 a2[j] 时,交换 ij 可以同时解决两个不匹配问题,这种情况优先处理并提前返回。

示例演示:理解算法执行过程

让我们通过一个具体例子来理解算法的执行过程:

输入s1 = "abac", s2 = "baca"

步骤解析

  1. 初始状态

    • s1 = "abac"
    • s2 = "baca"
    • 队列初始化为 ["abac"]
    • 步数 step = 0
  2. 处理第一层

    • 取出 "abac",找到第一个不匹配位置 i = 0s1[0] = 'a's2[0] = 'b'
    • 寻找候选位置 j > 0,满足 arr[j] == 'b'arr[j] != s2[j]
    • 找到 j = 1arr[1] = 'b'),交换 i = 0j = 1
    • 生成新字符串 "bAAC",加入队列
    • 步数 step = 1
  3. 处理第二层

    • 取出 "bAAC",找到第一个不匹配位置 i = 2s1[2] = 'A's2[2] = 'c'
    • 寻找候选位置 j > 2,满足 arr[j] == 'c'arr[j] != s2[j]
    • 找到 j = 3arr[3] = 'c'),交换 i = 2j = 3
    • 生成新字符串 "baca",与目标匹配,返回 step + 1 = 2

复杂度分析

  • 时间复杂度:最坏情况下为 O(n!),但通过剪枝策略,实际复杂度远低于此。对于长度为 n 的字符串,每个位置最多有 n-1 个候选交换位置,BFS 的层数通常较小,因此实际效率较高。

  • 空间复杂度:主要由队列和访问集合决定,最坏情况下为 O(n × n!),其中 n! 是可能的状态数,n 是每个状态的字符串长度。

总结:为什么这种方法有效?

  1. BFS 的逐层扩展:确保找到的路径是最短的,符合题目要求的最小交换次数。

  2. 候选位置的智能筛选

    • 只考虑能修复当前不匹配位置的交换
    • 优先处理能同时修复两个位置的最优交换
    • 避免交换已经在正确位置的字符
  3. 状态去重:使用哈希集合记录已访问的状态,避免重复计算,大幅提高效率。

通过这些优化,算法能够在合理时间内处理较大的输入,找到最优解。这种方法不仅适用于字符串相似度问题,还可推广到其他需要寻找最短操作序列的问题中。

相关推荐
野渡拾光1 小时前
【考研408数据结构-05】 串与KMP算法:模式匹配的艺术
数据结构·考研·算法
xiangxiongfly9151 小时前
Android 圆形和圆角矩形总结
android·圆形·圆角·imageview
tainshuai3 小时前
用 KNN 算法解锁分类的奥秘:从电影类型到鸢尾花开
算法·分类·数据挖掘
幻雨様6 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端8 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Coovally AI模型快速验证9 小时前
农田扫描提速37%!基于检测置信度的无人机“智能抽查”路径规划,Coovally一键加速模型落地
深度学习·算法·yolo·计算机视觉·transformer·无人机
pusue_the_sun9 小时前
数据结构:二叉树oj练习
c语言·数据结构·算法·二叉树
Meteors.9 小时前
Android约束布局(ConstraintLayout)常用属性
android
RaymondZhao349 小时前
【全面推导】策略梯度算法:公式、偏差方差与进化
人工智能·深度学习·算法·机器学习·chatgpt
alexhilton10 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack