前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法设计: 成语接龙,自动接龙到“为所欲为”?简单递归搜索就行!

算法设计: 成语接龙,自动接龙到“为所欲为”?简单递归搜索就行!

原创
作者头像
Mintimate
修改2023-10-03 20:54:36
8782
修改2023-10-03 20:54:36
举报
文章被收录于专栏:Mintimate's BlogMintimate's Blog
头图?这个能少?
头图?这个能少?

博客:https://www.mintimate.cn Mintimate’s Blog,只为与你分享

成语接龙

“成语接龙”的话,大家应该很熟悉,就是以一个成语开始,根据成语最后一个字,找到另一个可以接上的成语;再根据最后一个字继续找,一直连接下去。本身就是一个递归的过程,这是一个富有挑战且充满乐趣的语言游戏。

当然,不同地方有不同的版本,一般有两个主要分支版本:

  • 多音字版本: 成语与成语间,使用多音字进行连接;如: 风和日丽(lì)->立足之地(dì)->地大物博(bó)
  • 精准字版本: 成语与成语间,使用精确的字进行连接;如: 明知故问->问长问短->短兵相接

记得我小时候,语文老师就喜欢课前成语接龙,成语接龙失败的同学,就在三天后的语文课上一起表演节目;我当时偶尔接龙不出来,要上台表演唱歌,我每次都是假唱、对口型(🤫哔~~)

为所欲为

接龙到“为所欲为”的话,是上次粉丝突然和我说,给她任意一个成语,她可以接龙到“为所欲为”;我立马让她接龙一个: 魑魅魍魉

图片仅供参考
图片仅供参考

于是…… 我成功结束了对话……

牛吧
牛吧

不过,后来我得知,是因为她发现了一个神奇的网站:

自动成语接龙
自动成语接龙

确实挺好玩的;不过,不够有趣,我们能不能更有趣点?比如:接龙的内容更长? 接龙的成语有些随机?

算法设计

我们需要设计一个自动接龙到成语接龙的算法,需要如何设计呢?

算法的设计很简单,在有用词库的前提下,主要分为两种:

  • 广度优先和词图的算法;
  • 递归搜索成语。

实际上,上文展示的网站,使用的算法就是广度优先和词图的算法,建立一个词图,内部包含多条路径,根据所给的词进行命中。这个留到下期,有机会和大家说说如何设计。

本次,我们就演示看看,如何靠递归,一层一层搜索出成语。

词库获取

首先,我们需要一个词库,用一个词库来获取成语。你当然可以使用数据库实现,把古汉语大全直接入库,之后使用SQL去查询。

当然,对于我来说有些尴尬,最近我用的比较多的是Nuxt3;暂时,不想直接上Java。虽然用Nodejs,写个中间件或者直接用Nodejs也可以作为后端操作Sqlite、MySQL等等数据库,但是就为了一个小小的功能,引入数据库,我认为不是很划算。

为此,我这里找到了这个库: https://github.com/theajack/cnchar

cnchar库
cnchar库

而这个库有一个子库,集成了一个成语库:

成语库
成语库

我们按提示,使用一下,试试看:

代码语言:javascript
复制
import cnchar from "cnchar";
import 'cnchar-idiom'
const testWords = cnchar.idiom(['为', '', '', '']); // 查找“为”开头的成语
console.log(testWords)

打印结果:

打印结果
打印结果

这样就解决了词库问题。不过词库可能有点不准,或者和古汉语词典有所差异;如果有严格的要求,建议还是需要SQL。

嘿嘿
嘿嘿

递归搜索

接下来,我们需要构建递归搜索的算法,总体的逻辑:

递归逻辑
递归逻辑

调用API的方法,我们留在下次讲,也就是使用图的方法,实现的快速搜索。

这次我们的递归逻辑很简单,在Vue内,首先是判断是不是“为”结尾,如果是,就不用“为所欲为”遍历了:

代码语言:javascript
复制
// 获取成语最后一个字(或拼音=>预留,本次未使用)
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
}

之后,我们定义一个方法,用于查找每个成语最后一个字,可以关联出的成语:

代码语言:javascript
复制
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;
};

注意,我这里还使用了一个洗牌算法,实际上就是数组乱序打乱,参考:

代码语言:javascript
复制
// 洗牌算法
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;
}

当然,最后生成一个入口函数:

代码语言:javascript
复制
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构建:

NuxtUI
NuxtUI

构建了一个入口文件,我们来测试一下:

效果
效果

因为使用了洗牌算法,每次生成的成语接龙,存在随机性:

存在随机性
存在随机性

如果运气不好,出现递归超出设置的最大次数:

超出最大次数
超出最大次数

当然,一些词注定无法成语接龙:

无法成语接龙的词
无法成语接龙的词

END

本次的递归方法成语接龙就到这里啦~

要我说,这成语接龙程序简直是个调皮鬼!你跟它说个正经成语,它就故意接些莫名其妙的,把你绕晕了。甚至有时候,还会因为递归走得太远而走丢~ 成语接龙本就是个游戏,不必太较真。只要玩得开心,哪怕接不上龙也无妨。

反正人生就像这个程序,充满无穷乐趣。我们何必太执着结果,珍惜美好的过程就好。

当然,如果你想增加效率、避免走丢的话,下次我介绍用图的方法~~~

Nice
Nice

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 成语接龙
    • 为所欲为
    • 算法设计
    • 词库获取
    • 递归搜索
    • 效果
    • END
    相关产品与服务
    云数据库 MySQL
    腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档