从按定义递归定位、生成整张表,到沿编号逐步翻转,最后化简成公式 k xor (k >> 1),逐层拿满 50、80、95、100 分。
OJ: luogu
题目 ID: P5657
难度:普及-
标签:位运算构造递推
创建: 2026-05-31 16:38
更新: 2026-10-07 10:45
目录
形式化题目
给定
位格雷码是 0,1;位格雷码的前 个串,是把 位格雷码按原顺序排列后统一补前缀 0;- 后
个串,是把 位格雷码按逆序排列后统一补前缀 1。
编号从
解法路线
题面给出了四个真实的分数档位。它们的区别不在
| 档位 | 真实约束 | 最省事的做法 | 瓶颈 |
|---|---|---|---|
| 按定义逐层生成整张格雷码表 | 表里有 |
||
| 从 |
|||
| 套用反射格雷码公式 |
long long 上界 |
||
| 同上,改用无符号 |
无 |
这些档位不是「复杂度从坏到好」的单调序列,而是「约束越松,能用的实现越笨」。四份分档代码都能独立提交、各自拿到对应分数,正式主解是最后一档的 main.cpp。
在按档位写实现之前,先给出一个最直译定义的递归解法。它不参与分档,作用是当作后面公式法的对照与对拍基准。
暴力解法:按定义递归定位
适用范围
这是最贴近题面文字的写法:不生成整张表,只顺着定义一路定位到第
称它为「暴力」是指「逐字翻译定义、不做任何化简」,不是指复杂度差:因为题面定义本身就是线性递归,所以这份基准的时间也是
思路
要求
- 前半段(
):这一位填 0,剩下的部分仍然去位格雷码里按原顺序找编号 ; - 后半段:这一位填
1,剩下的部分要去位格雷码的逆序里找。原顺序的第 个,在逆序里对应编号
一路递归到
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:28
* update_at: 2026-10-01 20:28
*/
// brute.cpp:小数据/教学版,按格雷码的递归定义直接定位第 k 个串,用来辅助对拍。
// 每一层只做一次选择:判断 k 落在前半段还是后半段,再决定这一位填 0 还是 1。
// 递归深度只有 n 层,所以 n = 64 也能跑;它只是写法朴素,不代表复杂度高。
#include <bits/stdc++.h>
using namespace std;
// 与 main.cpp 保持一致的输入范围:k 可能达到 2^64-1,必须用无符号 64 位。
typedef unsigned long long ull;
int n; // 编码位数
ull k; // 编号
// solve_gray(len, idx):返回 len 位格雷码中编号为 idx 的二进制串。
string solve_gray(int len, ull idx) {
if (len == 1) {
return idx == 0 ? "0" : "1";
}
ull half = 1ULL << (len - 1); // 前半段的串数,len <= 64 时最多到 2^63
if (idx < half) {
// 前半段:保持 len-1 位格雷码原顺序,前面补 0。
return "0" + solve_gray(len - 1, idx);
}
// 后半段:使用 len-1 位格雷码的逆序,前面补 1。
ull reversed_idx = (half - 1) - (idx - half);
return "1" + solve_gray(len - 1, reversed_idx);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
cout << solve_gray(n, k) << '\n';
return 0;
}复杂度与瓶颈
每层只递归一次,时间 half = 1ULL << (len - 1) 在
子任务解法:
适用范围
思路
完全按题目给出的三条规则模拟。从
- 前一半保持原顺序,每个串前面补一个
0; - 后一半逆序,每个串前面补一个
1。
推到第
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:28
* update_at: 2026-10-01 20:28
*/
// subtask_50.cpp:50% 数据 n <= 10,按题意逐层生成完整的 2^n 个格雷码串。
// 从 1 位格雷码开始,每一层把上一层的串按原顺序补前缀 0、按逆序补前缀 1,
// 一直推到 n 位,再直接输出第 k 个。只有 n 很小、2^n 装得下时才可行。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n; // 编码位数,本文件只处理 n <= 10
ll k; // 编号,50% 数据里 k < 2^10
vector<string> gray; // gray[i] = 当前这一层的第 i 个格雷码串
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
// 第 1 层:只有 "0" 和 "1"。
gray.push_back("0");
gray.push_back("1");
// 每推一层,串的数量翻倍:前半段保持原顺序补 0,后半段逆序补 1。
for (int len = 2; len <= n; len++) {
int cnt = gray.size(); // 上一层共有 2^(len-1) 个串
vector<string> cur;
for (int i = 0; i < cnt; i++) {
cur.push_back("0" + gray[i]); // 前半段:原顺序补 0
}
for (int i = cnt - 1; i >= 0; i--) {
cur.push_back("1" + gray[i]); // 后半段:逆序补 1
}
gray = cur;
}
cout << gray[k] << '\n';
return 0;
}复杂度与瓶颈
第
子任务解法:
适用范围
思路
观察格雷码相邻两个编号的关系。把
| 编号 |
格雷码 | 相对上一个编号翻转的位 |
|---|---|---|
| 0 | 000 |
— |
| 1 | 001 |
第 |
| 2 | 011 |
第 |
| 3 | 010 |
第 |
| 4 | 110 |
第 |
| 5 | 111 |
第 |
| 6 | 101 |
第 |
| 7 | 100 |
第 |
第 lowbit(k) = k & (-k)。编号 0 的格雷码是全 0,所以从编号 1 一路异或到编号
这一步把「生成所有串」换成了「在编号上走
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:28
* update_at: 2026-10-01 20:28
*/
// subtask_80.cpp:80% 数据 k <= 5*10^6,可以从 0 号串一步步推到 k 号串。
// 格雷码的相邻两个编号恰好只差一位:编号 i 的串由编号 i-1 的串翻转
// i 的最低位 1 对应的那一位得到。走 k 步即可,时间 O(k),与 n 无关。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n; // 编码位数
ll k; // 编号,本文件只处理 k <= 5*10^6
ll gray; // 当前编号对应的格雷码数值
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
// 编号 0 的格雷码是全 0。
gray = 0;
// 从编号 1 推到编号 k:编号 i 相对 i-1 恰好翻转 lowbit(i) 这一位。
for (ll i = 1; i <= k; i++) {
ll low = i & (-i); // 取出 i 的最低非零二进制位
gray ^= low;
}
// 从最高位到最低位输出,恰好 n 位,前导零也要输出。
for (int bit = n - 1; bit >= 0; bit--) {
cout << ((gray >> bit) & 1LL);
}
cout << '\n';
return 0;
}复杂度与瓶颈
时间 long long。
子任务解法:
适用范围
long long 的表示范围内。
思路
上面那个循环其实可以一步算完。把编号
k = b[n-1] b[n-2] ... b[1] b[0]
k >> 1 = 0 b[n-1] ... b[2] b[1]两者异或,结果的第
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:28
* update_at: 2026-10-01 20:28
*/
// subtask_95.cpp:95% 数据 k <= 2^63-1,正好装进有符号 long long。
// 直接套二进制反射格雷码的公式 gray = k ^ (k >> 1),时间 O(n)。
// 注意 k 最大是 2^63-1,它的最高位一定是 0,所以 gray 非负、有符号右移也安全;
// 再大一点(n = 64 时 k 可以到 2^64-1)就装不下了,需要 main.cpp 的无符号写法。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n; // 编码位数
ll k; // 编号,0 <= k <= 2^63-1
ll gray; // 编号 k 对应的格雷码数值
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
// 编号与右移一位的编号异或,得到的每一位表示「相邻两位是否不同」。
gray = k ^ (k >> 1);
// 从最高位到最低位输出,恰好 n 位,前导零也要输出。
for (int bit = n - 1; bit >= 0; bit--) {
cout << ((gray >> bit) & 1LL);
}
cout << '\n';
return 0;
}复杂度与瓶颈
时间降到 long long 刚好装得下;但
正解
关键观察
公式
long long 的最大值 unsigned long long。
思路
- 读入
和 ; - 计算
; - 从第
位输出到第 位,恰好输出 位。
第 3 步只做右移,不做左移,因此不会写出 1ULL << 64 这种未定义行为——这是本题最容易踩的边界。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:28
* update_at: 2026-10-01 20:28
*/
// main.cpp:满分做法,直接用二进制反射格雷码公式 gray = k ^ (k >> 1)。
// 时间 O(n),空间 O(1),是本题正式提交版本。
#include <bits/stdc++.h>
using namespace std;
// 本题 k 的上界是 2^64-1,超出有符号 long long 的范围,
// 所以编号和答案都用无符号 64 位整数,靠声明类型解决,不做任何强制转换。
typedef unsigned long long ull;
int n; // 编码位数,1 <= n <= 64
ull k; // 编号,0 <= k < 2^n
ull gray; // 编号 k 对应的格雷码数值
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
// 每一位表示「编号的相邻两位是否不同」,这正是格雷码的定义。
gray = k ^ (k >> 1);
// 从第 n-1 位输出到第 0 位,恰好 n 位,前导零也要输出。
// n = 64 时最大只右移到第 63 位,不会出现 1ULL << 64 这种非法左移。
for (int bit = n - 1; bit >= 0; bit--) {
cout << ((gray >> bit) & 1ULL);
}
cout << '\n';
return 0;
}复杂度
- 时间复杂度:
,只输出 位。 - 空间复杂度:
,只用常数个整数变量。
总结
整篇文章沿着「不断消掉存储与时间瓶颈」推进:
档干脆把整张表造出来,瓶颈是 个串; 档改成在编号上一步步翻转,瓶颈变成 次迭代; 档用 把迭代压成一步,瓶颈变成 的存储类型; 档把 和答案换成无符号 位整数,补齐最后 。
定义直译的递归定位不在这条链条上:它虽然也能过全部数据,但逆序下标和
真正需要记住的只有一条公式和一条边界:公式是 1ULL << 64,只输出已有整数的各个二进制位。
本文的 main.cpp 已用递归定义版 brute.cpp 进行 400 组随机对拍,覆盖