题目:
A Brave New Wordle难度:Codewars 5 kyu
语言:C
分析日期:2026-10-14
题目页:Training on A Brave New Wordle | Codewars
1. 题目要求与问题核心
实现:
void wordle( char hints[5 + 1], const char guess[5 + 1], const char solution[5 + 1] );guess和solution都是长度为 5 的单词。函数需要向hints写入 5 个提示字符,并以'\0'结束:
提示 | 含义 |
|---|---|
| 当前字母与答案中同位置字母一致(green) |
| 当前字母存在于答案中,但位置不对(yellow) |
| 答案中没有可再匹配的该字母(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) | 额外空间 | 优点 | 适用情况 |
|---|---|---|---|---|
位置 |
|
| 最接近题意,容易验证,不依赖字符集大小 | 本题固定 5 个字符,首选 |
频次数组 |
|
| 对较长单词更快,重复字母配额表达清晰 | 单字节字符、长度可变的版本 |
对这道固定长度为 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. 建议测试集
|
| 期望 | 覆盖点 |
|---|---|---|---|
|
|
| 全部绿色 |
|
|
| 绿色优先,额外重复字母为黑色 |
|
|
| 仅一个黄色,其余重复字母为黑色 |
|
|
| 重复字母超出答案库存 |
|
|
| 绿色与黄色混合,重复字母 |
|
|
| 黄色配额仅能使用一次 |
|
|
| 无匹配,并验证所有输出位置均已初始化 |
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