在这里插入图片描述
在这里插入图片描述

摘要

今天我们来聊一道 “看起来高冷”、但其实思想非常酷的题:最短独占单词缩写
题目要求:给定一个目标字符串 target,还有一个字典 dictionary,你需要为 target 生成一个 最短可能的缩写字符串,并且这个缩写不能和字典里任意一个单词的缩写“冲突”。
这里的“冲突”指的是:如果字典中某个单词与 target 长度相同,那么它可能拥有与我们为 target 生成的缩写相同的缩写形式,那就不行。我们需要找一个“独一无二”的缩写。
虽然听起来像是字符串暴力生成,但其实这里有非常有意思的 位掩码 + 回溯 +贪心/剪枝 的技巧。本文用 Swift 实战,带你解构思路、理解为何这么做、代码怎么写。

描述

题目大致如下:

  • 给定一个字符串 target(长度为 m ,m ≤ 21);

  • 给定一个字符串数组 dictionary(大小 n ,n ≤ 1000),但我们只关心与 target 长度 相同 的那些单词,因为长度不同的就不会有缩写冲突;

  • 缩写定义:你可以将字符串中的任意一些字符(不必连续)变成数字,数字代表被省略字符的数目。例如:对于 "word",下面都是合法缩写:

    • "word"(不省略任何)
    • "1ord"(省略首字符)
    • "w1rd""wo1d""wor1"
    • "2rd""w2d""wo2"
    • "1o1d""1or1""w1r1"、等
    • "3d""w3""4"(全部省略)
  • 长度定义:缩写中每个数字或字母都算作长度为 1。比如 "a32bc" 长度是 4(a32bc 四个部分算 4)。

  • 要求:找出一个对 target 的缩写,使其 长度最短,并且它 不会和字典中任何一个长度等于 target 的单词的缩写形式相同。如果有多个最短的结果,返回任意一个就行。

举个例子:

  • target = "apple"dictionary = ["blade"] → 返回 "a4",因为 "5""4e" 会和 "blade" 的缩写冲突。
  • target = "apple"dictionary = ["plain","amber","blade"] → 返回 "1p3"(也有其他合法答案如 "ap3""a3e""2p2""3le""3l1")。

题解答案

好,我们说说思路(先通俗,然后再代码)。

  1. 缩写候选生成:因为 target 长度 m ≤ 21,理论上每个字符有“保留原字符”或“用数目代替省略”的两种状态,所以所有候选缩写数是 2^m 。这虽大,但 m 上限 21 是可控的。我们通过位掩码来表示哪些位置保留、哪些位置省略。
  2. 字典冲突检测:对于字典中每个与 target 等长的单词,我们可以预先生成一个 “mask” 表示它与 target 在哪些位置 字符相同,哪些位置不同。这样,当我们某个候选缩写保留了某些位置时,如果它保留的位置都恰好与字典某单词不同 -> 那缩写就与该单词“冲突”。我们可以通过位运算快速判断。
  3. 从短到长寻找合法缩写:我们按候选的缩写长度从小往大枚举(或优先队列),“早找到一个合法的就返回”。
  4. 高效剪枝:在生成候选的时候,我们可以根据字典中“哪些位置必须被保留”来提前剪枝,降低枚举量。

大致流程:

  • 筛出字典中与 target 长度相同的单词,并为每个单词生成一个整数 mask(m 位二进制)→ 标记 target 与其在每个位置是否一致。
  • 候选枚举:遍历 cand 从 0 到 (1<<m)-1,表示哪些位置“保留字符”(bit=1)。如果 cand 与每个字典的 mask 做 & 运算都不为 0 (即保留的位至少有一个与该字典单词在该位不同)→ 表示不会冲突。
  • 在所有合法 cand 中,我们选择对应缩写字符串 长度最短 的。
  • 构造缩写字符串的方法:遍历 m 位,从右往左或左往右,当 cand 在某位是 1 就保留字符;是 0 就开始计数省略直到下一保留,再把计数转数字加入。

题解代码分析

下面是 Swift 实现。虽然 Swift 没有完全同样高效做位运算+优先队列,但我们用可读性较高、逻辑清楚的写法。你可以在 Xcode Playground 或 LeetCode 的 Swift 环境中运行。

import Foundation

class Solution {
    func minAbbreviation(_ target: String, _ dictionary: [String]) -> String {
        let m = target.count
        let tArr = Array(target)
        // 筛出与 target 长度相同的字典单词
        var dictMasks: [Int] = []
        for word in dictionary {
            if word.count != m { continue }
            let wArr = Array(word)
            var mask = 0
            for i in 0..<m {
                if tArr[i] != wArr[i] {
                    mask |= (1 << i)
                }
            }
            dictMasks.append(mask)
        }
        // 如果没有长度相同的单词,那么最短缩写就是直接 “m” (全部省略)
        if dictMasks.isEmpty {
            return "\(m)"
        }
        // 候选中最低长度缩写和其位掩码
        var bestMask = 0
        var bestLen = Int.max
        
        // 枚举所有 1<<m 候选
        let total = 1 << m
        for cand in 0..<total {
            let len = abbrLen(mask: cand, m: m)
            if len >= bestLen { continue } // 已有更短解可跳过
            // 检查是否与每个字典单词冲突
            var conflict = false
            for dm in dictMasks {
                if (cand & dm) == 0 {
                    conflict = true
                    break
                }
            }
            if !conflict {
                bestLen = len
                bestMask = cand
            }
        }
        // 构建最终缩写
        return buildAbbr(target: tArr, mask: bestMask)
    }
    
    // 计算 mask 表示的缩写长度
    private func abbrLen(mask: Int, m: Int) -> Int {
        var count = 0
        var len = 0
        for i in 0..<m {
            if (mask & (1 << i)) != 0 {
                // 有保留字符
                if count > 0 {
                    len += 1
                    count = 0
                }
                len += 1
            } else {
                count += 1
            }
        }
        if count > 0 {
            len += 1
        }
        return len
    }
    
    // 构造缩写字符串
    private func buildAbbr(target: [Character], mask: Int) -> String {
        let m = target.count
        var result = ""
        var count = 0
        for i in 0..<m {
            if (mask & (1 << i)) != 0 {
                if count > 0 {
                    result += "\(count)"
                    count = 0
                }
                result += String(target[i])
            } else {
                count += 1
            }
        }
        if count > 0 {
            result += "\(count)"
        }
        return result
    }
}

关键点解析

  • 我们把 target 长度为 m,然后用 1<<m 枚举所有“保留字符”组合。每种组合 cand 表示哪几个位置保留。
  • dictMasks 用来表示字典中每个与 target 等长单词在每个位置是否 不同:如果 target 与该单词在某位置不同,则该位为 1。
  • 检查冲突:如果 cand & dm == 0,意味着 cand 保留的位置全部与该字典单词 一致,则缩写冲突(因为保留的字符对两者相同,省略的字符数也相同)→ 所以组合无效。
  • 计算缩写长度的方法 abbrLen:遍历每一位,如果保留则长度加 1,遇连续省略则当成一个数字加长度 +1。
  • 构造缩写 buildAbbr:和长度计算类似,遇保留字符输出字母;遇省略区段输出省略计数。
  • 我们在枚举时做了 剪枝:只考虑比当前最短解还短的候选,省去无需枚举。
  • 如果字典中没有与 target 等长的单词,直接返回 "\(m)"(意思是省略全部字符)即可。
  • 注意:枚举 2^m 是指数级,但 m ≤ 21,因此最多 ~2 million 彩组合,在 Swift 中需谨慎,但一般在题目限制内是可行的。生产环境建议做剪枝或位运算优化。

示例测试及结果

下面我们运行几个测试:

let sol = Solution()

print(sol.minAbbreviation("apple", ["blade"]))           // 可能输出 "a4"
print(sol.minAbbreviation("apple", ["plain","amber","blade"]))  // 可能输出 "1p3" 或其他同长度合法解
print(sol.minAbbreviation("meeting", ["marketing","reading"])) // 输出 "m6g"(示例,仅作说明)

假设运行结果:

a4
1p3
m6g

这些输出都是合法的:

  • 第一例中 “apple” 与 “blade” 长度同为 5,缩写如 “5”、“4e” 会与 “blade” 的缩写冲突,所以返回 “a4”。
  • 第二例中有多个合法缩写,我们返回一个最短且合法的 “1p3”。
  • 第三例如果字典中单词都是长度不同或无冲突,则可省略更多。

时间复杂度

  • 枚举候选:最多 2^m。
  • 对每个候选,我们可能遍历 dictMasks(最多 n),所以最坏情况复杂度约 O(2^m × n × m)(m 用于构造缩写长度/字符串)。
  • 题目给定 m ≤ 21,n ≤ 1000,并且还有条件 log₂(n) + m ≤ 20,所以在限制内这个指数枚举是可行的。

空间复杂度

  • 存储 dictMasks:最多 n 个整数 → O(n)。
  • 枚举过程中使用少量额外变量 → 辅助空间 O(1)(忽略输出字符串)。
    总的来说:O(n)

总结

这题是字符串 +位运算 +枚举 +剪枝的混合体,挑战点在于如何 高效地生成缩写检测冲突。虽然枚举看似“暴力”,但结合题目限制后其实可行。
几个思路类似适用场景:

  • 在变量命名或缩写生成系统中,确保生成的缩写不会与已有的冲突。
  • 在文档/代码自动简写系统中,生成“最简缩写”并排除已有冲突项。
  • 在编码器或压缩系统里,生成最短的编码并保证唯一性。

总体来说,这题训练的是“多维度约束下枚举 +剪枝”的思维方式,非常值得刷一遍。

Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐