灯谜是中国传统文化中的文字益智游戏,通过谜面的描述、谐音、拆字等手法隐射谜底。本文将使用Java实现一个灯谜求解系统,重点讲解字符串约束满足(Constraint Satisfaction)、Trie前缀树匹配以及候选答案智能筛选的核心算法。
灯谜结构与算法抽象
一个灯谜由谜面、谜目(提示猜射范围)和谜底组成。从算法角度看,灯谜求解可以建模为:
- 约束提取:从谜面解析出谜底的特征约束(字数、部首、谐音、类别等)
- 候选空间构建:根据谜目确定搜索范围(如”打一字”、”打一成语”、”打一动物”)
- 约束满足验证:逐一验证候选答案是否满足所有约束
- 评分排序:对满足约束的候选进行相关性评分
核心算法设计
约束模型
/**
* 灯谜约束类
* 描述谜底必须满足的条件
*/
public class Constraint {
public enum Type {
LENGTH, // 长度约束(字数)
PREFIX, // 前缀约束
SUFFIX, // 后缀约束
CONTAINS, // 包含特定字符
CATEGORY, // 类别约束(动物、植物、成语等)
HOMOPHONE, // 谐音约束
RADICAL, // 部首约束
STRUCTURE // 结构约束(拆字、组合等)
}
private final Type type;
private final String value;
private final int intValue; // 用于数值型约束如长度
public Constraint(Type type, String value) {
this(type, value, -1);
}
public Constraint(Type type, int intValue) {
this(type, null, intValue);
}
public Constraint(Type type, String value, int intValue) {
this.type = type;
this.value = value;
this.intValue = intValue;
}
public boolean matches(String candidate) {
switch (type) {
case LENGTH:
return candidate.length() == intValue;
case PREFIX:
return candidate.startsWith(value);
case SUFFIX:
return candidate.endsWith(value);
case CONTAINS:
return candidate.contains(value);
case CATEGORY:
// 类别验证由外部词典提供
return true;
case HOMOPHONE:
// 谐音验证需要拼音库支持
return true;
case RADICAL:
// 部首验证需要汉字拆解库
return true;
case STRUCTURE:
// 结构约束由专门逻辑处理
return true;
default:
return true;
}
}
public Type getType() { return type; }
public String getValue() { return value; }
public int getIntValue() { return intValue; }
}
Trie前缀树词典
import java.util.*;
/**
* Trie前缀树节点
* 用于高效存储和检索灯谜候选答案
*/
class TrieNode {
// 子节点映射:字符 -> 子节点
Map<Character, TrieNode> children;
// 是否为完整词条的结尾
boolean isEndOfWord;
// 存储该节点对应的完整词条列表(支持多词共享前缀)
List<String> words;
// 词条类别标签
Set<String> categories;
public TrieNode() {
this.children = new HashMap<>();
this.isEndOfWord = false;
this.words = new ArrayList<>();
this.categories = new HashSet<>();
}
}
/**
* Trie前缀树实现
* 支持按前缀搜索、按长度筛选、按类别过滤
*/
public class RiddleDictionary {
private final TrieNode root;
private int size; // 词条总数
public RiddleDictionary() {
this.root = new TrieNode();
this.size = 0;
}
/**
* 插入词条
* 时间复杂度: O(m),m为词条长度
*/
public void insert(String word, String category) {
if (word == null || word.isEmpty()) return;
TrieNode current = root;
for (char ch : word.toCharArray()) {
current = current.children.computeIfAbsent(ch, k -> new TrieNode());
}
current.isEndOfWord = true;
current.words.add(word);
if (category != null) {
current.categories.add(category);
}
size++;
}
/**
* 按前缀搜索所有匹配词条
* 时间复杂度: O(m + k),m为前缀长度,k为结果数
*/
public List<String> searchByPrefix(String prefix) {
List<String> results = new ArrayList<>();
if (prefix == null || prefix.isEmpty()) return results;
TrieNode node = findNode(prefix);
if (node == null) return results;
collectAllWords(node, results);
return results;
}
/**
* 按前缀和长度联合搜索
* 用于"X字开头且为N字"的约束
*/
public List<String> searchByPrefixAndLength(String prefix, int length) {
List<String> results = new ArrayList<>();
TrieNode node = findNode(prefix);
if (node == null) return results;
collectWordsByLength(node, prefix.length(), length, prefix, results);
return results;
}
/**
* 按长度范围搜索(用于无前缀约束时)
* 时间复杂度: O(n),n为词典规模
*/
public List<String> searchByLength(int length) {
List<String> results = new ArrayList<>();
collectWordsByLength(root, 0, length, "", results);
return results;
}
/**
* 辅助方法:定位前缀对应的节点
*/
private TrieNode findNode(String prefix) {
TrieNode current = root;
for (char ch : prefix.toCharArray()) {
TrieNode node = current.children.get(ch);
if (node == null) return null;
current = node;
}
return current;
}
/**
* 辅助方法:收集节点下所有完整词条
*/
private void collectAllWords(TrieNode node, List<String> results) {
if (node.isEndOfWord) {
results.addAll(node.words);
}
for (TrieNode child : node.children.values()) {
collectAllWords(child, results);
}
}
/**
* 辅助方法:按精确长度收集词条(DFS)
*/
private void collectWordsByLength(TrieNode node, int currentDepth,
int targetLength, String currentWord,
List<String> results) {
if (currentDepth == targetLength) {
if (node.isEndOfWord) {
results.addAll(node.words);
}
return;
}
for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
collectWordsByLength(entry.getValue(), currentDepth + 1,
targetLength, currentWord + entry.getKey(), results);
}
}
public int size() {
return size;
}
}
灯谜求解引擎
/**
* 灯谜求解引擎
* 基于约束满足思想,通过多轮筛选缩小候选空间
*/
public class RiddleSolver {
private final RiddleDictionary dictionary;
public RiddleSolver(RiddleDictionary dictionary) {
this.dictionary = dictionary;
}
/**
* 求解灯谜
* 核心流程:提取约束 -> 构建候选集 -> 逐条过滤 -> 评分排序
*
* @param riddle 谜面对象
* @return 按匹配度排序的候选答案列表
*/
public List<Candidate> solve(Riddle riddle) {
// 步骤1:提取约束
List<Constraint> constraints = extractConstraints(riddle);
System.out.println("[约束提取] 共 " + constraints.size() + " 条约束");
for (Constraint c : constraints) {
System.out.println(" - " + c.getType() + ": " +
(c.getValue() != null ? c.getValue() : c.getIntValue()));
}
// 步骤2:根据最严格约束构建初始候选集
List<String> candidates = buildInitialCandidates(constraints);
System.out.println("[候选集] 初始大小: " + candidates.size());
// 步骤3:逐条应用约束过滤
for (Constraint constraint : constraints) {
int before = candidates.size();
candidates = filterByConstraint(candidates, constraint);
System.out.println("[过滤] " + constraint.getType() + ": " +
before + " -> " + candidates.size());
}
// 步骤4:评分排序
List<Candidate> scored = scoreCandidates(candidates, riddle);
scored.sort((a, b) -> Double.compare(b.score, a.score));
return scored;
}
/**
* 约束提取
* 从谜面和谜目中解析显式和隐式约束
*/
private List<Constraint> extractConstraints(Riddle riddle) {
List<Constraint> constraints = new ArrayList<>();
// 从谜目提取长度约束(如"打一字"=长度1,"打一成语"=通常4字)
String category = riddle.getCategory();
if (category != null) {
constraints.add(new Constraint(Constraint.Type.CATEGORY, category));
// 根据类别推断长度
int inferredLength = inferLengthFromCategory(category);
if (inferredLength > 0) {
constraints.add(new Constraint(Constraint.Type.LENGTH, inferredLength));
}
}
// 从谜面提取字符包含约束
String surface = riddle.getSurface();
if (surface != null) {
// 检测谜面中是否暗示了部首或特定字符
List<String> hintedChars = extractHintedChars(surface);
for (String ch : hintedChars) {
constraints.add(new Constraint(Constraint.Type.CONTAINS, ch));
}
}
return constraints;
}
/**
* 根据类别推断谜底长度
*/
private int inferLengthFromCategory(String category) {
switch (category) {
case "字": return 1;
case "成语": return 4;
case "人名": return 2; // 通常2-3字
case "地名": return 2; // 通常2-3字
case "动物": return 2; // 通常2字
case "植物": return 2; // 通常2字
default: return -1; // 未知,不添加长度约束
}
}
/**
* 从谜面提取暗示字符
* 简化的启发式规则:提取谜面中的关键提示字
*/
private List<String> extractHintedChars(String surface) {
List<String> hints = new ArrayList<>();
// 检测"X字"、"X旁"等模式
String[] patterns = {"字", "旁", "头", "底", "边"};
for (String pattern : patterns) {
int idx = surface.indexOf(pattern);
if (idx > 0) {
hints.add(surface.substring(idx - 1, idx));
}
}
return hints;
}
/**
* 构建初始候选集
* 优先使用最严格的约束(如前缀约束)减少搜索空间
*/
private List<String> buildInitialCandidates(List<Constraint> constraints) {
// 寻找最严格的约束来构建初始集
Optional<Constraint> prefixConstraint = constraints.stream()
.filter(c -> c.getType() == Constraint.Type.PREFIX)
.findFirst();
Optional<Constraint> lengthConstraint = constraints.stream()
.filter(c -> c.getType() == Constraint.Type.LENGTH)
.findFirst();
if (prefixConstraint.isPresent()) {
String prefix = prefixConstraint.get().getValue();
if (lengthConstraint.isPresent()) {
return dictionary.searchByPrefixAndLength(prefix, lengthConstraint.get().getIntValue());
}
return dictionary.searchByPrefix(prefix);
}
if (lengthConstraint.isPresent()) {
return dictionary.searchByLength(lengthConstraint.get().getIntValue());
}
// 无有效约束时返回空(实际应返回全部或要求更明确的谜目)
return new ArrayList<>();
}
/**
* 应用单条约束过滤候选集
*/
private List<String> filterByConstraint(List<String> candidates, Constraint constraint) {
List<String> filtered = new ArrayList<>();
for (String candidate : candidates) {
if (constraint.matches(candidate)) {
filtered.add(candidate);
}
}
return filtered;
}
/**
* 候选答案评分
* 基于与谜面的相关性计算匹配分数
*/
private List<Candidate> scoreCandidates(List<String> candidates, Riddle riddle) {
List<Candidate> scored = new ArrayList<>();
String surface = riddle.getSurface();
for (String candidate : candidates) {
double score = calculateScore(candidate, surface);
scored.add(new Candidate(candidate, score));
}
return scored;
}
/**
* 计算单个候选的匹配分数
* 综合多种启发式特征
*/
private double calculateScore(String candidate, String surface) {
double score = 0.0;
if (surface == null || surface.isEmpty()) {
return score;
}
// 特征1:字符重叠度(谜底字出现在谜面中的比例)
int overlap = 0;
for (char ch : candidate.toCharArray()) {
if (surface.indexOf(ch) >= 0) {
overlap++;
}
}
score += (overlap / (double) candidate.length()) * 30;
// 特征2:长度匹配(默认基础分)
score += 10;
// 特征3:常见度(简化为词条长度相关,成语/俗语通常固定长度)
if (candidate.length() == 4) {
score += 5; // 四字成语加分
}
return score;
}
/**
* 灯谜数据类
*/
public static class Riddle {
private String surface; // 谜面
private String category; // 谜目(如"字"、"成语"、"动物")
private String hint; // 额外提示
public Riddle(String surface, String category) {
this.surface = surface;
this.category = category;
}
public String getSurface() { return surface; }
public String getCategory() { return category; }
public String getHint() { return hint; }
}
/**
* 候选答案评分结果
*/
public static class Candidate {
public final String answer;
public final double score;
public Candidate(String answer, double score) {
this.answer = answer;
this.score = score;
}
@Override
public String toString() {
return String.format("%s (%.1f)", answer, score);
}
}
}
演示程序
/**
* 灯谜系统演示
*/
public class RiddleDemo {
public static void main(String[] args) {
// 构建词典
RiddleDictionary dict = new RiddleDictionary();
// 插入常用字
String[] chars = {"日", "月", "明", "山", "水", "火", "木", "林", "森", "人",
"大", "天", "王", "玉", "主", "石", "口", "回", "品", "晶"};
for (String c : chars) {
dict.insert(c, "字");
}
// 插入常见成语
String[] idioms = {"一马当先", "二话不说", "三心二意", "四面楚歌", "五光十色",
"六神无主", "七上八下", "八面玲珑", "九牛一毛", "十全十美",
"画蛇添足", "守株待兔", "亡羊补牢", "掩耳盗铃", "刻舟求剑"};
for (String idiom : idioms) {
dict.insert(idiom, "成语");
}
System.out.println("词典加载完成,共 " + dict.size() + " 条词条\n");
// 创建求解器
RiddleSolver solver = new RiddleSolver(dict);
// 测试灯谜1:打一字
RiddleSolver.Riddle riddle1 = new RiddleSolver.Riddle(
"太阳挂在树梢上", "字"
);
System.out.println("灯谜1:" + riddle1.getSurface() + " (" + riddle1.getCategory() + ")");
List<RiddleSolver.Candidate> answers1 = solver.solve(riddle1);
System.out.println("候选答案: " + answers1.subList(0, Math.min(5, answers1.size())));
System.out.println();
// 测试灯谜2:打一成语
RiddleSolver.Riddle riddle2 = new RiddleSolver.Riddle(
"画蛇添足反类犬", "成语"
);
System.out.println("灯谜2:" + riddle2.getSurface() + " (" + riddle2.getCategory() + ")");
List<RiddleSolver.Candidate> answers2 = solver.solve(riddle2);
System.out.println("候选答案: " + answers2.subList(0, Math.min(5, answers2.size())));
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| Trie插入 | O(m) | O(m) | m为词条长度 |
| 前缀搜索 | O(m + k) | O(k) | m为前缀长,k为结果数 |
| 约束过滤 | O(n * c) | O(n) | n为候选数,c为约束验证成本 |
| 候选评分 | O(n * L) | O(n) | L为谜面长度 |
| 整体求解 | O(d + n * (c + L)) | O(d + n) | d为词典规模 |
扩展方向
- 汉字拆解库:引入部首、笔画、结构信息,支持”拆字”类灯谜
- 拼音谐音匹配:集成拼音库,处理谐音类谜语(如”羊”谐音”洋”)
- NLP语义分析:使用词向量计算谜面与谜底的语义相似度
- 用户反馈学习:记录猜谜历史,通过机器学习优化评分模型
总结
灯谜求解系统将传统文化游戏转化为约束满足问题,通过Trie前缀树实现高效的候选空间检索,利用多轮约束过滤逐步缩小解空间。虽然真实灯谜涉及复杂的语言学和文化遗产知识,但本文实现的框架展示了如何将算法思维应用于趣味问题的求解,为读者提供了字符串处理和搜索算法的实践案例。