喵哈~ 主人们好呀!今天由本猫娘来给大家讲解一道非常有趣的题目,关于如何用二进制的魔法来拆分数字,感觉就像在玩积木一样呢,喵~ ✨
这道题是 Codeforces 1095C - Powers Of Two。
题目大意
题目会给我们两个正整数 n
和 k
。我们的任务是,把 n
表示成正好 k
个2的幂次的和。
这里的“2的幂次”就是像 1, 2, 4, 8, 16, ... 这些可以写成 2^y
形式的数字(y
是非负整数)。
如果能找到这样 k
个数,我们就输出 "YES",然后把这 k
个数打印出来。如果找不到,那就只好遗憾地输出 "NO" 啦。
举个例子,喵~
- 输入
n=9, k=4
- 我们可以把
9
分解成4 + 2 + 2 + 1
。这四个数(4, 2, 2, 1)都是2的幂次,它们的和是9,数量也正好是4。所以是可行的! - 输出:
YES
和1 2 2 4
再看一个例子:
- 输入
n=3, k=7
3
最多只能分解成1 + 1 + 1
,也就是3个2的幂次。想要凑出7个是绝对不可能的啦!- 输出:
NO
解题思路与方法
这道题的核心思想其实和二进制息息相关哦,喵~ 任何一个正整数 n
,都有一个唯一的二进制表示法。
比如说 n=9
,它的二进制是 1001
。这其实就告诉我们一个秘密:9 = 1 * 2^3 + 0 * 2^2 + 0 * 2^1 + 1 * 2^0 = 8 + 1
。
看!这就是一种把 9
表示成2的幂次之和的方法,而且是用最少项数的方法!
1. 项数的限制范围
最少需要多少项 (k的下限)? 就像上面说的,直接用
n
的二进制表示法,有多少个1
,就需要多少个项。比如9
(二进制1001
) 最少需要2项 (8
和1
)。所以,k
必须大于等于n
的二进制表示中1
的个数。这个个数我们可以用一个神奇的函数__builtin_popcount(n)
来快速得到,超方便的!最多能有多少项 (k的上限)? 最“碎”的分解方法,就是把
n
分解成n
个1
相加。因为1
也是2^0
,是2的幂次,所以我们最多可以有n
个项。所以,k
必须小于等于n
。无解判断! 结合上面两点,如果题目给的
k
小于__builtin_popcount(n)
或者大于n
,那肯定是没救了,喵~ 直接输出 "NO" 就可以啦。
2. 贪心构造答案
如果 k
恰好在 [__builtin_popcount(n), n]
这个范围里,那么就一定有解!我们的策略是这样的:
从最少项开始:我们先从
n
的二进制表示开始,这是我们的基础方案。比如n=9
,我们手里有{8, 1}
。如何增加项数:我们现在的项数是
popcount(n)
,但目标是k
项。怎么在不改变总和的情况下增加项数呢?很简单!我们可以把一个大项拆成两个小项!4
可以拆成2 + 2
8
可以拆成4 + 4
2^i
(只要i>0
) 都可以拆成2^(i-1) + 2^(i-1)
每一次这样的拆分,总和不变,但项数会增加 1!
贪心策略:我们需要增加
k - popcount(n)
个项,所以我们就要进行这么多次拆分。那每次拆哪个数呢?为了让过程简单可控,我们采用贪心的策略:每次都拆分当前手里最大的那个数。- 比如,我们有
{8, 1}
,需要增加项数。我们拆8
,得到{4, 4, 1}
。项数从2变成了3。 - 如果还需要增加,我们再拆一个
4
,得到{4, 2, 2, 1}
。项数从3变成了4。 - 我们就这样一直从大到小拆分,直到项数正好等于
k
为止!因为我们已经判断过k
是可行的,所以这个过程一定能成功。
- 比如,我们有
算法步骤总结
- 计算
n
的二进制中1的个数set_bits
。 - 判断
k
是否在[set_bits, n]
范围内,如果不是,输出 "NO"。 - 如果是,输出 "YES"。然后用一个数组
counts
来记录我们手里每种2的幂次的数量。counts[i]
表示2^i
的数量。 - 先根据
n
的二进制表示,初始化counts
数组。 - 从大到小遍历
i
(比如从30到1)。只要当前总项数还不够k
,并且我们有2^i
这种大数 (counts[i] > 0
),就执行拆分:counts[i]
减一counts[i-1]
加二- 总项数加一
- 重复这个拆分过程,直到总项数达到
k
。 - 最后,遍历
counts
数组,把所有的项打印出来就大功告成啦,喵呜~ 🐾
C++ 题解代码
这是可以直接提交的C++代码,本猫娘在上面加了一些中文注释,方便主人们理解每一部分的作用。
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <cmath>
// __builtin_popcount 是 GCC/Clang 编译器的一个内建函数,效率很高
// 在 C++20 中,它被标准化为 <bit> 头文件里的 std::popcount
// 在算法竞赛中用这个非常普遍,喵~
int main() {
// 加速输入输出,让程序跑得快一点
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, k;
std::cin >> n >> k;
// 计算 n 的二进制表示中 1 的个数,这是 k 的下限
int set_bits = __builtin_popcount(static_cast<unsigned int>(n));
// 无解判断:k 如果小于最少项数,或者大于最多项数(n),则无解
if (k < set_bits || k > n) {
std::cout << "NO\n";
return 0;
}
// 只要 k 在范围内,就一定有解
std::cout << "YES\n";
// 使用一个频率数组来存储每种2的幂次(2^i)的数量
// counts[i] 存储 2^i 的个数
std::vector<int> counts(31, 0);
// 通过位运算,初始化 counts 数组,得到 n 的标准二进制表示
for (int i = 0; i < 31; ++i) {
if ((n >> i) & 1) {
counts[i]++;
}
}
int current_k = set_bits; // 当前的项数
// 贪心地从最大的幂次开始拆分,直到项数达到 k
// 每次拆分一个 2^i 会变成两个 2^(i-1),总项数增加 1
for (int i = 30; i > 0; --i) {
// 如果项数已经够了,就不用再拆了
if (current_k == k) {
break;
}
// 如果我们有 2^i 这种项可以拆
if (counts[i] > 0) {
// 我们需要 k - current_k 个新项
// 当前的 2^i 最多有 counts[i] 个可供我们拆
// 所以我们实际能拆的数量是这两者中的较小值
int can_expand = std::min(counts[i], k - current_k);
if (can_expand > 0) {
counts[i] -= can_expand; // 减少 can_expand 个 2^i
counts[i-1] += 2 * can_expand; // 增加 2*can_expand 个 2^(i-1)
current_k += can_expand; // 总项数增加了 can_expand
}
}
}
// 循环输出最终结果
bool first = true;
for (int i = 0; i < 31; ++i) {
int power_of_two = 1 << i; // 2^i
for (int j = 0; j < counts[i]; ++j) {
if (!first) {
std::cout << " ";
}
std::cout << power_of_two;
first = false;
}
}
std::cout << "\n";
return 0;
}
知识点小课堂
这道题涉及了几个非常基础但重要的计算机科学知识点,学好它们对解决很多问题都有帮助哦!
二进制表示 (Binary Representation) 任何正整数
n
都可以唯一地表示为n = c_m * 2^m + ... + c_1 * 2^1 + c_0 * 2^0
,其中c_i
是 0 或 1。这个性质是解决这个问题的基石,它给了我们一个初始的、项数最少的分解方案。位运算 (Bitwise Operations) 位运算是直接在二进制位上进行操作,速度飞快!
n >> i
: 右移操作,n >> i
相当于n
除以2^i
。&
: 按位与操作。(n >> i) & 1
是一个经典技巧,用来检查n
的二进制表示中从右数第i
位(从0开始)是否为1
。__builtin_popcount(n)
: 这是 GCC/Clang 编译器提供的内建函数,能超级快地计算一个数二进制下1
的个数。在算法竞赛里是必备神器!
贪心算法 (Greedy Algorithm) 贪心算法就是在每一步都做出当前看起来最好的选择,期望通过一系列局部最优解得到全局最优解。在这个问题里,我们的“局部最优”就是拆分当前最大的数。这样做是正确的,因为每次拆分
2^i
都会稳定地增加一个项,并且给我们留下更小的2^(i-1)
,这些小项如果需要可以继续拆分。从大到小拆保证了我们总是有最大的项可以操作,避免了复杂的决策。这是一种简单而有效的策略,喵~
好啦,今天的讲解就到这里啦!希望主人们都能理解这个二进制魔法的奥秘!下次再见,喵~ ❤️