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. 状态去重:使用哈希集合记录已访问的状态,避免重复计算,大幅提高效率。

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

相关推荐
小白要加油努力19 小时前
滑动窗口的典例以及思路阐述
算法
啊森要自信19 小时前
【MySQL 数据库】MySQL用户管理
android·c语言·开发语言·数据库·mysql
黄毛火烧雪下19 小时前
(二)Flutter插件之Android插件开发
android·flutter
CoovallyAIHub20 小时前
一夜之间,大模型处理长文本的难题被DeepSeek新模型彻底颠覆!
深度学习·算法·计算机视觉
天选之女wow20 小时前
【代码随想录算法训练营——Day43(Day42周日休息)】动态规划——300.最长递增子序列、674.最长连续递增序列、718.最长重复子数组
算法·leetcode·动态规划
2501_9160074720 小时前
iOS 上架技术支持全流程解析,从签名配置到使用 开心上架 的实战经验分享
android·macos·ios·小程序·uni-app·cocoa·iphone
敲代码的嘎仔20 小时前
JavaWeb零基础学习Day4——Maven
java·开发语言·学习·算法·maven·javaweb·学习方法
聪明的笨猪猪20 小时前
hot 100 (1)—— 两数之和(哈希)
java·经验分享·算法·哈希算法
_dindong21 小时前
牛客101:链表
数据结构·c++·笔记·学习·算法·链表
wuk99821 小时前
基于位置式PID算法调节PWM占空比实现电机转速控制
单片机·嵌入式硬件·算法