F. Goodbye, Banker Life - 题解
比赛与标签
比赛: Codeforces Round 1006 (Div. 3) 标签: 2-sat, bitmasks, combinatorics, constructive algorithms, fft, math, number theory 难度: *1700
题目大意喵~
主人,我们的任务是帮助 Akito 构建一个魔法护盾!这个护盾需要一个特殊的咒语,也就是一个“伟大魔法三角形”的第 n 行。
这个三角形是这样生成的:
- 第一行只有一个数字,就是
k。 - 第
i行有i个数字。 - 我们把第
i行第j个数记作T[i][j]。它的生成规则是:T[i][1] = T[i-1][1](最左边的数直接继承)T[i][i] = T[i-1][i-1](最右边的数也直接继承)T[i][j] = T[i-1][j-1] ⊕ T[i-1][j](中间的数是它正上方两个数的异或和,⊕就是XOR啦)
我们需要根据给定的 n 和 k,计算出这个三角形第 n 行的所有数字。
解题思路大揭秘!ฅ'ω'ฅ
刚看到这个规则,是不是感觉和杨辉三角(Pascal's Triangle)很像呀?杨辉三角是 C(i,j) = C(i-1,j-1) + C(i-1,j),而我们这里是把加法换成了异或(XOR)运算。这可是一个非常重要的线索哦!
在杨辉三角中,第 i 行第 j 个数(从0开始计数)是组合数 C(i, j)。由于我们的规则和它如此相似,我们可以大胆猜测,这个魔法三角形的元素也和组合数有关。
因为所有数字都是从第一行的 k 通过异或运算得到的,所以第 n 行的每个数,要么是 k,要么是 0。它到底是什么,取决于 k 在这个计算过程中被异或了奇数次还是偶数次。
- 如果
k被异或了奇数次,结果就是k。 - 如果
k被异或了偶数次,结果就是0(因为k ⊕ k = 0)。
k 被异或的次数,正好对应着杨辉三角中第 n-1 行的组合数 C(n-1, j-1)(注意行列号从1开始,组合数从0开始)。所以,问题就变成了: T[n][j] 的值是 k 当且仅当 C(n-1, j-1) 是奇数;否则是 0。
那么,我们怎么判断 C(n, k) 的奇偶性呢?这里就要请出我们的好朋友——**卢卡斯定理(Lucas's Theorem)**的一个重要推论了:
C(n, k)是奇数,当且仅当(k & n) == k。 这里的&是按位与运算。这个式子意味着,在二进制表示下,k的所有为1的位,在n的对应位上也必须是1。
所以,我们的任务就转化为了:生成一个长度为 n 的序列,其中第 j 个数(1 <= j <= n)根据 C(n-1, j-1) 的奇偶性来决定是 k 还是 0。
直接计算每个组合数太慢啦,我们得找个更快的办法。这个 C(n, k) mod 2 形成的三角形(也叫谢尔宾斯基三角形)具有非常漂亮的分形结构!我们可以利用这个结构进行递归构造。
设 S(m) 是 C(m, 0), C(m, 1), ..., C(m, m) 模2后的序列。
基础情况:
S(0) = [1]S(1) = [1, 1]
递归构造: 对于任意的
m,我们找到不大于m的最大2的幂次方,记作p = 2^w。令m = p + y。 神奇的事情发生了!S(m)可以由更小的S(y)构造出来!S(m)的结构是:[S(y)]后面跟着一堆0,然后再跟着一个[S(y)]。S(m) = [S(y)] [一串0] [S(y)]中间这串0有多少个呢?S(m)总长m+1,S(y)长y+1。所以0的个数是(m+1) - 2 * (y+1) = m - 2y - 1 = (p+y) - 2y - 1 = p - y - 1个。特殊情况: 如果
m本身就是2的幂次方,即m = p = 2^w,那么y=0。S(0) = [1]。 此时S(m) = [S(0)] [p - 0 - 1 个 0] [S(0)],也就是[1, 0, 0, ..., 0, 1],两端是1,中间m-1个全是0。
这不就是代码里的递归逻辑嘛!我们只需要写一个函数 work(m) 来生成 S(m) 对应的 k 和 0 序列,然后用 n-1 来调用它就可以啦!
代码实现喵!
#include <bits/stdc++.h>
using namespace std;
int main() {
// 喵~ 加速输入输出,让程序跑得飞快!
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, k;
cin >> n >> k;
// 使用 ostringstream 来拼接字符串,比多次 cout 快很多哦!
ostringstream oss;
// 递归函数 work(x) 用来生成第 x 行的谢尔宾斯基三角形序列
// x 对应我们分析中的 m = n-1
function<void(int)> work = [&](int x) {
if (x == 0) {
// 基础情况: S(0) = [1],对应输出 k
oss << k << " ";
} else if (x == 1) {
// 基础情况: S(1) = [1, 1],对应输出 k k
oss << k << " " << k << " ";
} else {
// 找到不大于 x 的最大2的幂 p = 2^w
// __builtin_clz(x) 计算前导零的数量,31 - clz(x) 就能得到最高位的索引 w
int w = 31 - __builtin_clz(x);
int power_of_two = 1 << w;
if (x == power_of_two) {
// 特殊情况:x 是 2 的幂次方,S(x) = [1, 0, ..., 0, 1]
oss << k << " "; // 第一个 k
for (int i = 0; i < x - 1; i++) {
oss << 0 << " "; // 中间的 x-1 个 0
}
oss << k << " "; // 最后一个 k
} else {
// 递归构造:x = p + y
int y = x - power_of_two;
// 先构造 S(y)
work(y);
// 中间填充 p - y - 1 个 0
for (int i = 0; i < power_of_two - y - 1; i++) {
oss << 0 << " ";
}
// 再次构造 S(y)
work(y);
}
}
};
// 我们需要的是第 n 行,对应组合数 C(n-1, j-1),所以调用 work(n-1)
work(n - 1);
// 将 ostringstream 中的内容转为 string
string s = oss.str();
// 去掉末尾多余的空格
if (!s.empty()) {
s.pop_back();
}
cout << s << '\n';
}
return 0;
}复杂度分析的说
时间复杂度: O(Σn) 的说 整个算法的核心是递归函数
work。虽然它在递归,但它本质上是在构造一个长度为n的输出序列。每个数字(k或0)都只被访问和生成一次。因为题目保证了所有测试用例的n的总和不超过10^6,所以总的时间复杂度就是O(sum of n)。非常高效呐!空间复杂度: O(max(n)) 的说 空间主要消耗在两个地方:递归调用栈和
ostringstream。递归深度最多是O(log n),很小。而ostringstream需要存储一整个测试用例的输出,其大小与该测试用例的n成正比。因此,空间复杂度由单个测试用例中最大的n决定,即O(max(n))。
知识点与总结喵~
这道题真是太有趣啦,它把组合数学和分形几何巧妙地结合在了一起!
- 核心思想: 将问题从一个自定义的异或三角转化为了经典的**杨辉三角模2(谢尔宾斯基三角形)**问题。这是解题的关键一步!
- 数学原理: 卢卡斯定理的推论
C(n, k) % 2 == 1 <=> (k & n) == k是背后坚实的数学支撑,它揭示了组合数奇偶性与二进制位之间的关系。 - 算法技巧: 我们没有暴力计算,而是利用了谢尔宾斯基三角形的分形/递归性质,实现了分治求解。这种思想在处理与位运算和2的幂相关的序列问题时非常有用。
- 编程实践:
__builtin_clz()是一个GCC/Clang内建函数,可以高效地计算前导零数量,是快速找到最高有效位的利器。- 对于有大量输出的题目,使用
ostringstream或者手动拼接字符串再统一输出,是避免因I/O耗时过长而超时的常用优化手段。
希望这次的讲解能帮助到你哦!只要我们善于观察,找到问题的本质,再复杂的魔法也能被我们破解的说!下次再一起解题吧,喵~ (ฅ´ω`ฅ)