Medium

题目描述

给你一个由 n 个字符串组成的数组 words。每个字符串的长度为 m,只包含小写英文字母。

如果可以对两个字符串 s 和 t 执行以下操作任意次数(可能为 0 次),使得 s 和 t 变得相等,则称这两个字符串相似:

  • 选择 s 或 t 中的一个
  • 将选择的字符串中的每个字母替换为字母表中的下一个字母(循环)。字母 ‘z’ 的下一个字母是 ‘a’。

计算满足以下条件的索引对 (i, j) 的数量:

  • i < j
  • words[i] 和 words[j] 相似

返回表示此类对数的整数。

示例 1:

输入:words = [“fusion”,“layout”]

输出:1

解释:

words[0] = “fusion” 和 words[1] = “layout” 相似,因为我们可以对 “fusion” 执行 6 次操作。字符串 “fusion” 的变化如下:

  • “fusion”
  • “gvtjpo”
  • “hwukqp”
  • “ixvlrq”
  • “jywmsr”
  • “kzxnts”
  • “layout”

示例 2:

输入:words = [“ab”,“aa”,“za”,“aa”]

输出:2

解释:

words[0] = “ab” 和 words[2] = “za” 相似。words[1] = “aa” 和 words[3] = “aa” 相似。

约束条件:

  • 1 <= n == words.length <= 10^5
  • 1 <= m == words[i].length <= 10^5
  • 1 <= n * m <= 10^5
  • words[i] 只包含小写英文字母

提示:

  • 如果两个字符串的相邻字符之间的差值(mod 26)相同,则它们相似;通过将每个字符串的第一个字符移位到 ‘a’ 来标准化每个字符串
  • 为每个标准化字符串计算一个可哈希的键,表示其相对字符差异
  • 使用映射来计算有多少字符串共享相同的标准化键

解题思路

解题思路

这道题的核心在于理解什么是"相似"的字符串。两个字符串相似,意味着它们可以通过凯撒密码的方式互相转换。

关键观察

  1. 凯撒密码的本质:如果字符串 A 可以通过 k 次移位变成字符串 B,那么 A 和 B 中对应位置字符的差值都是 k(mod 26)。

  2. 标准化策略:我们可以将每个字符串标准化,使其第一个字符变为 ‘a’,这样相似的字符串会有相同的标准化形式。

  3. 差值序列:对于标准化后的字符串,我们可以计算相邻字符之间的差值序列。相似的字符串会有相同的差值序列。

算法步骤

  1. 对每个字符串进行标准化:将第一个字符移位到 ‘a’,其他字符按相同偏移量移位
  2. 计算标准化后字符串的特征(可以是差值序列或标准化字符串本身)
  3. 使用哈希表统计具有相同特征的字符串数量
  4. 对于每组有 count 个相似字符串的组,贡献的配对数为 count * (count - 1) / 2

这种方法的时间复杂度为 O(n * m),空间复杂度为 O(n * m),其中 n 是字符串数量,m 是字符串长度。

代码实现

class Solution {
public:
    long long countPairs(vector<string>& words) {
        unordered_map<string, int> count;
        
        for (const string& word : words) {
            string normalized = normalize(word);
            count[normalized]++;
        }
        
        long long result = 0;
        for (const auto& pair : count) {
            long long c = pair.second;
            result += c * (c - 1) / 2;
        }
        
        return result;
    }
    
private:
    string normalize(const string& word) {
        if (word.empty()) return word;
        
        int shift = word[0] - 'a';
        string normalized;
        
        for (char c : word) {
            int newChar = c - 'a' - shift;
            if (newChar < 0) newChar += 26;
            normalized += (char)('a' + newChar);
        }
        
        return normalized;
    }
};
class Solution:
    def countPairs(self, words: List[str]) -> int:
        from collections import defaultdict
        
        count = defaultdict(int)
        
        for word in words:
            normalized = self.normalize(word)
            count[normalized] += 1
        
        result = 0
        for c in count.values():
            result += c * (c - 1) // 2
        
        return result
    
    def normalize(self, word: str) -> str:
        if not word:
            return word
        
        shift = ord(word[0]) - ord('a')
        normalized = []
        
        for c in word:
            new_char = (ord(c) - ord('a') - shift) % 26
            normalized.append(chr(ord('a') + new_char))
        
        return ''.join(normalized)
public class Solution {
    public long CountPairs(string[] words) {
        Dictionary<string, int> count = new Dictionary<string, int>();
        
        foreach (string word in words) {
            string normalized = Normalize(word);
            if (count.ContainsKey(normalized)) {
                count[normalized]++;
            } else {
                count[normalized] = 1;
            }
        }
        
        long result = 0;
        foreach (int c in count.Values) {
            result += (long)c * (c - 1) / 2;
        }
        
        return result;
    }
    
    private string Normalize(string word) {
        if (string.IsNullOrEmpty(word)) return word;
        
        int shift = word[0] - 'a';
        char[] normalized = new char[word.Length];
        
        for (int i = 0; i < word.Length; i++) {
            int newChar = (word[i] - 'a' - shift + 26) % 26;
            normalized[i] = (char)('a' + newChar);
        }
        
        return new string(normalized);
    }
}
var countPairs = function(words) {
    function normalize(word) {
        if (word.length === 0) return word;
        const firstChar = word[0];
        const shift = firstChar.charCodeAt(0) - 'a'.charCodeAt(0);
        return word.split('').map(char => 
            String.fromCharCode((char.charCodeAt(0) - 'a'.charCodeAt(0) - shift + 26) % 26 + 'a'.charCodeAt(0))
        ).join('');
    }
    
    const normalizedMap = new Map();
    
    for (const word of words) {
        const normalized = normalize(word);
        normalizedMap.set(normalized, (normalizedMap.get(normalized) || 0) + 1);
    }
    
    let count = 0;
    for (const freq of normalizedMap.values()) {
        count += freq * (freq - 1) / 2;
    }
    
    return count;
};

复杂度分析

复杂度类型复杂度说明
时间复杂度O(n × m)n 是字符串数量,m 是字符串平均长度,需要遍历所有字符串并标准化
空间复杂度O(n × m)存储哈希表中的标准化字符串,最坏情况下所有字符串都不同