Skip to content

Problem: 1927E - Klever Permutation

题目大意

主人,这道题是这样的~ 我们需要构造一个长度为 n 的排列 p。一个排列 p 呢,就是一个包含从 1n 所有整数,并且每个整数只出现一次的数组。

这个排列需要满足一个特殊的条件,叫做 k-level。这个条件是这样定义的:

  1. 首先,我们计算所有长度为 k 的连续子数组的和。比如,第一个和是 p[0] + ... + p[k-1],第二个是 p[1] + ... + p[k],一直到最后一个 p[n-k] + ... + p[n-1]。这样我们会得到 n-k+1 个和。
  2. 然后,我们找到这些和中的最大值 max(s) 和最小值 min(s)
  3. 如果 max(s) - min(s) <= 1,那么这个排列就满足 k-level 条件啦。

题目会给我们 nkk 保证是偶数),我们的任务就是找到任意一个满足条件的 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 的窗口和,最多只能有两种取值,而且这两个值必须是连续的整数,比如 XX+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 的值在 XX+1 之间来回跳动呢? 我们希望 S_1 - S_0 = p_k - p_0+1-1S_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, ...,完美满足条件!

所以,我们的构造策略就出来啦:

  1. 对于链条 0, 2, 4, ... (偶数开头的链条),我们让它们内部的数字是递增的。比如 p_0 < p_k < p_{2k}。这样 p_{i+k} - p_i 就是 +1
  2. 对于链条 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--

这样就能保证所有 1n 的数字都被用上,且只用一次,一个完美的排列就诞生啦!


题解代码

下面是根据这个思路写出来的C++代码,本喵加了一些注释,方便主人理解哦~

cpp
#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;
}

知识点介绍

主人,解决这道题不仅有趣,还能学到很多有用的知识点呢!

  1. 构造性算法 (Constructive Algorithms) 这道题是一个典型的构造题。这类问题不要求找到最优解,而是要求根据给定的一系列限制条件,构造出 任何一个 符合条件的解。关键在于分析限制条件,找到构造方法。

  2. 差分思想 (Difference Analysis) 我们解题的核心突破口,就是去分析相邻两个窗口和的差 S_{i+1} - S_i。通过简化这个差值,我们把一个复杂的关于“和”的约束,转化为了一个简单的关于“单个元素”的约束 |p_{i+k} - p_i| = 1。这种分析相邻项差异的技巧在很多数组、序列问题中都非常有用。

  3. 模运算与分组 (Modulo and Grouping) 我们将下标 0, 1, ..., n-1 分成了 k 个“链条”,实际上是按模 k 的余数进行了分组。所有下标 j 满足 j % k == i 的位置组成了第 i 个链条。这种基于模运算的分组思想是数论和组合数学中的基本工具。

  4. 双指针 (Two Pointers) 使用 lowhigh 两个指针,一个从头开始,一个从尾开始,来填充 1n 的数字,是一种非常经典且高效的技巧。它能确保我们不重不漏地使用所有数字,常用于构造排列或处理有序数据。

好啦,这次的题解就到这里啦!希望本喵的讲解能帮助到主人哦~ 如果还有其他问题,随时可以再来找我玩!喵~ (ฅ'ω'ฅ)

Released under the MIT License.