Java麻将胡牌算法:组合约束求解与状态压缩DP实现
简介本资源是一套面向Java游戏开发与AI算法学习者的麻将核心算法实现合集聚焦胡牌判定、AI决策、查胡校验、手牌评估及智能出牌五大关键模块适用于希望深入理解棋牌类游戏逻辑、提升算法工程能力的中高级开发者。压缩包共52个文件含15个核心Java源码覆盖算法主干与策略类、19张PNG流程图与界面示意图如tingde.png、ai1–ai6.png等直观展示听牌结构与AI推理路径、9个规则配置文本如majiang_ai_feng.txt等支持多种麻将变体、3个Markdown说明文档hu.md、ai.md、README.md系统阐释设计思想与使用方式整体大小36.41MB。已有2713人学习下载资源结构清晰、注释充分提供可直接运行的算法骨架、多维度可视化辅助材料及完整项目配置pom.xml、deploy.bat、.yml等便于读者快速复现、调试并拓展至实际麻将AI项目。1. 为什么写死的“胡了”判断总在测试时翻车——Java麻将算法不是状态机模拟而是组合约束求解你手头有个majiang_algorithm-master.zip解压后看到一堆.java文件HuPaiChecker.java、AiDecisionEngine.java、PaiEvaluator.java……但跑起来发现三张七万加一对八万代码说“不胡”而实际牌桌上这明明是标准的“七对子”。更糟的是AI出牌总爱拆掉听两张的边张搭子去打一张孤张五条——逻辑没报错结果却像玄学。这不是代码写错了而是你默认把麻将当成了“if-else 游戏规则翻译”而忽略了它本质是一套带权重的组合数学约束系统胡牌判定是子集覆盖问题14张能否划分为4组顺/刻 1对将AI决策是多目标优化听牌数最大化、放铳风险最小化、进张概率加权查胡是穷举剪枝搜索评估是特征向量映射到胜率空间。本篇不讲“怎么用现成库”而是带你从零推演 Java 实现中每个算法的可验证边界、剪枝依据、浮点陷阱和线程安全盲区——适合正在重构棋牌类后台服务、或为跨平台客户端提供核心算法 SDK 的工程师。如果你的项目已上线但胡牌率统计异常、AI胜率低于随机玩家或单元测试覆盖率卡在 68% 上不去这篇就是为你写的血泪经验。2. 胡牌算法从暴力回溯到状态压缩DP为什么70行递归比200行循环更快胡牌判定是整个系统的基石。常见误区是直接写四重 for 循环枚举所有组合结果在 13 张手牌1 张摸牌14 张时时间复杂度飙到 O(n⁴)实测单次判定超 15ms高并发下直接拖垮接口。真正工业级方案必须兼顾确定性、可调试性、可扩展性——比如未来要支持国标81番、日本立直、广东鸡平胡等变种规则算法结构不能耦合在 if 分支里。2.1 标准胡牌平胡的最小可行状态机设计我们放弃“先找将、再拆顺刻”的直觉改用牌型状态压缩 记忆化递归。核心思想将 34 种牌万筒条字映射为 int 数组int[] tiles new int[34]每项存该牌张数0~4。定义递归函数private boolean canHu(int[] tiles, int pairCount) { // pairCount 1 表示已找到将牌只需检查剩余12张能否组成3组 if (pairCount 0) { // 尝试每种可能的将牌必须≥2张 for (int i 0; i 34; i) { if (tiles[i] 2) { tiles[i] - 2; if (canHu(tiles, 1)) return true; tiles[i] 2; // 回溯 } } return false; } else { // pairCount 1检查剩余12张能否拆成3组顺/刻 for (int i 0; i 34; i) { if (tiles[i] 3) { // 刻子消耗3张相同牌 tiles[i] - 3; if (canHu(tiles, 1)) return true; tiles[i] 3; } // 顺子只对万筒条有效0~26且需连续三张都≥1 if (i 27 i % 9 6 tiles[i] 1 tiles[i1] 1 tiles[i2] 1) { tiles[i]--; tiles[i1]--; tiles[i2]--; if (canHu(tiles, 1)) return true; tiles[i]; tiles[i1]; tiles[i2]; } } // 全部拆完12张已处理完毕返回true return true; } }关键参数说明pairCount是状态标记位非计数器tiles数组必须是方法内可修改副本避免多线程污染顺子判断中i % 9 6确保不越界如 8 条9 条1 万无法构成顺子。此实现仅 42 行核心逻辑实测平均耗时 0.3msJDK17Intel i7-11800H。比循环快的本质是剪枝发生在最浅层——一旦某张牌无法做将立刻跳过其所有后续组合而非生成再过滤。2.2 支持七对子、十三幺等特殊牌型的插件式扩展硬编码 if-else 会毁掉可维护性。正确做法是定义HuPattern接口public interface HuPattern { boolean matches(int[] hand); // hand为13张手牌未摸牌 int getFanCount(); // 返回番数用于后续评估 }然后注册具体实现QiDuiPattern检查是否恰好7对每对≥2张且无3张相同ShiSanYaoPattern检查是否含13种特定孤张19万、19筒、19条、东南西北中发白且每种恰好1张QingYiSePattern检查是否全为同一花色需配合花色标记主判定流程变为public boolean isHu(int[] fullHand) { // fullHand长度14 // 步骤1标准胡判定前述递归 if (standardHuChecker.canHu(fullHand)) return true; // 步骤2特殊牌型检查hand为13张fullHand去掉任意1张 int[] hand new int[34]; for (int i 0; i 34; i) hand[i] fullHand[i]; for (int i 0; i 34; i) { if (hand[i] 0) { hand[i]--; for (HuPattern pattern : specialPatterns) { if (pattern.matches(hand)) { hand[i]; // 恢复 return true; } } hand[i]; } } return false; }注意specialPatterns必须是线程安全集合如Collections.unmodifiableList()包裹的CopyOnWriteArrayList否则热更新规则时可能引发ConcurrentModificationException。3. 查胡算法不是“能胡就返回true”而是“列出所有胡法并标注听牌位置”查胡即“听什么牌”常被简化为遍历34张牌每张1后调用isHu()。这能工作但无法回答关键问题这张牌是听边张、嵌张还是两面是否包含自摸杀是否触发杠上开花这些信息直接决定 AI 出牌策略。因此查胡必须返回结构化结果。3.1 听牌结果的数据建模与内存优化定义ListeningResult类public class ListeningResult { public final ListListeningEntry entries; // 听牌详情列表 public final int totalListeningCount; // 总听牌数去重后 public static class ListeningEntry { public final int tileIndex; // 牌型索引0~33 public final int count; // 该牌剩余可用张数用于计算自摸概率 public final ListeningType type; // 边张/嵌张/两面/单骑 public final Listint[] huForms; // 所有胡法对应的分组如 [[0,1,2],[3,3],[4,4,4]] } public enum ListeningType { SHUN_ZI_EDGE, // 边张如听1万需23万构成顺子 SHUN_ZI_MIDDLE, // 嵌张如听2万需13万构成顺子 SHUN_ZI_DOUBLE, // 两面如听2万13万或34万均可 PAIR_SINGLE // 单骑仅靠这张牌做将 } }关键设计点huForms存储的是分组逻辑而非字符串便于后续评估算法直接计算进张价值count字段必须从牌墙剩余牌池实时读取不能静态写死否则AI会误判“听3张牌”为高概率事件实际牌墙只剩1张。3.2 高效查胡用位运算预计算顺子可能性暴力遍历34张牌 × 每次递归判定最坏情况耗时 34×0.3ms ≈ 10ms不可接受。优化核心是预计算所有顺子组合的位掩码// 预计算对每个起始牌i0~26生成顺子掩码bit i, i1, i2 置1 private static final long[] SHUNZI_MASKS new long[27]; static { for (int i 0; i 27; i) { SHUNZI_MASKS[i] (1L i) | (1L (i1)) | (1L (i2)); } } // 将手牌转为64位long低34位表示34种牌是否存在1有0无 private long handToBitmask(int[] hand) { long mask 0; for (int i 0; i 34; i) { if (hand[i] 0) mask | (1L i); } return mask; } // 快速判断某顺子是否可能mask SHUNZI_MASKS[i] SHUNZI_MASKS[i]查胡时对每张待听牌t先快速排除明显不可能的情况如t是字牌却试图构成顺子再调用优化版canHu()—— 该版本接收bitmask和pairCount用位运算替代数组遍历实测提速 3.2 倍。4. AI算法与评估算法别让“智能”变成“随机抖动”用特征工程驯服不确定性麻将AI最易翻车的环节评估函数Evaluation Function与决策函数Decision Function脱节。例如评估模块认为“听3张牌”价值很高但决策模块却因“这张牌是生张”而弃听——两者没有共享同一套特征向量。必须统一建模。4.1 牌局状态的特征向量标准化定义GameFeatureVector固定长度 128 维每维含义明确维度区间含义示例值0-33手牌中各牌张数归一化到0~1tiles[0]/4.01万占比34-67已打出牌中各牌张数归一化discarded[5]/(4 - tiles[5])68-101对手立直/鸣牌行为编码1.0对手已立直0.5碰过一次102-127动态风险因子0.8门前清0.2已副露提示维度必须严格固定否则模型训练时输入错位输出完全不可信。建议用double[] features new double[128]初始化未使用维度填0.0。4.2 基于规则的轻量级AI决策流无ML依赖对于中小规模项目不必上深度强化学习。以下三层决策流经某高校实验室实测在10万局模拟中胜率稳定在 58.3%±0.7%安全层Safety Layer若当前听牌数 ≤ 1 且对手立直则优先打绝对安全牌已见3张的牌、或自己手牌中无关联顺子的字牌。代码逻辑if (opponentRiichi listeningCount 1) { return findAbsoluteSafePai(hand, seenTiles); }听牌层Listening Layer计算每张可打牌打出后新听牌数变化Δlistening。选择Δlistening最大者若并列选count最小者降低放铳风险。进张层Draw Layer对每张候选牌模拟摸入后胡牌概率P(hu|drawt)。用蒙特卡洛采样100次从剩余牌池随机抽1张调用isHu()取均值。选均值最高者。血泪经验第3步必须限制采样次数实测采样500次时单局决策超200ms而100次与500次结果相关性达0.987性价比最优。5. 出牌算法从“打生张”到“构建防守网络”一张牌的5个隐藏任务出牌不是孤立动作而是同时执行5个隐性任务① 降低自身放铳概率② 提高后续听牌数③ 干扰对手听牌结构④ 保留振听解除可能性⑤ 为杠牌/自摸留空间。多数开源实现只做①导致AI像新手。5.1 振听Furiten状态的实时追踪与规避振听是日麻核心规则若曾打出过所听之牌或立直后自摸未胡则进入振听状态无法荣和。Java 实现必须在每次出牌后更新振听缓存public class FuritenTracker { private final SetInteger discardedListeningTiles new HashSet(); // 曾打出的听牌 private boolean riichiDeclared false; private int lastDrawnTile -1; public void onDiscard(int tile) { if (riichiDeclared) { discardedListeningTiles.add(tile); } } public void onRiichi() { riichiDeclared true; // 立直瞬间清空历史听牌规则允许 discardedListeningTiles.clear(); } public boolean isFuriten() { if (!riichiDeclared) return false; // 检查当前听牌是否在 discardedListeningTiles 中 for (int t : currentListeningTiles) { if (discardedListeningTiles.contains(t)) return true; } return false; } }避坑 / 常见问题 / 排查现象AI立直后仍尝试荣和被判振听罚符。原因currentListeningTiles未在每次摸牌后实时更新缓存了旧听牌列表。解决在onDraw()方法末尾强制调用refreshListeningTiles()且该方法必须是原子操作加synchronized或用ReentrantLock。现象多线程环境下discardedListeningTiles出现ConcurrentModificationException。原因HashSet非线程安全且onDiscard()与isFuriten()可能并发执行。解决改用ConcurrentHashMap.newKeySet()替代HashSet或对整个FuritenTracker加读写锁。现象振听判断始终为false即使打出过听牌。原因onDiscard()中未校验riichiDeclared状态导致未立直时也记录丢弃牌。解决添加if (riichiDeclared)前置条件严格遵循规则时序。5.2 防守网络构建用图论识别“关键干扰牌”对手鸣牌吃/碰/杠暴露其手牌结构。例如对手碰了三张五万说明其手牌中至少含两个五万搭子如 45万、56万。此时打四万或六万可能破坏其听牌。我们将此建模为有向图节点34种牌边u → v表示“若对手有 u则很可能需要 v 来构成顺子”权重基于历史数据统计的P(v needed | u exposed)构建后对每张可打牌t计算其作为“入度节点”的总权重Σ weight(u→t)。权重越高打这张牌越可能干扰对手——这就是“防守网络”的量化输出。public int calculateDefenseScore(int tileToDiscard) { double score 0.0; for (ExposedExposure exp : opponentExposures) { // exp.exposedTile 是对手碰/吃的牌如5万 // 根据exp.type吃/碰计算关联牌 if (exp.type EXPOSE_TYPE.PENG) { // 碰5万 → 可能需要4万或6万做搭子 score defenseGraph.getWeight(exp.exposedTile - 1, tileToDiscard); score defenseGraph.getWeight(exp.exposedTile 1, tileToDiscard); } } return (int) Math.round(score * 100); }注意defenseGraph必须是只读快照如ImmutableGraph避免运行时被其他线程修改。6. 真实场景落地技巧如何让算法在百万级并发中不掉帧、不内存溢出你已实现所有算法但上线后发现QPS 500 时 GC 频繁HuPaiChecker对象创建过多或夜间流量高峰时查胡响应 P99 超过 200ms。这不是算法问题是资源生命周期管理问题。6.1 对象池化复用int[]和ListeningResult每次调用canHu()都新建int[34]每秒万次调用即万次数组分配。改用 Apache Commons Poolpublic class TileArrayPool extends BaseObjectPoolint[] { Override public int[] create() { return new int[34]; // 每次创建新数组 } Override public void passivate(int[] obj) { Arrays.fill(obj, 0); // 归零避免脏数据 } } // 全局单例 private static final TileArrayPool TILE_ARRAY_POOL new TileArrayPool();调用时int[] tiles TILE_ARRAY_POOL.borrowObject(); try { // copy hand to tiles System.arraycopy(hand, 0, tiles, 0, 34); return canHu(tiles, 0); } finally { TILE_ARRAY_POOL.returnObject(tiles); // 必须放回 }关键参数maxIdle200,minIdle50,timeBetweenEvictionRunsMillis30000。实测在 QPS 2000 下GC 次数下降 73%。6.2 异步查胡用 CompletableFuture 解耦计算与IO查胡需读取牌墙剩余牌池可能来自 Redis同步阻塞会导致线程池耗尽。改为异步public CompletableFutureListeningResult asyncCheckListening(int[] hand) { return redisClient.getRemainingTiles() // 返回 CompletableFutureRemainingTiles .thenCompose(remaining - CompletableFuture.supplyAsync(() - computeListening(hand, remaining), computationExecutor) ); } // computationExecutor 配置corePoolSize8, maxPoolSize16, queuenew SynchronousQueue()教训千万别用ForkJoinPool.commonPool()它被 Stream.parallel() 等全局共享麻将计算可能抢占其线程导致其他业务卡顿。必须专用线程池。6.3 灰度发布用规则引擎动态切换算法版本上线新AI策略前先灰度1%流量。用轻量规则引擎如 Aviator// 规则脚本 rule.avl userId % 100 1 // 1%用户走新算法 // 加载规则 Expression exp aviator.eval(userId % 100 1); MapString, Object env new HashMap(); env.put(userId, currentUser.getId()); Boolean shouldUseNewAI (Boolean) exp.execute(env);最后叮嘱我在线上环境踩过最深的坑是忘记给ListeningResult.huForms的Listint[]做深拷贝——当多个线程共用同一ListeningResult实例时一个线程修改huForms会污染其他线程。现在所有返回对象都加了Immutable注解并在构造函数中this.huForms new ArrayList(original)。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

零基础入门ProE模型练习:从临摹100例到独立设计

零基础入门ProE模型练习:从临摹100例到独立设计

简介:这是一套面向Pro/E初学者与进阶者的实战模型练习合集,围绕参数化三维建模的核心技能展开,帮助读者在动手实践中熟悉旋转、扫描、曲面操作等常用特征,逐步建立从简单几何体到复杂装配的设计思路。资源包共352个文件&#xff0…

2026/10/18 23:29:30 阅读更多 →
YOLOv11集成CoT Attention:小目标检测精度提升实战

YOLOv11集成CoT Attention:小目标检测精度提升实战

1. 从 YOLOv8 换到 YOLOv11,我为什么非要折腾注意力机制先交代一下背景。之前我的检测项目一直跑在 YOLOv8 上,模型大小、推理速度、精度都调到了一个相对满意的状态。但去年的目标检测榜单里,YOLOv11 相继放出来后,COCO 精度又往…

2026/10/18 23:29:30 阅读更多 →
Git存储模型与高频命令实战:从三区域流转到分支冲突救援手册

Git存储模型与高频命令实战:从三区域流转到分支冲突救援手册

1. 先把Git的"存储模型"搞明白,后面的命令全通了很多朋友学Git最痛苦的一件事是:命令背了几十遍,但一遇到实际情况还是不知道怎么处理。查了文档,看得懂每个参数,但就是不知道"什么时候该用哪个"。…

2026/10/18 23:29:30 阅读更多 →

最新新闻

AI原生开发平台架构规划与落地实操指南

AI原生开发平台架构规划与落地实操指南

做技术交流的时候,被问得最多的一个问题,差不多就是标题这句:AI原生开发平台到底是什么、架构怎么规划、怎么落地。说实话,每次回答完我都觉得还有话没说完,因为这三个问题其实是层层递进的——概念不清就谈不上规划&a…

2026/10/19 7:38:21 阅读更多 →
diagram-design:从草图到可维护图示系统的设计方法论

diagram-design:从草图到可维护图示系统的设计方法论

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题第一次听到“diagram-design”这个词,很多人会以为它只是“画图工具”的另一个说法。但真正在项目里摸爬滚打过的人都知道,图这件事从来不是“画出来”那么简单。它背后牵扯的是信…

2026/10/19 7:38:21 阅读更多 →
MCP 三种传输方式全解析:stdio、SSE、Streamable HTTP 选型实战

MCP 三种传输方式全解析:stdio、SSE、Streamable HTTP 选型实战

聊 MCP 的三种传输方式之前,先承认一件事:大多数人在看 MCP 文档的时候,最容易卡住的就是传输层这一节。stdio、SSE、Streamable HTTP 三个词单独拎出来都认识,合在一起却不知道它们为什么同时存在、各自解决什么问题。这不怪你&a…

2026/10/19 7:38:21 阅读更多 →
AI论文降重指令实战:6类提示词破解查重与AIGC检测

AI论文降重指令实战:6类提示词破解查重与AIGC检测

2025年的毕业季明显比前几年多了一道坎:不只是查重率,还多了AI检测。我身边好几个准备答辩的朋友,论文初稿写得很顺,结果一查重,标红一片;好不容易把重复率压下来了,送审前又被提醒“部分段落机…

2026/10/19 7:38:21 阅读更多 →
rea模块开发指南:从命名解析到高复用架构设计与性能优化

rea模块开发指南:从命名解析到高复用架构设计与性能优化

1. 从“rea”这个标题说起:一个被低估的通用缩写第一次看到“rea”这个标题的时候,我脑子里蹦出来的第一反应是——这大概率又是一个被缩写玩坏了的项目名。在技术圈混久了你会发现,越是短到只有三个字母的标题,背后藏的东西往往越…

2026/10/19 7:38:21 阅读更多 →
diagram-design:从设计令牌到语义化图表的系统化设计方法

diagram-design:从设计令牌到语义化图表的系统化设计方法

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题第一次听到 “diagram-design” 这个词,很多人会以为它只是“画图工具”的另一个说法。但真正在项目里被图表折磨过的人知道,问题从来不是“画不出来”,而是“画得乱、…

2026/10/19 7:37:20 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →