LeetCode 411 - 最短独占单词缩写
今天我们来聊一道 “看起来高冷”、但其实思想非常酷的题:最短独占单词缩写。题目要求:给定一个目标字符串 target,还有一个字典 dictionary,你需要为 target 生成一个 最短可能的缩写字符串,并且这个缩写不能和字典里任意一个单词的缩写“冲突”。这里的“冲突”指的是:如果字典中某个单词与 target 长度相同,那么它可能拥有与我们为 target 生成的缩写相同的缩写形式,那就不


摘要
今天我们来聊一道 “看起来高冷”、但其实思想非常酷的题:最短独占单词缩写。
  题目要求:给定一个目标字符串 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(a、32、b、c四个部分算 4)。
- 
  要求:找出一个对 target的缩写,使其 长度最短,并且它 不会和字典中任何一个长度等于target的单词的缩写形式相同。如果有多个最短的结果,返回任意一个就行。
举个例子:
- target = "apple",- dictionary = ["blade"]→ 返回- "a4",因为- "5"或- "4e"会和- "blade"的缩写冲突。
- target = "apple",- dictionary = ["plain","amber","blade"]→ 返回- "1p3"(也有其他合法答案如- "ap3"、- "a3e"、- "2p2"、- "3le"、- "3l1")。
题解答案
好,我们说说思路(先通俗,然后再代码)。
- 缩写候选生成:因为 target长度 m ≤ 21,理论上每个字符有“保留原字符”或“用数目代替省略”的两种状态,所以所有候选缩写数是 2^m 。这虽大,但 m 上限 21 是可控的。我们通过位掩码来表示哪些位置保留、哪些位置省略。
- 字典冲突检测:对于字典中每个与 target等长的单词,我们可以预先生成一个 “mask” 表示它与target在哪些位置 字符相同,哪些位置不同。这样,当我们某个候选缩写保留了某些位置时,如果它保留的位置都恰好与字典某单词不同 -> 那缩写就与该单词“冲突”。我们可以通过位运算快速判断。
- 从短到长寻找合法缩写:我们按候选的缩写长度从小往大枚举(或优先队列),“早找到一个合法的就返回”。
- 高效剪枝:在生成候选的时候,我们可以根据字典中“哪些位置必须被保留”来提前剪枝,降低枚举量。
大致流程:
- 筛出字典中与 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)。
总结
这题是字符串 +位运算 +枚举 +剪枝的混合体,挑战点在于如何 高效地生成缩写 并 检测冲突。虽然枚举看似“暴力”,但结合题目限制后其实可行。
  几个思路类似适用场景:
- 在变量命名或缩写生成系统中,确保生成的缩写不会与已有的冲突。
- 在文档/代码自动简写系统中,生成“最简缩写”并排除已有冲突项。
- 在编码器或压缩系统里,生成最短的编码并保证唯一性。
总体来说,这题训练的是“多维度约束下枚举 +剪枝”的思维方式,非常值得刷一遍。
更多推荐
 
 



所有评论(0)