Skip to content

喵~ 主人,欢迎回来!今天我们来看一道有趣的构造题,Codeforces 2117B - Shrink。这道题就像是和数字玩捉迷藏,要把它们一个个藏起来,直到藏不下为止,很有趣的喵!就让本猫娘带你一步步解开它的谜底吧!(^・ω・^)

题目大意

这道题给了我们一个叫做 “Shrink” (收缩) 的操作,可以对一个数组进行。操作规则是这样的:

  1. 在一个大小为 m 的数组 a 中,找到一个索引 i(这个索引不能是第一个也不能是最后一个,也就是 2 \le i \le m-1)。
  2. 这个位置的元素 a_i 必须比它左边的邻居 a_{i-1} 和右边的邻居 a_{i+1} 都大,也就是 a_i > a_{i-1} 并且 a_i > a_{i+1}
  3. 如果找到了这样的 a_i,就可以把它从数组里拿走。

我们称一个排列(就是 1 到 n 的数字不重复不遗漏地排成一列)的分数,是它能执行 “Shrink” 操作的最大次数。

任务: 给定一个整数 n,我们需要构造一个长度为 n 的排列,让它的分数尽可能高。

简单来说,就是要我们想个办法排列 1 到 n 这些数字,使得我们能尽可能多次地移除那些“山峰”一样的数字喵。

解题思路

首先,我们来想想最多能操作多少次呢?每次操作都会让数组的长度减 1。操作的条件是元素不能在数组的两端,所以数组的长度至少要为 3。当数组只剩下 2 个元素时,就再也不能进行任何操作了。所以,从一个长度为 n 的数组开始,最多可以移除 n-2 个元素。我们的目标就是构造一个排列,让它真的能实现这 n-2 次操作!

那么,什么样的数字最容易成为“山峰”呢?当然是最大的数字啦!喵~ 比如数字 n,它比 1 到 n-1 的任何数都大。只要我们不把它放在数组的开头或者结尾,它就一定比它的邻居大,我们就可以第一个把它移除。

这给了我们一个很棒的贪心思路:每次都移除当前数组里最大的那个数

为了实现这个想法,我们可以倒着想:

  1. 我们希望最后剩下两个数。让它们是 12 好了。
  2. 在它们被剩下之前,我们移除的最后一个数应该是 3。为了能移除 3,它必须在 12 中间,形成 [1, 3, 2] 或者 [2, 3, 1] 这样的结构。
  3. 再往前一步,我们移除了 4。在移除 4 之前,4 必须是一个山峰。比如,当时的数组可能是 [1, 4, 3, 2]。移除 4 之后,就变成了 [1, 3, 2],正好是我们上一步的状态!
  4. 再往前,我们移除了 5。当时的数组可能是 [1, 5, 4, 3, 2]... 咦,不对,这个排列里 5 不是山峰。

看来倒着推有点复杂,我们还是顺着来吧。怎么构造一个排列,让我们能依次移除 n, n-1, n-2, \dots, 3 呢?

我们来试试这个结构:[1, 3, 4, 5, ..., n, 2]

让我们用 n=6 作为例子来看看这个排列 [1, 3, 4, 5, 6, 2] 的表现:

  1. 初始排列: [1, 3, 4, 5, 6, 2]

    • 当前最大的数是 6。它的邻居是 526 > 56 > 2,太棒了!6 是一个山峰,可以移除。
    • 移除 6 后,数组变成: [1, 3, 4, 5, 2]
  2. 第一步后: [1, 3, 4, 5, 2]

    • 当前最大的数是 5。它的邻居是 425 > 45 > 2,完美!5 也是一个山峰,移除它。
    • 移除 5 后,数组变成: [1, 3, 4, 2]
  3. 第二步后: [1, 3, 4, 2]

    • 当前最大的数是 4。它的邻居是 324 > 34 > 2,又是一个山峰!移除 4
    • 移除 4 后,数组变成: [1, 3, 2]
  4. 第三步后: [1, 3, 2]

    • 当前最大的数是 3。它的邻居是 123 > 13 > 2,最后的山峰!移除 3
    • 移除 3 后,数组变成: [1, 2]
  5. 最后: [1, 2]

    • 只剩下两个元素了,游戏结束喵~

我们成功进行了 4 次操作,对于 n=6,正好是 n-2 次!这个构造看起来非常有效。

为什么这个构造总是有效的呢?

  • 我们把最大的数 n 放在了 n-12 的旁边(在 [1, 3, ..., n-1, n, 2] 这个序列里)。因为 n 是老大,所以它肯定比邻居大,第一个被移除。
  • 移除 n 后,序列变成了 [1, 3, ..., n-1, 2]。现在的老大是 n-1,它的邻居变成了 n-22。因为 n \ge 3,所以 n-1 \ge 2。只要 n>3,就有 n-1 > 2。所以 n-1 也比它的新邻居大,可以被移除。
  • 这个过程会一直持续下去。每当我们移除当前最大的数 k 时,它的邻居总是 k-1(或者在移除3的时候是1)和 2。因为 k > k-1k > 2(对于所有 k \ge 3),所以它总能被移除。
  • 最终,我们移除了 n, n-1, \dots, 3,共 n-2 个数,达到了最大操作次数!

所以,我们的策略就是构造 [1, 3, 4, ..., n, 2] 这个排列。

题解代码

这是把上面的思路变成代码的样子,非常简单直观哦~

cpp
#include <iostream>
#include <vector>
#include <numeric>

void solve() {
    int n;
    std::cin >> n;
    
    // 我们的神奇构造法喵!
    // 先把 1 放出来
    std::cout << 1 << " ";
    
    // 然后把 3, 4, ..., n 依次放出来
    for (int i = 3; i <= n; ++i) {
        std::cout << i << " ";
    }
    
    // 最后把 2 放出来当“刹车片”~
    std::cout << 2 << std::endl;
}

int main() {
    // 加速一下,让程序跑得像猫一样快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    
    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }
    
    return 0;
}

相关知识点

这道题虽然简单,但背后也藏着一些有趣的计算机科学概念呢,主人~

  1. 排列 (Permutation)

    • 排列是指将一组对象进行有顺序的安排。在本题中,就是将 1 到 nn 个不同的整数排成一列。例如,[1, 3, 2] 是长度为 3 的一个排列,但 [1, 2, 2] 就不是,因为有重复的数字。
  2. 构造算法 (Constructive Algorithm)

    • 这类算法不是要求你计算某个值或者判断某个性质,而是要求你“构造”一个满足特定条件的对象。就像这道题,要求我们构造一个排列。解决这类问题的关键通常是找到一个普适的规律或模式,然后用代码实现这个模式的生成。
  3. 贪心算法 (Greedy Algorithm)

    • 我们的解题思路其实就是一种贪心策略。贪心算法在每一步选择中都采取在当前状态下最好或最优的选择,从而希望能导致结果是全局最好或最优的。在这里,我们“贪心”地选择每次都移除最大的数,并围绕这个目标来构造初始排列。幸运的是,在这个问题里,局部最优(能移除当前最大数)确实导向了全局最优(总共移除 n-2 个数)。

好啦,今天的讲解就到这里啦!是不是感觉很简单又好玩?只要找到那个特殊的排列模式,问题就迎刃而解了。主人下次遇到难题,也随时可以来找我玩哦!喵~ (ฅ'ω'ฅ)

Released under the MIT License.