From 5409ab0b6b3001fc4e51beb28491b1a9ed12d874 Mon Sep 17 00:00:00 2001 From: forFudan Date: Mon, 27 Nov 2023 17:31:11 +0100 Subject: [PATCH] Update tutorial --- docs/learn.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/docs/learn.md b/docs/learn.md index 0c615a1..ec45e30 100644 --- a/docs/learn.md +++ b/docs/learn.md @@ -332,7 +332,9 @@ nav_order: 12 > >以上优先级,还可以高度归纳为:**少顺整散连交断大**。 -取根时,如果有多种不同的拆分方式,则按**优先级从高到低**依次检查以上规则,选取符合条件的,排除不满足的,最终得到唯一的拆分方案。以下为规则详细介绍: +取根时,如果有多种不同的拆分方式,则按**优先级从高到低**依次检查以上规则,选取符合条件的,排除不满足的,最终得到唯一的拆分方案。因此,某种意义上来说,宇浩输入法的拆分规则,其实是一种「比较和淘汰」的排除法,[后文中会展示这些步骤的机器实现](#机器实现)。 + +以下为规则详细介绍: {: .note} 在进行拆分前,首先需要「确认」某个字根到底存不存在,是否合规,也就是检查字根的「内在属性」。比如「土」「士」,两横的长度直接决定了字根的异同,再比如「王」中间如果被笔画「穿心」,则不再是「王」。核心规则有三条:散件不分割、竖向不包夹、横间不穿心。这部分的讨论作为进阶内容,于此处跳过。若有兴趣,可以在后续章节阅读。 @@ -404,10 +406,10 @@ nav_order: 12 字根取大,指的是按照笔顺拆字时: 1. 让完全符合笔顺的部件尽可能地大。也就是说,只要其中某个字根多写一笔仍然符合笔顺,就多写一笔。 -2. 让首笔靠前的字根尽可能地大。 -3. 让非歪斜根尽可能地大。 +2. 让非歪斜根尽可能地大。 +3. 让首笔靠前的字根尽可能地大。 -「取大原则」,按定义,是一个兜底原则。它保证了最终只有一个候选方案能够胜出。一般而言,上述两条中的第二条比较常见。 +「取大原则」,按定义,是一个兜底原则。它保证了最终只有一个候选方案能够胜出。一般而言,上述两条中的第三条比较常见。 {: .example } >比如: @@ -426,7 +428,7 @@ nav_order: 12 >- 结构合理都不适用。 >- 我们最后检查「字根取大」规则。我们发现,两个字根是穿插书写的。根据规则,只要其中某个字根多写一笔仍然符合笔顺,就多写一笔。「十彐女」可以保证合笔顺的字根写了三笔。而「龶乛女」中,符合笔顺的字根只有一笔。因此「十彐女」胜出。 -第三条只有一种场合会出现,那就是部分字根有竖变撇的附属根。如:「千」字根的「丨」变成「丿」,「干」字根的「丨」变成「丿」,称为「歪斜根」。 +第二条只有一种场合会出现,那就是部分字根有竖变撇的附属根。如:「千」字根的「丨」变成「丿」,「干」字根的「丨」变成「丿」,称为「歪斜根」。 {: .example } >比如: @@ -435,6 +437,132 @@ nav_order: 12 >- 「井」拆「二{介下}」而不拆「キ丨」,因为因为「キ」的一竖是撇。故而对{介下}取大。 >- 「缓」拆「纟爪干又」而不拆「纟爪二夂」。虽然前者「干」的竖是撇,但后者出现了「字根相交」。根据规则优先级,「字根相交」低于「字根相连」,故而拆为「纟爪干又」。 +### 机器实现 + + +这里释出宇浩拆分规则的伪代码,用以展示使用计算机程序来实现拆分筛选的算法逻辑。注意,宇浩的拆分规则是**淘汰性**而非**引导性**的,故而算法的前置条件是已经拥有了若干的候选拆分,在此基础上,程序可以筛选出最满足宇浩拆分规则的唯一拆分。 + +假设某个汉字存在`N`个候选拆分,记为`divs = {div_i | i in [1, 2, ..., N]}`。每个拆分都是字根的**有序**集合,记为`div = {root_i | i in [1, 2, ..., M]}`,有以下属性: + +- div.len: int 字根个数 +- div.bishun: Booleann 字根是否完全合笔顺 +- div.san: Boolean 字根是否分散 +- div.lian: Boolean 字根是否相连 +- div.jiao: Boolean 字根是否相交 +- div.duan: Boolean 字根是否断开 + +注意:div.san, div.lian, div.jiao, div,duan 中有且只有一个是 True。 + +以下伪代码会从`N`个候选拆分`divs = {div_i | i in [1, 2, ..., N]}`中选出最优拆分: + +```python +# 字根最少 +for div_i in divs: + # 移除字根数量不是最少的候选拆分 + if div_i.len != min([i.len for i in divs]): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 符合笔顺 +for div_i in divs: + # 存在完全符合笔顺的拆分时,移除不完全符合笔顺的候选拆分 + if div_i.bishun != max([i.bishun for i in divs]): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 结构完整 +root_completeness_score = { + 囗: 2, + 日: 2, + 目: 2, + 田: 2, + 冂: 1, + 勹: 1, + 匚: 1, + 凵: 1, + コ: 1, + 其他: 0, +} # 包围结构得分更高 +for div_i, div_j in permutation(divs, 2): # 俩俩取候选拆分比较 + common = intersect(div_i, div_j) # 共同的拆分部分 + unique_i = div_i.difference(common) # 非共同的拆分部分 + unique_j = div_j.difference(common) # 非共同的拆分部分 + # 非共同字根中有包围结构的保留 + if unique_i.map(root_completeness_score) < unique_j.map(root_completeness_score): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根分散 +for div_i in divs: + # 存在散的拆分时,移除不散的候选拆分 + if div_i.san != max([i.san for i in divs]): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根相连 +for div_i in divs: + # 存在连的拆分时,移除不连的候选拆分 + if div_i.lian != max([i.lian for i in divs]): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根相交 +for div_i in divs: + # 存在交的拆分时,移除不交的候选拆分 + if div_i.jiao != max([i.jiao for i in divs]): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根断开 +# 断属性为散连交的补集,故不用再进行判断 + +# 字根取大之一 +# 让完全符合笔顺的部件尽可能地大 +for div_i, div_j in permutation(divs, 2): # 俩俩取候选拆分比较 + common = intersect(div_i, div_j) # 共同的拆分部分 + unique_i = div_i.difference(common) # 非共同的拆分部分 + unique_j = div_j.difference(common) # 非共同的拆分部分 + # root_bishun_score(div) 是局部拆分中完全符合笔顺的字根数量 + # 非共同字根中,完全符合笔顺的字根数量多的保留 + if root_bishun_score(unique_i) < root_bishun_score(unique_j): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根取大之二 +# 让非歪斜根尽可能地大 +for div_i, div_j in permutation(divs, 2): # 俩俩取候选拆分比较 + common = intersect(div_i, div_j) # 共同的拆分部分 + unique_i = div_i.difference(common) # 非共同的拆分部分 + unique_j = div_j.difference(common) # 非共同的拆分部分 + # leaning_root_strokes(div) 是局部拆分中歪斜根的笔画数 + # 非共同字根中,非歪斜根笔画多的保留 + if leaning_root_strokes(unique_i) < leaning_root_strokes(unique_j): + divs.remove(div_i) + if len(divs) == 1: + break # 剩下唯一拆分候选,则停止判断 + +# 字根取大之三 +# 让首笔靠前的字根尽可能地大 +for div_i, div_j in permutation(divs, 2): # 俩俩取候选拆分比较 + common = intersect(div_i, div_j) # 共同的拆分部分 + unique_i = div_i.difference(common) # 非共同的拆分部分 + unique_j = div_j.difference(common) # 非共同的拆分部分 + # root_i.strokes.len 是字根的笔画数 + # 非共同字根中,靠前的字根笔画多的保留 + for root_i, root_j in zip(unique_i, unique_j): # 俩俩取字根比较 + if root_i.strokes.len < root_j.strokes.len: + divs.remove(div_i) + +# 到此,必然只剩下唯一的拆分候选,即为最终拆分 +``` + ### 拆字举例