-
题目:
A Brave New Wordle -
难度:Codewars 5 kyu
-
语言:C
-
分析日期:2026-10-14
1. 题目要求与问题核心
实现:
void wordle(
char hints[5 + 1],
const char guess[5 + 1],
const char solution[5 + 1]
);
guess 和 solution 都是长度为 5 的单词。函数需要向 hints 写入 5 个提示字符,并以 '\0' 结束:
| 提示 | 含义 |
|---|---|
g |
当前字母与答案中同位置字母一致(green) |
y |
当前字母存在于答案中,但位置不对(yellow) |
b |
答案中没有可再匹配的该字母(black) |
真正的难点不是普通的逐字符比较,而是重复字母的配额管理:
-
所有同位置匹配必须优先标成
g。 -
一个字母最多只能被标记
g或y的次数,不能超过它在solution中的出现次数。 -
对非绿色字母判定黄色时,必须从左到右处理。
例如:
guess: xxxxx
solution: yxxxx
hints: bgggg
后四个 x 已经消耗了答案中的四个 x,第一个 x 没有剩余配额,因此是 b,不是 y。
2. 正确的抽象:两阶段"消耗库存"
可以把 solution 视作每种字母的有限库存。每次给出 g 或 y,都必须恰好消耗答案中的一个尚未使用的位置或一个剩余计数。
阶段 1:逐位置找 green
guess: a a x x x
solution: x a x x a
. g g g .
阶段 2:只处理非 green 的 guess,按从左到右顺序匹配尚未消耗的 solution 字母
剩余答案位置:0 -> x,4 -> a
guess[0] = a -> y,消耗 solution[4]
guess[4] = x -> y,消耗 solution[0]
结果:y g g g y
该顺序保证了两个关键性质:
-
绿色匹配永远优先,不会被黄色匹配抢占。
-
对重复字母,较左侧的非绿色猜测会优先得到黄色资格。
3. 你的实现
void wordle(char hints[5 + 1], const char guess[5 + 1], const char solution[5 + 1]) {
char solution1[6];
for (int i = 0; i < 5; i++) {
solution1[i] = solution[i];
}
for (int i = 0; i < 5; i++) {
if (guess[i] == solution1[i]) {
solution1[i] = '1';
hints[i] = 'g';
}
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if ((hints[i] != 'g') && (solution1[j] != '1')) {
if (guess[i] == solution1[j]) {
hints[i] = 'y';
solution1[j] = '1';
break;
}
}
}
}
for (int j = 0; j < 5; j++) {
if ((hints[j] != 'g') && (hints[j] != 'y')) {
hints[j] = 'b';
}
}
hints[5] = '\0';
}
4. 对你的算法的评价
做对的部分
你的主体思路正是本题需要的两阶段消耗算法:
-
复制
solution到工作数组solution1。 -
第一轮标记所有绿色位置,同时把对应答案位置写成
'1',表示已消耗。 -
第二轮从左到右扫描非绿色
guess[i],在solution1中寻找第一个同字符且未消耗的位置;找到后标为黄色并消耗该位置。 -
没有得到绿色或黄色的位置最后写成黑色。
在 hints 已正确初始化的前提下,这个策略能正确处理题目中的重复字母、绿色优先和黄色从左到右规则。
关键问题:读取了未初始化的 hints
以下代码会在非绿色位置读取尚未赋值的 hints[i]:
if ((hints[i] != 'g') && (solution1[j] != '1'))
第一轮只在绿色位置写了 hints[i] = 'g'。因此,对一个不是绿色的位置,第二轮在判断 hints[i] != 'g' 前,hints[i] 还没有被初始化。最后一轮的 hints[j] != 'g' 和 hints[j] != 'y' 也有同样问题。
这不是"默认不是 g"的安全前提。在 C 中,自动局部数组中未赋值元素的值不应被读取;程序行为不能依赖它恰好不是 g 或 y。实际运行时,它可能碰巧通过,也可能导致个别位置没有进入黄色匹配或没有被正确写成 b。
最小修复 是在第一轮先给每个提示位置写默认值 b,然后第二轮仅处理 b:
for (int i = 0; i < 5; ++i) {
hints[i] = 'b';
if (guess[i] == solution1[i]) {
solution1[i] = '1';
hints[i] = 'g';
}
}
次要问题:用 '1' 作为已消耗标记
在当前题目的"5 个字母"约束下,'1' 不会与普通字母冲突,因此通常可以工作。但它把"输入永远不含字符 '1'"变成了一个隐含依赖。若以后函数被复用到任意字符数据,真实的 '1' 会被错误当成已消耗位置。
更稳健的方式是使用独立的 used[5] 数组保存位置状态。这样不修改答案字符,也不需要保留字符来充当哨兵。
次要问题:solution1[6] 不是一个完整字符串
你只复制了下标 0 到 4,没有写入 solution1[5] = '\0'。在当前实现中,所有访问都限制在前 5 个位置,所以不会因这一点直接越界;只是 solution1 实际上被当作"长度为 5 的字符数组",不是 C 字符串。
两种清晰的写法:
char solution1[5]; // 明确它只是 5 个字符的工作缓冲区
或:
char solution1[6];
/* 复制前 5 个字符后: */
solution1[5] = '\0'; // 明确它可作为字符串使用
5. 保留你原思路的推荐实现
下面的版本仍然是"绿色优先 + 从左到右找未使用答案位置",但消除了未初始化读取和哨兵字符依赖:
#include <stdbool.h>
void wordle(
char hints[5 + 1],
const char guess[5 + 1],
const char solution[5 + 1]
) {
bool used[5] = { false };
/* 先写好默认黑色,并优先处理精确匹配。 */
for (int i = 0; i < 5; ++i) {
hints[i] = 'b';
if (guess[i] == solution[i]) {
hints[i] = 'g';
used[i] = true;
}
}
/* 仅为非绿色位置,从左到右消耗答案中剩余的匹配字母。 */
for (int i = 0; i < 5; ++i) {
if (hints[i] == 'g') {
continue;
}
for (int j = 0; j < 5; ++j) {
if (!used[j] && guess[i] == solution[j]) {
hints[i] = 'y';
used[j] = true;
break;
}
}
}
hints[5] = '\0';
}
这个版本的优点:
-
hints[0..4]在任何读取前都已初始化。 -
used[j]只表达"答案的第j个位置是否已被消耗",语义直接。 -
不改写
solution,满足const的设计意图。 -
除了 C99 标准头
<stdbool.h>外没有额外依赖。 -
与你的原算法具有相同的匹配顺序,因此保留题目需要的重复字母行为。
6. 更可扩展的计数法
当单词长度从固定的 5 推广为 L 时,上述双层扫描的时间复杂度为 O(L^2)。如果输入是单字节字符,可以把"尚未被绿色消耗的答案字符"改为频次数组:
#include <limits.h>
#include <stddef.h>
void wordle(
char hints[5 + 1],
const char guess[5 + 1],
const char solution[5 + 1]
) {
unsigned char remaining[UCHAR_MAX + 1] = { 0 };
for (size_t i = 0; i < 5; ++i) {
hints[i] = 'b';
if (guess[i] == solution[i]) {
hints[i] = 'g';
} else {
++remaining[(unsigned char)solution[i]];
}
}
for (size_t i = 0; i < 5; ++i) {
unsigned char letter;
if (hints[i] == 'g') {
continue;
}
letter = (unsigned char)guess[i];
if (remaining[letter] != 0) {
hints[i] = 'y';
--remaining[letter];
}
}
hints[5] = '\0';
}
两个版本如何取舍
| 方案 | 时间复杂度(长度为 L) | 额外空间 | 优点 | 适用情况 |
|---|---|---|---|---|
位置 used[] |
O(L^2) |
O(L) |
最接近题意,容易验证,不依赖字符集大小 | 本题固定 5 个字符,首选 |
| 频次数组 | O(L + 256) |
O(256) |
对较长单词更快,重复字母配额表达清晰 | 单字节字符、长度可变的版本 |
对这道固定长度为 5 的 kata,5 * 5 = 25 次比较是常数时间。计数法在这里不会带来可感知的性能收益,反而比 used[] 多使用 256 个计数器。因此实际提交时,推荐前面的 used[] 版本。
7. 正确性说明
设答案每个下标只能被消耗一次。
第一阶段:
-
若
guess[i] == solution[i],将hints[i]设为g并消耗答案下标i。 -
因此每个绿色提示都对应一个同位置答案字符,并且不会被后续黄色匹配使用。
第二阶段:
-
仅处理非绿色位置,按
i = 0到4的顺序进行。 -
只有找到未消耗的
solution[j] == guess[i]时,才给出y并立即消耗j。 -
所以同一个答案位置不会对应多个
y,任一字母得到的g加y数不会超过它在答案中的出现次数。 -
由于
i从左到右扫描,重复猜测字母的黄色名额会先分配给更靠左的位置。
没有匹配到绿色或黄色的位置保持初始化时写入的 b,故所有输出位置都符合题目定义。
8. 建议测试集
guess |
solution |
期望 hints |
覆盖点 |
|---|---|---|---|
crane |
crane |
ggggg |
全部绿色 |
xxxxx |
yxxxx |
bgggg |
绿色优先,额外重复字母为黑色 |
xxxxz |
aaaax |
ybbbb |
仅一个黄色,其余重复字母为黑色 |
aaaaa |
aaaab |
ggggb |
重复字母超出答案库存 |
aaxxx |
xaxxa |
ygggy |
绿色与黄色混合,重复字母 |
aabbb |
abcde |
gbybb |
黄色配额仅能使用一次 |
abcde |
fghij |
bbbbb |
无匹配,并验证所有输出位置均已初始化 |
9. 结论
你的算法方向是正确的,且对固定长度 5 的输入已经足够高效:一般形式为 O(L^2),本题实际是常数时间。最重要的改进不是性能,而是安全性:在读取前初始化 hints,并用独立的 used[] 表示"答案位置已消耗"。
推荐提交第 5 节的 used[] 版本。它和你的设计思想一致,但更符合 C 的初始化规则,语义更清晰,也不依赖 '1' 不是输入字符这一隐含前提。
来源
-
Codewars kata(题意、重复字母规则、示例):Training on A Brave New Wordle | Codewars
-
Codewars challenge API(题目元数据与完整描述):https://www.codewars.com/api/v1/code-challenges/62013b174c72240016600e60