前言
这道题对应 LeetCode 1374「生成每种字符都是奇数个的字符串」:给定整数 n,返回一个长度为 n 的字符串,要求字符串中每一种字符出现的次数都是奇数。字符限定为小写英文字母,答案不唯一。
题目短,但它的考点是奇偶性推理,而不是字符串操作。常见误解是"必须把所有 26 个字母都用上"或者"要搜索一个满足条件的组合"。实际上只需要一两个字符就能构造出来,关键是想清楚:若干个奇数相加,和的奇偶性由奇数个数决定。
本文先讲奇偶性原理,再给出常数时间的构造,最后讨论边界输入和这类构造题的通用思路。
一、奇偶性原理
设字符串中不同字符有 k 种,第 i 种出现 c_i 次,每个 c_i 都是奇数。总长度 n = c_1 + c_2 + ... + c_k。
奇数个奇数相加得奇数,偶数个奇数相加得偶数。也就是说:
- 若 k 是奇数,则 n 必为奇数;
- 若 k 是偶数,则 n 必为偶数。
反过来构造:
- n 为奇数时,取 k = 1,让唯一一种字符出现 n 次。n 是奇数,满足要求。
- n 为偶数时,取 k = 2,让两种字符分别出现 n-1 次和 1 次。n-1 是奇数,1 也是奇数,两个奇数之和为偶数 n,满足要求。
这两种构造都只需要常数时间,且不需要任何搜索。
二、构造方案
n 为奇数:返回 n 个'a'。
n 为偶数:返回 n-1 个'a'再加一个'b'。
// 适用于 C++17
#include <cstddef>
#include <string>
std::string generateTheString(int n)
{
if (n <= 0) {
return std::string(); // 题目约束 n >= 1,这里做防御
}
if (n % 2 == 1) {
return std::string(static_cast<std::size_t>(n), 'a');
}
return std::string(static_cast<std::size_t>(n - 1), 'a') + 'b';
}std::string(count, ch)是标准构造函数:构造一个由 count 个 ch 组成的字符串。count的类型是size_type,所以这里显式转换,避免int到无符号的隐式转换警告。
n 为偶数且 n = 2 时,n - 1 = 1,结果就是"ab":'a'出现 1 次,'b'出现 1 次,都是奇数,长度 2,正确。
n 为奇数且 n = 1 时,结果是"a":一种字符出现 1 次,正确。
三、正确性说明
分两种情况验证:
| n 的奇偶 | 构造 | 各字符计数 | 计数是否全为奇数 | 总长度 |
|---|
| 奇数 | n 个'a' | 'a': n | n 是奇数,成立 | n |
| 偶数 | n-1 个'a'+ 1 个'b' | 'a': n-1,'b': 1 | n-1 是奇数,1 是奇数 | n |
两种情况下字符串长度都恰好是 n,且每种出现的字符计数都是奇数,所以构造满足题意。
题目不要求字符种类最少,也不要求字典序,所以这个方案在正确性上已经足够。它的时间复杂度和空间复杂度都是 O(n),因为必须写出长度为 n 的字符串,这是无法避免的下界。
四、另一种写法与取舍
也可以统一成"先放 n-1 个'a',再看奇偶补一个字符":
// 适用于 C++17
#include <cstddef>
#include <string>
std::string generateTheString2(int n)
{
if (n <= 0) {
return std::string();
}
std::string result(static_cast<std::size_t>(n - 1), 'a');
result.push_back((n % 2 == 1) ? 'a' : 'b');
return result;
}当 n 为奇数时,n-1是偶数个'a',再补一个'a',总共 n 个'a';当 n 为偶数时,补'b',得到 n-1 个'a'加一个'b'。两种写法等价。
第二种写法只构造一次字符串,少了一次临时对象和拼接,语义上也更统一。第一种写法更直观,适合讲解。实际提交时两者都能通过,差别只在常数因子,不该据此断言谁"更快多少"。
五、边界与约束
题目给出的约束通常是 1 <= n <= 500。在这个范围内,std::string的构造不会有任何问题。
如果调用方传入 0 或负数,第一版会返回空串,第二版同样。严格说,空串没有任何字符,也就没有"每种字符都是奇数个"的问题,但题目保证 n >= 1,所以这只是防御性处理。
如果 n 很大(比如接近int上限),static_cast<std::size_t>(n)本身没问题,但构造这么长的字符串会抛出std::bad_alloc或std::length_error,调用方需要有异常处理。这类构造题通常不会给到这种规模。
还要注意,本题是"答案不唯一"的构造题,判题器通常用特殊校验而不是逐字符比对。这意味着"aaa"和"abb"之类的不同答案都可能被接受,没必要去猜标准答案长什么样。写代码时把精力放在满足约束上,而不是复现某个"官方解"。
六、变体:要求每种字符出现偶数次
把条件反过来,要求每种字符的出现次数都是偶数,结论会变得不一样。若干个偶数相加仍是偶数,所以总长度 n 必须是偶数;n 为奇数时无解。
n 为偶数时,最简单的构造是 n 个'a':只有一种字符,计数 n 是偶数,满足条件。也可以拆成两种字符各若干偶数次,但没必要。
这个对比说明:看到"每种字符计数满足某奇偶性",先算总和的奇偶性,能立刻判断有没有解,而不是急着写循环。
七、写一个自检函数
构造类题目容易在边界上写错,交卷前可以用一个简单的计数器自检:统计每个字母的出现次数,确认出现过的字符计数都是奇数,同时确认总长度正确。
// 适用于 C++17
#include <string>
bool isValid(const std::string &s, int expectedLength)
{
if (static_cast<int>(s.size()) != expectedLength) {
return false;
}
int freq[26] = {};
for (char c : s) {
if (c < 'a' || c > 'z') {
return false; // 题目限定小写字母
}
++freq[c - 'a'];
}
for (int i = 0; i < 26; ++i) {
if (freq[i] != 0 && freq[i] % 2 == 0) {
return false; // 出现过但不是奇数个
}
}
return true;
}判断条件里必须带freq[i] != 0。没有出现的字母计数是 0,而 0 是偶数,如果直接判断freq[i] % 2 == 0,任何字符串都会被判为不合法。
这个函数的时间是 O(n + 26),空间是常数,适合在本地对拍时使用。它不参与提交,只是帮助确认构造结果。
常见坑点
- ❌ 试图用全部 26 个字母,给每个字母分配奇数个。
✅ 一两个字符就够;种类多反而增加出错面。
- ❌ n 为偶数时返回 n 个
'a',计数是偶数,不满足。
✅ 偶数时改成 n-1 个'a'加一个'b'。
- ❌
std::string(n, 'a')中 n 为负数。
✅ 先判断n <= 0;负数转成无符号会变成极大值,导致异常或崩溃。
- ❌ 用
'a' + std::string(...)之外的顺序拼接,写出std::string + char之外的非法表达式。
✅std::string支持operator+接char;也可以先构造再push_back。
- ❌ 认为"每种字符"必须包含所有小写字母。
✅ 只要求出现过的字符计数为奇数,未出现的字符不计。
- ❌ 忘记
#include <string>,只靠其他头文件间接引入。
✅ 显式包含所用组件的头文件。
- ❌ 用
int直接传给std::string的构造参数,产生有符号/无符号转换警告。
✅ 显式static_cast<std::size_t>,并先排除负数。
- ❌ 把构造结果的长度算错,比如偶数时写 n-2 个
'a'加'b'。
✅ 校验count('a') + count('b') == n,两种计数都要是奇数。
总结
| n 的奇偶 | 返回 | 'a'计数 | 其他计数 |
|---|
| 奇数 | n 个'a' | n(奇) | 无 |
| 偶数 | n-1 个'a'+'b' | n-1(奇) | 'b': 1(奇) |
这道题的价值在于"用奇偶性直接构造",而不是枚举。看到"每种字符出现奇数/偶数次"这类条件,先想总和的奇偶性约束,往往能一步得到答案。代码只有几行,真正要小心的是 n 为偶数时的长度和负数输入。