Codewars: A Brave New Wordle(C)解题分析

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)

真正的难点不是普通的逐字符比较,而是重复字母的配额管理:

  1. 所有同位置匹配必须优先标成 g。

  2. 一个字母最多只能被标记 g 或 y 的次数,不能超过它在 solution 中的出现次数。

  3. 对非绿色字母判定黄色时,必须从左到右处理。

例如:

复制代码
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. 对你的算法的评价

做对的部分

你的主体思路正是本题需要的两阶段消耗算法:

  1. 复制 solution 到工作数组 solution1。

  2. 第一轮标记所有绿色位置,同时把对应答案位置写成 '1',表示已消耗。

  3. 第二轮从左到右扫描非绿色 guess[i],在 solution1 中寻找第一个同字符且未消耗的位置;找到后标为黄色并消耗该位置。

  4. 没有得到绿色或黄色的位置最后写成黑色。

在 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' 不是输入字符这一隐含前提。

来源

相关推荐
小草cys1 小时前
用 GPT 直接生成可编辑 PPTX 的实操记录,artifact-tool是OpenAICodex环境专用
gpt·算法·pptx
Escalating_xu1 小时前
【数据结构】顺序表的应用:动态通讯录项目与双指针经典算法
c语言·数据结构·算法
其实防守也摸鱼1 小时前
CTF Web 6 题全解 Writeup
linux·sql·学习·算法·安全·ctf·xss
宇明一不急1 小时前
string和byte的相互转换
算法
波力海苔夹心脆6751 小时前
VisionPro 与 VisionMaster 对比(上):软件定位、开发环境与核心视觉工具对照
图像处理·算法·计算机视觉·c#·视觉检测·个人开发·vp9
动词ing2 小时前
【题目练习】盛最多水的容器
算法·目标检测
海绵宝宝转agent2 小时前
2026-10-14算法+面经总结
java·笔记·算法·缓存
虚无的纽扣2 小时前
【力扣刷题】第四天:三数之和、四数之和
算法·leetcode·职场和发展
ZiLing2 小时前
2026 ROS 2 Lyrical 工程实例(一):从零搭建 AMR 激光 SLAM——Gazebo、slam_toolbox 与 Nav2 全链路跑通
算法