博客:https://www.mintimate.cn Mintimate’s Blog,只为与你分享
“成语接龙”的话,大家应该很熟悉,就是以一个成语开始,根据成语最后一个字,找到另一个可以接上的成语;再根据最后一个字继续找,一直连接下去。本身就是一个递归的过程,这是一个富有挑战且充满乐趣的语言游戏。
当然,不同地方有不同的版本,一般有两个主要分支版本:
记得我小时候,语文老师就喜欢课前成语接龙,成语接龙失败的同学,就在三天后的语文课上一起表演节目;我当时偶尔接龙不出来,要上台表演唱歌,我每次都是假唱、对口型(🤫哔~~)
接龙到“为所欲为”的话,是上次粉丝突然和我说,给她任意一个成语,她可以接龙到“为所欲为”;我立马让她接龙一个: 魑魅魍魉。
于是…… 我成功结束了对话……
不过,后来我得知,是因为她发现了一个神奇的网站:
确实挺好玩的;不过,不够有趣,我们能不能更有趣点?比如:接龙的内容更长? 接龙的成语有些随机?
我们需要设计一个自动接龙到成语接龙的算法,需要如何设计呢?
算法的设计很简单,在有用词库的前提下,主要分为两种:
实际上,上文展示的网站,使用的算法就是广度优先和词图的算法,建立一个词图,内部包含多条路径,根据所给的词进行命中。这个留到下期,有机会和大家说说如何设计。
本次,我们就演示看看,如何靠递归,一层一层搜索出成语。
首先,我们需要一个词库,用一个词库来获取成语。你当然可以使用数据库实现,把古汉语大全直接入库,之后使用SQL去查询。
当然,对于我来说有些尴尬,最近我用的比较多的是Nuxt3;暂时,不想直接上Java。虽然用Nodejs,写个中间件或者直接用Nodejs也可以作为后端操作Sqlite、MySQL等等数据库,但是就为了一个小小的功能,引入数据库,我认为不是很划算。
为此,我这里找到了这个库: https://github.com/theajack/cnchar
而这个库有一个子库,集成了一个成语库:
我们按提示,使用一下,试试看:
import cnchar from "cnchar";
import 'cnchar-idiom'
const testWords = cnchar.idiom(['为', '', '', '']); // 查找“为”开头的成语
console.log(testWords)
打印结果:
这样就解决了词库问题。不过词库可能有点不准,或者和古汉语词典有所差异;如果有严格的要求,建议还是需要SQL。
接下来,我们需要构建递归搜索的算法,总体的逻辑:
调用API的方法,我们留在下次讲,也就是使用图的方法,实现的快速搜索。
这次我们的递归逻辑很简单,在Vue内,首先是判断是不是“为”结尾,如果是,就不用“为所欲为”遍历了:
// 获取成语最后一个字(或拼音=>预留,本次未使用)
const getLastWordOrSpell = (s, m) => {
s = s || ''
if (m === 'spell') {
return cnchar.spell(s.replace(/^(.*[n])*.*(.|n)$/g, '$2')).toLowerCase()
} else {
return s.slice(-1);
}
}
…
const keyWord = inputWord.value
const lastWord = getLastWordOrSpell(keyWord)
if (lastWord === '为') {
generatorChainsWords.value = "『" + inputWord.value + "』不用『为所欲为』,也能『为所欲为』";
loading.value = false;
return
}
之后,我们定义一个方法,用于查找每个成语最后一个字,可以关联出的成语:
const getIdiomChainByRecursive = (lastWord, deep, find, outputResult, selected) => {
let result = -3;
if (selected.has(lastWord)) {
// 当前词已经存在
return -2;
}
// 获取所有以该拼音开头的成语数组(使用洗牌算法)
const tempResArrSource = shuffleArray(cnchar.idiom([lastWord, '', '', '']))
const tempResArr = tempResArrSource.filter(item => {
const lastCharacter = item.slice(-1); // 获取最后一个字符
return !selected.has(lastCharacter); // 检查最后一个字符是否存在于 Set 内
});
if (!tempResArr.length) {
return -1;
}
// 判断最后一个字的拼音是否是“为”
const isFirstIdiomWei = getLastWordOrSpell(tempResArr[0]) === '为';
if (isFirstIdiomWei) {
// 如果是,直接将结果设置为"为所欲为"
outputResult.push(tempResArr[0]);
result = outputResult.join(' -> ');
} else {
for (let i = 0; i < tempResArr.length; i++) {
// 本次遍历的成语
const currentIdiom = tempResArr[i];
// 本次遍历成语的最后一个字
const currentLastWordSpell = getLastWordOrSpell(currentIdiom);
selected.add(lastWord)
outputResult.push(currentIdiom);
if (result === 0) {
break;
}
if (currentLastWordSpell === '为') {
// 如果最后一个字是"为",找到了目标成语,结束循环
result = outputResult.join(' -> ');
break;
} else {
if (deep > MAX_DEPTH) {
result = 0;
break
}
// 否则,继续递归调用
result = getIdiomChainByRecursive(currentLastWordSpell, deep++, false, outputResult, new Set(selected));
if (result === -2) {
// 没找到,或者深度达到最大
outputResult.pop()
continue
}
if (result && result !== -1) {
break
} else {
// 如果没有找到结果,回溯,尝试其他成语
outputResult.pop();
}
}
}
}
return result;
};
注意,我这里还使用了一个洗牌算法,实际上就是数组乱序打乱,参考:
// 洗牌算法
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
当然,最后生成一个入口函数:
const chainWords = getIdiomChainByRecursive(lastWord, 0, false, [keyWord], selected)
if (chainWords === -1) {
generatorChainsWords.value = "『" + inputWord.value + "』不能『为所欲为』"
} else if (chainWords === 0) {
generatorChainsWords.value = "『" + inputWord.value + "』迷路了,请重试『为所欲为』"
} else {
generatorChainsWords.value = chainWords + "->为所欲为"
}
loading.value = false
到此,我们的递归就生成完成了。
最后,我们来看看效果,我这里用Nuxt3配合NuxtUI构建:
构建了一个入口文件,我们来测试一下:
因为使用了洗牌算法,每次生成的成语接龙,存在随机性:
如果运气不好,出现递归超出设置的最大次数:
当然,一些词注定无法成语接龙:
本次的递归方法成语接龙就到这里啦~
要我说,这成语接龙程序简直是个调皮鬼!你跟它说个正经成语,它就故意接些莫名其妙的,把你绕晕了。甚至有时候,还会因为递归走得太远而走丢~ 成语接龙本就是个游戏,不必太较真。只要玩得开心,哪怕接不上龙也无妨。
反正人生就像这个程序,充满无穷乐趣。我们何必太执着结果,珍惜美好的过程就好。
当然,如果你想增加效率、避免走丢的话,下次我介绍用图的方法~~~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。