喵~ 主人,今天由我来给你讲解一道非常有趣的算法题哦!这道题就像是给一堆数字做变身魔法,看看它们能不能变成我们想要的可爱模样,嘿嘿~ 准备好了吗?我们开始吧!
C. Division by Two and Permutation
题目大意
这道题是说,我们有一个由 n
个正整数组成的数组 a
。我们可以对数组里的任何一个数字 a[i]
进行一种操作:把它变成 ⌊a[i] / 2⌋
,也就是把它除以2然后向下取整。这个操作可以进行任意多次。
我们的任务是判断,通过这些操作,我们最终能不能让数组 a
变成一个 1
到 n
的排列。
排列 (Permutation) 是什么意思呢?喵~ 简单来说,就是一个包含
1, 2, 3, ..., n
所有数字,并且每个数字都只出现一次的集合。例如,当n=4
时,[1, 4, 3, 2]
就是一个排列,但[1, 1, 2, 3]
就不是啦,因为1
出现了两次,而且缺少了4
。
举个例子,如果 n=4
,数组 a
是 [1, 8, 25, 2]
。 我们可以:
- 把
25
变成⌊25/2⌋ = 12
。 - 再把
12
变成⌊12/2⌋ = 6
。 - 再把
6
变成⌊6/2⌋ = 3
。 - 再把
8
变成⌊8/2⌋ = 4
。 这样数组就变成了[1, 4, 3, 2]
,这是一个1
到4
的完美排列!所以答案是 "YES"。
是不是很有趣呀?喵~
解题思路
一看到这个问题,可能会觉得有点复杂,因为每个数字都有好多变化的可能。但是,我们可以用一种聪明的 贪心策略 来解决它!
贪心算法就像一只看到小鱼干就马上扑过去的小猫咪,喵~ 它在每一步都做出当前看起来最好的选择。
我们的核心思路是:让选择更多的数,去满足更难满足的条件。
谁的选择更多? 一个很大的数,比如
100
,可以变成50, 25, 12, 6, 3, 1
,选择非常多。而一个比较小的数,比如5
,只能变成5, 2, 1
,选择就少多啦。所以,大数比小数更“灵活”。什么条件更难满足? 我们需要凑齐
1
到n
的所有数字。显然,凑出像n
这样的大数字,比凑出1
这样的小数字要难。因为能变成n
的原始数字必须大于等于n
,而几乎所有数字都能变成1
。
结合这两点,我们的贪心策略就出来啦:
优先处理数组 a
中最大的数字,让它去尝试匹配 1
到 n
中还未被匹配的、最大的那个数。
具体步骤如下:
- 排序:先把数组
a
从大到小排序。这样我们就可以先处理那些最“灵活”的大数。 - 标记:我们用一个布尔数组
taken
,大小为n+1
,来记录1
到n
这些目标数字是否已经被“认领”了。taken[i] = true
表示数字i
已经被某个a
中的元素变成了。 - 遍历和匹配:
- 我们从排序后最大的
a[i]
开始遍历。 - 对于当前的数字
x
,我们让它不断地除以2,看看它能变成哪些数。 - 在它所有能变的目标中(比如
x, x/2, x/4, ...
),我们从大到小检查:如果当前变出的值val <= n
并且taken[val]
还是false
(也就是还没被认领),太棒了!我们就让x
认领val
,把taken[val]
设为true
,然后就去处理下一个a
中的数字。 - 如果把
x
一路除到0
,都找不到一个可以认领的目标(所有它能变成的1
到n
之间的数都已经被别的数认领了),那就说明没希望啦,这个x
找不到位置了。这种情况我们直接判定为 "NO"。
- 我们从排序后最大的
- 最终结果:如果数组
a
中所有的数字都成功找到了自己的位置,那么最后我们就可以自豪地宣布 "YES"!
让我们用 a = [24, 7, 16, 7]
, n=4
这个例子走一遍流程喵~
- 排序
a
->[24, 16, 7, 7]
taken
数组 ->[F, F, F, F]
(对应 1, 2, 3, 4)- 处理
x = 24
:val = 24
( > 4, 跳过)val = 12
( > 4, 跳过)val = 6
( > 4, 跳过)val = 3
( <= 4 并且taken[3]
是false
) -> 认领3
!taken
变为[F, F, T, F]
。
- 处理
x = 16
:val = 16
( > 4, 跳过)val = 8
( > 4, 跳过)val = 4
( <= 4 并且taken[4]
是false
) -> 认领4
!taken
变为[F, F, T, T]
。
- 处理
x = 7
:val = 7
( > 4, 跳过)val = 3
( <= 4 但taken[3]
是true
,被占了)val = 1
( <= 4 并且taken[1]
是false
) -> 认领1
!taken
变为[T, F, T, T]
。
- 处理
x = 7
:val = 7
( > 4, 跳过)val = 3
(被占了)val = 1
(被占了)- 找不到了!这个
7
无论如何都变不出一个还没被占用的1
到4
之间的数。
- 所以,最终结果是 "NO"。
题解代码 (C++)
这是实现上面思路的C++代码,我已经加上了可爱的注释哦~
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
void solve() {
int n;
std::cin >> n;
std::vector<long long> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// 喵~ 先把数组从大到小排个序,这样我们就能优先处理那些“选择更多”的大数字啦
std::sort(a.begin(), a.end(), std::greater<long long>());
// 用一个布尔数组来记录1到n这些数字是不是已经被“认领”了喵
std::vector<bool> taken(n + 1, false);
bool possible = true;
// 遍历排序后的数组a中的每一个数字
for (long long x : a) {
long long val = x;
bool found_slot = false; // 标记当前这个x有没有找到它的位置
// 从它本身开始,不停地除以2,寻找一个可以安家的位置
while (val > 0) {
// 如果找到一个可以用的数字v(v <= n 并且还没被认领),就太棒啦!
if (val <= n && !taken[val]) {
taken[val] = true; // 把它标记为“已认领”
found_slot = true; // 成功找到位置!
break; // 跳出循环,处理下一个a中的数字吧
}
val /= 2; // 继续变小,寻找下一个可能性
}
// 如果这个x变来变去,都找不到一个可以用的位置,那就说明不行啦,喵呜~
if (!found_slot) {
possible = false;
break;
}
}
if (possible) {
std::cout << "YES\n";
} else {
std::cout << "NO\n";
}
}
int main() {
// 加速输入输出,让程序跑得更快一点~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
相关知识点介绍
贪心算法 (Greedy Algorithm) 贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。就像前面说的,我们的策略“让大数优先匹配它能变成的、最大的、还没被占用的目标”就是一个典型的贪心策略。我们相信,通过满足最苛刻的条件,可以为后续更简单的条件留出更多空间,从而提高整体成功的概率。这道题的贪心策略是正确的!
排序 (Sorting) 排序是将一组数据依照指定的顺序进行排列的过程。在这里,我们使用降序排序,它是我们贪心策略能够成功实施的前提。先处理大数,再处理小数,这个顺序至关重要。
排列 (Permutation) 排列是组合数学中的一个基本概念。从n个不同元素中取出m(m≤n)个元素,按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列。当m=n时,我们称之为全排列。这道题的目标就是构造一个
1
到n
的全排列。
希望这篇讲解能帮助到主人哦!如果还有其他问题,随时可以再来问我,喵~ ❤️