Codeforces 2157H - Keygen 3

高端题目。

\(\text{LIM} = 2000\)

首先需要注意,这题并不需要知道 \(k\) 的准确值,只需要知道 \(\min(k, \text{LIM})\),如果没发现的话可以先不往下看了/续标识。

首先考虑一些能做的 \((n, m)\),因为题目说明了这个排列得是单峰的,从大到小考虑每个值,这个值只能放在当前序列的最左侧和最右侧,于是只会有 \(2^{n - 1}\) 种单峰排列,此时暴力 \(\mathcal{O}(n)\) 求环数判断是否和 \(m\) 相同,就可以做到 \(\mathcal{O}(2^n n)\) 的复杂度。

但是这个做法只能用在 \(n\) 较小的情况,但是感受一下,因为 \(\text{LIM}\) 并不大,所以当 \(n\) 在大一点的时候,或许有很多组 \((n, m)\) 都能有 \(\text{LIM}\) 种合法排列。

这启发我们尝试一些增量构造,即若这一组 \((n, m)\) 已经有 \(\text{LIM}\) 个合法排列了,尝试让每一个排列扩展到 \((n + x, m + y)\),这样就能构造出 \((n + x, m + y)\)\(\text{LIM}\) 个合法排列了。

为了方便表述,接下来把合法排列的单峰条件转为单谷条件。

一个合法的单谷排列 \(p\) 转化到合法的单峰排列 \(p\) 只需要把 \(p_x\) 变为 \(n - p_x + 1\) 并 reverse 整个序列,因为这相当于是把一条 \(i\to p_i\) 的边的端点都按 \(x = \frac{n + 1}{2}\) 对称。

那么首先尝试一些简单的增量。

\((n, m)\to (n + 1, m + 1)\),发现只需要在末尾加入一个自环,即令 \(p_{n + 1} = n + 1\) 即可。重复进行便可以做到 \((n, m)\to (n + x, m + x)(x\ge 0)\)

接下来继续尝试,刚刚的构造 \(n - m\) 的值始终没有改变,当 \(n - m\) 较大的时候就无法从 \(n\) 小的情况扩展了,即我们想要尝试 \((n, m)\to (n + x, m + y)(0\le y\le x)\)

但这个形式并不是很好看,因为中间的过程不太好进行一些微调,于是可以让步骤变为 \((n, m)\to (n + x, m + x)\to (n + x, m + x - y)(0\le y\le x)\)

那么同样的,可以先尝试 \((n + x, m + x)\to (n + x, m + x - 1)\) 再重复操作。

那么只减少一个环,就可以考虑通过 \(\operatorname{swap}(p_x, p_y)\) 合并两个环(要满足 \(x, y\) 不在一个环里),发现如果 \(x, y\) 都在谷的同一侧就会破坏单谷的结构,于是只能让 \(x, y\) 在谷的两侧。

为了尽可能不打乱结构,尝试选一些比较特殊的值,比如说令 \(x = 1\),分析 \(p_1\),发现序列的末尾一定是 \(p_1 + 1, p_1 + 2, \cdots, n + x - 1, n + x\),即 \(p_1 + 1\sim n + x\) 都是在序列末尾的自环,因为这些值只能在谷的右侧并且要满足递增关系。

那么就可以考虑令 \(x = 1, y = p_1 + 1\),这样合并后就把 \(p_1 + 1\) 的自环合并进了 \(1\) 的环,并且 \(p_1\) 只会 \(+1\),因为初值 \(p_1\le n\),所以可以知道构造到 \((n + x, m + x - y)\) 时一定有 \(p_1\le n + y\le n + x\),并不会出问题。

那么只要存在较小的 \((n, m)\) 满足合法排列数 \(\ge \text{LIM}\),对于 \(n'\ge n, m'\ge m, n' - m'\ge n - m\)\((n', m')\) 就做完了。

那么就只需要考虑 \(n - m\) 较小的情况了。

考虑 \(n - m\) 较小就意味着排列应当存在许多个 \(p_i = i\) 的自环。

最极端的情况下分析,即非自环的环都是二元环时,也可以知道至多能有 \(n - m\) 个二元环,也就是说至少有 \(2m - n\) 个点都是 \(p_i = i\) 的自环。

考虑把 \((i, p_i)\) 画在平面上,并画出 \(x = y\) 这一条直线。

根据前面的分析,会知道 \(p_1 + 1\sim n\) 肯定都是在这个平面上的,并且 \((p_1, p_{p_1})\) 肯定不在这条直线上(\(p_{p_1} < p_1\)),那么记谷的位置在 \(x\),就可以知道 \(x\sim p_1\) 的位置都不可能在平面上。

继续考虑 \(1\sim x\) 的部分,即谷的左侧,会发现此时 \(x\) 坐标递增 \(y\) 坐标递减,这说明这部分与 \(x = y\) 这条直线也至多只有 \(1\) 个交点。

这说明如果固定了 \(p_1\),这个排列的自环数至多为 \(n - (p_1 + 1) + 1 + 1 = n - p_1 + 1\),而这个值需要 \(\ge 2m - n\),解不等式可得 \(p_1\le 2n - 2m + 1\)

并且因为 \(p_1 + 1\sim n\) 都是自环,这说明对于每个排列只需要关心 \(1\sim p_1\) 的部分,此时跑暴力就有一个 \(\mathcal{O}(4^{n - m}(2n - 2m))\) 的做法了。

平衡一下,会发现当 \(n = 18\)\(1\le m\le 8\)\((n, m)\) 都至少有 \(\text{LIM}\) 个合法排列,于是考虑:

  • \(n\le 18\) 时,跑 \(\mathcal{O}(2^n n)\) 的暴力。
  • \(n - m\ge 10\) 时,先 \(\mathcal{O}(2^{18}\times 18)\) 跑出 \(\text{LIM}\)\(n' = 18\) 的方案,再 \(\mathcal{O}(n\times \text{LIM})\) 增量构造。
  • \(n - m\le 9\) 时,跑 \(\mathcal{O}(4^{n - m}(2n - 2m))\) 的暴力做法。
#include <bits/stdc++.h>

using seq = std::vector<int>;

constexpr int LIM = 2000;

inline void print(int n, const std::vector<seq> &ans) {
  printf("%zu\n", ans.size());
  for (seq p : ans) {
    std::reverse(p.begin(), p.end());
    for (int x : p) {
      printf("%d ", n - x);
    }
    puts("");
  }
}

inline std::vector<seq> brute_force(int n, int m) {
  std::vector<seq> ans;

  for (int s = 0; s < (1 << n - 1) && (int)ans.size() < LIM; s++) {
    seq p(n);
    for (int i = 0, pl = 0, pr = n - 1; i < n; i++) {
      if (s >> i & 1) {
        p[pl++] = n - 1 - i;
      } else {
        p[pr--] = n - 1 - i;
      }
    }

    std::vector<int> vis(n, 0);
    int cnt = 0;
    for (int i = 0; i < n; i++) {
      if (! vis[i]) {
        for (int j = i; ! vis[j]++; ) {
          j = p[j];
        }
        cnt++;
      }
    }

    if (cnt == m) {
      ans.push_back(p);
    }
  }

  return ans;
}

int main() {
  int n, m;
  scanf("%d%d", &n, &m);

  if (n <= 18) {
    print(n, brute_force(n, m));
    return 0;
  }

  const int delta = n - m;

  if (delta >= 10) {
    const int _n = 18;
    const int _m = std::max(_n - delta, 1);
    const int sub = delta - (_n - _m);

    std::vector<seq> ans = brute_force(_n, _m);
    for (seq &p : ans) {
      while ((int)p.size() < n) {
        p.push_back((int)p.size());
      }
      const int l = p[0] + 1, r = p[0] + sub;
      for (int &x : p) {
        x -= l <= x && x <= r;
      }
      p[0] = r;
    }
    print(n, ans);
    
    return 0;
  }

  std::vector<seq> ans = brute_force(delta * 2 + 1, delta + 1);
  for (seq &p : ans) {
    while ((int)p.size() < n) {
      p.push_back((int)p.size());
    }
  }
  print(n, ans);
  
  return 0;
}
posted @ 2025-11-24 21:40  rizynvu  阅读(10)  评论(0)    收藏  举报