Anagram及相关算法题

什么是Anagram

Anagram常被译为变位词,私以为译为易位构词或者易位字谜更准确。变位这个词常跟变格一起作为拉丁语系的语法现象出现,而anagram指将一个单词或词组的字母重新排列,形成另一个具有意义的组合。相同数量和种类的字母、却能产生逻辑相关或相反的单词组合。有点像中文的字谜,只不过象形文字是通过笔画或组成部分的排列组合或增减来变换。

第一次接触到变位词是在某本单词书里,作者写到 dormitory 可以变换成 dirty room,哇哦,魔法!竟然可以这么巧,是人创造了语言还是神创造了语言,巴别塔还有彩蛋吗。

常见的还有:

引自 Anagram - Wikipedia

  • 转换后是对某事物的评论,可以是戏谑的、批评的或讽刺的:

    • "New York Times" = "monkeys write"
    • "Church of Scientology" = "rich-chosen goofy cult"
    • "McDonald's restaurants" = "Uncle Sam's standard rot"(小编评:不准说麦麦坏话!
  • 转换后是同义词:

    • "evil" = "vile"
    • "a gentleman" = "elegant man"
    • "silent" = "listen"
    • "eleven plus two" = "twelve plus one"
  • 转换后是反义词(也叫 antigram

    • "restful" = "fluster"
    • "cheater" = "teacher"
    • "funeral" = "real fun"
    • "adultery" = "true lady"
    • "forty five" = "over fifty"
    • "Santa" = "Satan"
  • 转换后是个句子:

    • "William Shakespeare" = "I am a weakish speller"
    • "Madam Curie" = "Radium came"
    • "George Bush" = "He bugs Gore"
    • "Tom Marvolo Riddle" = "I am Lord Voldemort"
    • "Jim Morrison" = "Mr Mojo Risin"
    • "The Morse code" = "Here come dots"

文艺作品里

Anagram的文字游戏早在希腊就出现了,他们尤其喜欢用来解读名字,相信这里面隐藏着命运的旨意;我想这是很符合希腊哲学的,相信感性的事物后隐藏着普遍的规律。到了中世纪,神学家用它去解读圣经,科学家用它来保护自己的成果(用来记录研究发现避免被抄袭,类似于一种加密手段吧)。而现代的许多小说家、推理作家更是热衷于这个游戏,比如纳博科夫,喜欢以语言为游戏、以文本为迷宫,小说里有大量的双关、变位词、名字暗号、对称结构,之前还看过一篇文章讲他和超文本的渊源

“正如莎翁《哈姆雷特》是所有复仇题材的原型小说一样,《微暗的火》至今都是文学界、计算机技术界的超文本原型研究主要关注的对象。”

著名的还有:

部分引自 变位词 - 吾萌百科

  • 《凉宫春日的分裂》&《凉宫春日的惊愕》
    • 渡桥泰水说她的名字读法是わたはし やすみ(Watahashi Yasumi),但实际上正常读作わたはし やすみず(Wa_ta_ha_shi Ya_su_mi_zu)即可,而这个名字的变位是“わたしはすずみや”(Wa_ta_shi wa Su_zu_mi_ya,私は涼宮),意为“我是凉宫”
  • 泡坂妻夫
    • 日本推理小说家,本名厚川昌男(あつかわ まさお,A_tsu_ka_wa Ma_sa_o),笔名あわさか つまお(A_wa_sa_ka Tsu_ma_o)是其变位词。
  • 《爱丽丝镜中奇遇》(刘易斯·卡罗尔)
    • 卡罗尔是文字游戏大师。在《Jabberwocky》这首诗中,虽然没有直接的 anagram,但他创造的 nonsense words(无意义词)本身就带有字母重组的趣味性。
    • 他本人的名字 Charles Lutwidge Dodgson 的笔名 Lewis Carroll 也是一个 anagram 式的变体(拉丁化双关)。
  • 纳博科夫
    • 变位词是纳博科夫小说中频繁出现的文字游戏,几乎在其任何一部小说作品中都能找到。…在《洛丽塔》中,他的名字 Vladimir Nabokov 是用变位词的方式隐藏在 Vivian Darkbloom 之中。
    • 《美国丽人》作为一部同样以恋童癖为主题的电影,其男主人公莱斯特·伯纳姆(Lester Burnham)的名字致敬了《洛丽塔》:Lester Burnham ⇄ "Humbert learns(亨伯特知错了)"
  • 《哈利·波特》系列
    • 伏地魔本名汤姆·马沃罗·里德尔(Tom Marvolo Riddle)来源是“我是伏地魔”(I am Lord Voldemort)的变位词。
    • 野史记载西弗勒斯·斯内普(Severus Snape)名字来源是"Persues Evans"
  • 《达·芬奇密码》
    • O, Draconian devil! Oh, lame Saint! (哦,严酷的魔鬼!哦,跛足的圣人!) ⇄ LEONARDO DA VINCI THE MONA LISA(列奥纳多·达·芬奇 《蒙娜丽莎》)

算法题里

anagram相关的算法题比较少且简单,不像回文能衍生出子子孙孙无穷尽也的题目。在忽略词组含义的情况下,说白了就是字母守恒、排列组合。通常给定一个原词,返回给定输入以该词为基础的变位词组合、组合数量等,多用哈希表来记录,可用滑动窗口来优化。

值得一提的是这里哈希表的key有一些trick的做法。比如可以用字母与'a'的UTF-16 编码单元值之差(正好落在1-26)代替字母,如此便可用26长度的数组代替哈希表、数组索引作为key。再比如评论区有人提到,美区有大神用质数表示26个字母,把字符串的各个字母相乘,这样可保证字母异位词的乘积必定是相等的(如果一些质数的乘积相同,那么这些质数一定相同)。当然这个有溢出风险。

49. 字母异位词分组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * @param {string[]} strs
 * @return {string[][]}
 */
var groupAnagrams = function (strs) {
    const map = {}
    for (let str of strs) {
        const wordCnt = Array(26).fill(0)
        for (let c of str) {
            // str.charCodeAt(index) 是一个用来获取字符串中特定位置字符的 UTF-16编码数值 的方法,不传参时默认index=0
            wordCnt[c.charCodeAt() - 'a'.charCodeAt()]++
        }
        map[wordCnt] ? map[wordCnt].push(str) : map[wordCnt] = [str]
    }
    return Object.values(map)
};

438. 找到字符串中所有字母异位词

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, p) {
    const result = []
    const sLen = s.length
    const pLen = p.length
    const sWindowArr = new Array(26).fill(0)
    const pWindowArr = new Array(26).fill(0)

    const isWindowEqual = (m, n) => {
        for (let i = 0; i < 26; i++) {
            if (m[i] !== n[i]) {
                return false
            }
        }
        return true
    }

    // 第一个窗口
    for (let i = 0; i < pLen; i++) {
        sWindowArr[s.charCodeAt(i) - 'a'.charCodeAt(0)]++
        pWindowArr[p.charCodeAt(i) - 'a'.charCodeAt(0)]++
    }
    if (isWindowEqual(sWindowArr, pWindowArr)) {
        result.push(0)
    }
    // 左出右进,定长滑动窗口
    for (let i = pLen; i < sLen; i++) {
        sWindowArr[s.charCodeAt(i - pLen) - 'a'.charCodeAt(0)]--
        sWindowArr[s.charCodeAt(i) - 'a'.charCodeAt(0)]++
        if (isWindowEqual(sWindowArr, pWindowArr)) {
            result.push(i - pLen + 1)
        }
    }

    return result
};

2514. 统计同位异构字符串数目

这个题,纯数学题,涉及一些数论知识,我反手就是抄(不是,抄都懒得抄,看看得了...有空再研究下吧)。

要点有二。一是排列组合运算,对于一个长度为 n 的单词,如果其中字符 c 出现的频次为 cnt[c],那么这个单词的异位词数量为 n! / (cnt[c1]! * cnt[c2]! * ... * cnt[ck]!)

二是理解模运算和乘法逆元,乘法逆元之于模运算,类似于倒数之于除法运算。除法的分配率在模运算中不成立,因此要用乘法逆元来解决除法的模运算。对于质数 MODa 的乘法逆元是 a^(MOD-2) % MOD。(费马小定理 & 欧拉定理 - OI Wiki