Problem: 1927E - Klever Permutation
题目大意
主人,这道题是这样的~ 我们需要构造一个长度为 n
的排列 p
。一个排列 p
呢,就是一个包含从 1
到 n
所有整数,并且每个整数只出现一次的数组。
这个排列需要满足一个特殊的条件,叫做 k-level。这个条件是这样定义的:
- 首先,我们计算所有长度为
k
的连续子数组的和。比如,第一个和是p[0] + ... + p[k-1]
,第二个是p[1] + ... + p[k]
,一直到最后一个p[n-k] + ... + p[n-1]
。这样我们会得到n-k+1
个和。 - 然后,我们找到这些和中的最大值
max(s)
和最小值min(s)
。 - 如果
max(s) - min(s) <= 1
,那么这个排列就满足 k-level 条件啦。
题目会给我们 n
和 k
(k
保证是偶数),我们的任务就是找到任意一个满足条件的 k-level
排列。
举个例子,如果 n=3, k=2
,一个可行的排列是 [1, 3, 2]
。 它的连续子数组和是:
p[0] + p[1] = 1 + 3 = 4
p[1] + p[2] = 3 + 2 = 5
咦?这个例子好像不对... 让我看看题目的例子... 啊!是[1, 3, 2]
吗?p1+p2=1+3=4
,p2+p3=3+2=5
。最大是5,最小是4,差是1。不对呀... 哦!猫猫看错了,Example里的n=3, k=2
的输出是1 3 2
,但是求和的时候p1+p2 = 1+3=4
,p2+p3 = 3+2=5
,这个差值是1。 唔...等一下,Note里说的是p1+p2=3+1=4
,p2+p3=1+2=3
。所以排列是[3, 1, 2]
。喵,原来是这样! 对于[3, 1, 2]
:p[0] + p[1] = 3 + 1 = 4
p[1] + p[2] = 1 + 2 = 3
最大和是 4,最小和是 3,差为 1。满足条件!看来猫猫要更仔细一点才行呢 >.<。
解题思路
这个 max(s) - min(s) <= 1
的条件看起来非常严格,就像要求一排猫咪的身高不能差太多一样!这意味着所有长度为 k
的窗口和,最多只能有两种取值,而且这两个值必须是连续的整数,比如 X
和 X+1
。
让我们来分析一下相邻两个窗口和的关系吧! 设 S_i
是从 p_i
开始的窗口和:S_i = p_i + p_{i+1} + ... + p_{i+k-1}
那么下一个窗口和 S_{i+1}
就是:S_{i+1} = p_{i+1} + p_{i+2} + ... + p_{i+k}
观察一下 S_{i+1}
和 S_i
的关系,喵~ 它们共享了大部分元素! S_{i+1} - S_i = (p_{i+1} + ... + p_{i+k}) - (p_i + ... + p_{i+k-1}) = p_{i+k} - p_i
这可是一个惊人的发现呀!两个相邻窗口和的差,就等于它们各自多出来的那个元素的差! 因为我们要求所有窗口和之间的差值不能超过 1,那么相邻窗口和的差 S_{i+1} - S_i
只能是 -1
, 0
, 或 1
。 也就是 p_{i+k} - p_i
只能是 -1
, 0
, 或 1
。
但是,排列中的元素都是独一无二的,所以 p_{i+k}
不能等于 p_i
,因此 p_{i+k} - p_i
不能是 0
。 所以我们得到了最最核心的结论: |p_{i+k} - p_i| = 1
这意味着,在排列中相隔 k
个位置的两个数,必须是连续的整数!比如 (5, 6)
或者 (10, 9)
。
这个结论把整个排列分成了 k
个独立的“链条”。
- 链条 0:
p_0, p_k, p_{2k}, ...
- 链条 1:
p_1, p_{1+k}, p_{2k+1}, ...
- ...
- 链条
k-1
:p_{k-1}, p_{2k-1}, p_{3k-1}, ...
在每个链条内部,元素的值必须是连续的整数。比如链条0可以是 [3, 4, 5, ...]
或者 [12, 11, 10, ...]
。
那么,怎么安排这些链条,才能让窗口和 S_i
的值在 X
和 X+1
之间来回跳动呢? 我们希望 S_1 - S_0 = p_k - p_0
是 +1
或 -1
。 S_2 - S_1 = p_{k+1} - p_1
是 +1
或 -1
。 最简单的方法就是让这些差值交替出现,比如 +1, -1, +1, -1, ...
。
p_k - p_0 = +1
p_{k+1} - p_1 = -1
p_{k+2} - p_2 = +1
- ...
这样,窗口和序列就会是 S_0, S_0+1, S_0, S_0+1, ...
,完美满足条件!
所以,我们的构造策略就出来啦:
- 对于链条 0, 2, 4, ... (偶数开头的链条),我们让它们内部的数字是递增的。比如
p_0 < p_k < p_{2k}
。这样p_{i+k} - p_i
就是+1
。 - 对于链条 1, 3, 5, ... (奇数开头的链条),我们让它们内部的数字是递减的。比如
p_1 > p_{1+k} > p_{2k+1}
。这样p_{i+k} - p_i
就是-1
。
要填充这些数字,我们可以用两个小爪子(指针)!一个 low
指向最小的可用数字(从 1
开始),一个 high
指向最大的可用数字(从 n
开始)。
- 当我们要填充一个递增链条时,就从小爪子
low
那里取数字,然后low++
。 - 当我们要填充一个递减链条时,就从大爪子
high
那里取数字,然后high--
。
这样就能保证所有 1
到 n
的数字都被用上,且只用一次,一个完美的排列就诞生啦!
题解代码
下面是根据这个思路写出来的C++代码,本喵加了一些注释,方便主人理解哦~
#include <iostream>
#include <vector>
#include <numeric> // 虽然这个代码里没用到,但有时候处理数组很好用哦
void solve() {
int n, k;
std::cin >> n >> k;
std::vector<int> p(n); // 这就是我们要构造的排列p,像一个空空的猫窝
// 准备好两个小爪子!
int low = 1; // 指向最小的可用数字
int high = n; // 指向最大的可用数字
// 我们要填充 k 个链条,链条的起始位置是 0, 1, ..., k-1
for (int i = 0; i < k; ++i) {
// i % 2 == 0 判断链条的起始位置是偶数还是奇数
if (i % 2 == 0) { // 偶数链条,比如 0, 2, 4...
// 这个链条是递增的,所以我们从小到大填数字
// j = i, i+k, i+2k, ... 遍历这个链条的所有位置
for (int j = i; j < n; j += k) {
p[j] = low++; // 把 low 指向的数字放进去,然后 low 向前走一步
}
} else { // 奇数链条,比如 1, 3, 5...
// 这个链条是递减的,所以我们从大到小填数字
// j = i, i+k, i+2k, ... 遍历这个链条的所有位置
for (int j = i; j < n; j += k) {
p[j] = high--; // 把 high 指向的数字放进去,然后 high 向后退一步
}
}
}
// 喵~ 排列构造完成!现在把它打印出来给主人看
for (int i = 0; i < n; ++i) {
std::cout << p[i] << (i == n - 1 ? "" : " ");
}
std::cout << "\n";
}
int main() {
// 这两行是为了让输入输出快一点,就像猫咪跑得飞快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
知识点介绍
主人,解决这道题不仅有趣,还能学到很多有用的知识点呢!
构造性算法 (Constructive Algorithms) 这道题是一个典型的构造题。这类问题不要求找到最优解,而是要求根据给定的一系列限制条件,构造出 任何一个 符合条件的解。关键在于分析限制条件,找到构造方法。
差分思想 (Difference Analysis) 我们解题的核心突破口,就是去分析相邻两个窗口和的差
S_{i+1} - S_i
。通过简化这个差值,我们把一个复杂的关于“和”的约束,转化为了一个简单的关于“单个元素”的约束|p_{i+k} - p_i| = 1
。这种分析相邻项差异的技巧在很多数组、序列问题中都非常有用。模运算与分组 (Modulo and Grouping) 我们将下标
0, 1, ..., n-1
分成了k
个“链条”,实际上是按模k
的余数进行了分组。所有下标j
满足j % k == i
的位置组成了第i
个链条。这种基于模运算的分组思想是数论和组合数学中的基本工具。双指针 (Two Pointers) 使用
low
和high
两个指针,一个从头开始,一个从尾开始,来填充1
到n
的数字,是一种非常经典且高效的技巧。它能确保我们不重不漏地使用所有数字,常用于构造排列或处理有序数据。
好啦,这次的题解就到这里啦!希望本喵的讲解能帮助到主人哦~ 如果还有其他问题,随时可以再来找我玩!喵~ (ฅ'ω'ฅ)