比赛链接

A

题意

给一个数 \(k\) 找到最大的 \(x\) ,满足 \(1 \leq x < k\)\(x!+(x-1)!\)\(k\) 的倍数。

题解

知识点:数学。

猜测 \(x = k-1\) ,证明 \((k-1)! + (k-2)! = (k-1+1) \cdot(k-2)! = k \cdot (k-2)!,k \geq 2\)

因此 \(x = k-1\)

时间复杂度 \(O(1)\)

空间复杂度 \(O(1)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

bool solve() {
    int k;
    cin >> k;
    cout << k - 1 << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

B

题意

给一个长为 \(n\) 的排列,每次操作从排列中取出 \(k\) 个数,从小到大排序好放回排列尾部。问最少操作多少次,才能将原排列变成从小到大排序好的排列。

题解

知识点:贪心。

注意到每次操作都会把数字放到尾部,不会影响之前数字的相对位置。因此为了使得操作最小化,我们先找到不用选的数字有多少,显然我们需要从 \(1\) 开始递增往后找。设 \(pos\) 是第一个要选的数字,那么答案便是 \(\Big\lceil \dfrac{n - pos + 1}{k} \Big\rceil\)

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[100007];
bool solve() {
    int n, k;
    cin >> n >> k;
    for (int i = 1;i <= n;i++) cin >> a[i];
    int pos = 1;
    for (int i = 1;i <= n;i++) {
        if (pos == a[i]) pos++;
    }
    cout << (n - pos + 1 + k - 1) / k << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

C

题意

给出 \(n\) 个数 \(a_i\) ,要求两个长为 \(n\) 的排列 \(p,q\) 使得 \(a_i = \max(p_i,q_i)\)

题解

知识点:构造。

先记录每个数字出现的位置 \(pos[a[i]]\) ,随后从小到大构造:

  1. 数字没出现过,那么可以放入队列 \(qu\) ,用于补齐出现两次的数字的空位。
  2. 数字只出现了一次,假设出现在 \(a_i\) ,那么令 \(p_i = q_i = a_i\) 是最优的。因为 \(p_i,q_i\) 其中一个可以更小,但小的数字可能要用于填充别的地方,所以最优解是填两个相等的。
  3. 数字出现了两次,假设出现在 \(a_i = a_j = a\) ,那么令 \(p_i = q_j = a\) ,设 \(qu\) 里队首元素为 \(x\) ,令 \(q_i = p_j = x\) 。因为是从小到大构造,所以 \(qu\) 里的元素一定是比 \(a\) 小的,所以可以用来填充空位;如果队空,则无解。
  4. 数字出现三次及以上,无解。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[200007];
int p[200007], q[200007];
vector<int> pos[200007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i], pos[i].clear();
    for (int i = 1;i <= n;i++) pos[a[i]].push_back(i);
    queue<int> qu;
    for (int i = 1;i <= n;i++) {
        if (pos[i].size() > 2) return false;
        if (pos[i].size() == 0) qu.push(i);//空闲数字放入队列
        else if (pos[i].size() == 1) {
            p[pos[i][0]] = i;
            q[pos[i][0]] = i;
        }//可以用小的但不是最优的
        else {
            if (qu.empty()) return false;//没有空闲的小的数字,无解
            p[pos[i][0]] = i;
            p[pos[i][1]] = qu.front();
            q[pos[i][0]] = qu.front();
            q[pos[i][1]] = i;
            qu.pop();
        }
    }
    cout << "YES" << '\n';
    for (int i = 1;i <= n;i++) cout << p[i] << " \n"[i == n];
    for (int i = 1;i <= n;i++) cout << q[i] << " \n"[i == n];
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << "NO" << '\n';
    }
    return 0;
}

D

题意

给一个长为 \(n\) 的排列,每次可以选择两个数交换,问最少交换几次可以使得排列逆序数为 \(1\)

题解

知识点:枚举,数学。

关于这类排列的题,都可以先进行一个构造,连接所有 \(i \to a[i]\) ,图中会形成若干个环,称为置换环。例如 \(2,3,4,1,5\) ,可以得到 \(1,2,3,4\) 构成的环和 \(5\) 构成的环( \(5\) 是自环)。

我们进行一次交换操作 \((i,j)\),将使得 \(i \to a_i,j \to a_j\) 两条边变成 \(i \to a_j,j \to a_i\) 。这个操作在图中可以做到以下两个结果之一:

  1. 一个环被裂解成两个环
  2. 两个环被合并成一个环

前提是不破坏相对元素的位置,例如 \(1,3,2,4\) 环不可能分解成 \(1,2\)\(3,4\) 环;\(1,2\)\(3,4\) 环也不可能合并成 \(1,3,2,4\) 环。

举个例子,我们对 \(2,3,4,1,5\) 交换 \((2,4)\) ,则排列变成 \(2,1,4,3,5\) ,图中边 \(2 \to 3,4\to 1\) 变成 \(2 \to 1,4 \to 3\) ,即 \(1,2,3,4\) 环被拆成 \(1,2\)\(3,4\) 两个环;或者交换 \((4,5)\) ,则排列变成 \(2,3,4,5,1\) ,图中边 \(4 \to 1,5 \to 5\) 变成 \(4 \to 5,5 \to 1\) ,即 \(1,2,3,4\)\(5\) 环被合成为 \(1,2,3,4,5\) 环。

回到题目。题目要求的最终状态化成图后,实际上就是一组相邻元素成环,剩下的元素自环。

我们对原排列化为置换环图,假设这些环中已经有至少一组相邻元素(环中位置不一定需要相邻,因为可以通过操作使其相邻),如 \(1,4,2\) 环就有 \(1,2\) 两个相邻元素,我们可以在之后的操作中保留这组元素,把其他元素全都操作成自环即可;如果没有,那么先将元素都操作成自环,再多一次操作把一组相邻元素合并成环,如排列 \(3,4,5,2,1\)\(1,3,5\)\(2,4\) 环,一个相邻元素都没有。

假设 \(n\) 个元素的图中有 \(cnt\) 个环,那么如果我们需要把环中元素都操作成自环,实际上需要操作 \(n-cnt\) 次,因为每个环保留一个元素,剩下的元素都需要通过操作挪出来。再考虑相邻元素的结论,如果有相邻元素那么可以少操作一次 \(n-cnt-1\) ;否则需要多操作一次 \(n-cnt+1\)

环的实现可以看代码,和并查集类似但简单许多。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[200007];
int fa[200007];

bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i], fa[i] = -1;
    int ans = n;
    for (int i = 1;i <= n;i++) {
        if (fa[i] != -1) continue;
        int j = i;
        ans--;//一个环减一次,
        while (fa[j] == -1) {
            fa[j] = i;//环内元素的根设为i
            j = a[j];
        }
    }
    bool ok = 0;
    for (int i = 1;i <= n - 1;i++) ok |= fa[i] == fa[i + 1];//环内有一队相邻元素,可以少操作一次
    cout << ans + (ok ? -1 : 1) << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

E

题意

对一个长度为 \(3n\) 的排列 \(p\),有两种操作:

  1. 对前 \(2n\) 个元素从小到大排序
  2. 对后 \(2n\) 个元素从小到大排序

定义 \(f(p)\) 为将排列 \(p\) 通过操作从小到大排序好的最少次数。

现在给出一个 \(n\) ,求长度为 \(3n\)\((3n)!\) 个排列的 \(f(p)\) 之和。

题解

知识点:容斥原理,排列组合。

显然,对于任意排列 \(p\)\(f(p) \leq 3\) ,只要使用操作 \(1,2,1\) 就能排序好。

现在分类讨论 \(f(p)\) ,求对应的种类数。注意到,求精确 \(f(p) = 1,2,3\) 的种类数比较困难,因此考虑求 \(f(p) \leq 1,f(p)\leq 2,f(p) \leq 3\) 的情况,最后作差即可。

  1. \(f(p) = 0\)

显然只有一种 \(1,\cdots,3n\)

  1. \(f(p) \leq 1\)

至多一次操作就可以排序好,那么一定是前 \(n\) 个或者后 \(n\) 个元素已经排好了,其他元素自由排列,对剩下部分操作一次即可。

\(n\) 个元素排好了,后 \(2n\) 个自由排列,一共 \((2n)!\) 种。同理,后 \(n\) 个元素排好了也有 \((2n)!\) 种。

当然,最后需要减去交集,即前 \(n\) 个元素和后 \(n\) 个元素同时排好了,则中间 \(n\) 个元素自由排列,有 \(n!\) 种。

因此,最终有 \(2 (2n)! - n!\) 种。

  1. \(f(p) \leq 2\)

至多两次操作就可以排序好,那么元素 \([1,n]\) 需要出现在 \([1,2n]\) 的区间里,其他元素自由排列,这样对 \([1,2n]\) 操作一次前 \(n\) 个就排好了,再对 \([n+1,3n]\) 操作一次即可,同理元素 \([2n+1,3n]\) 出现在 \([n+1,3n]\) 其他元素自由排列也可以,只需要最多操作两次。

元素 \([1,n]\) 出现在 \([1,2n]\) ,那么在 \(2n\) 个位置里选 \(n\) 个位置放这 \(n\) 个元素,并且这 \(n\) 个元素可以自由排列,剩下 \(2n\) 个元素也可以自由排列,因此有 \(C_{2n}^n \cdot n!\cdot (2n)!\) 种。同理,元素 \([2n+1,3n]\) 出现在 \([n+1,3n]\) 也有 \(C_{2n}^n \cdot n!\cdot (2n)!\) 种。

最后减去交集部分,即元素 \([1,n]\) 出现在 \([1,2n]\) 同时元素 \([2n+1,3n]\) 出现在 \([n+1,3n]\) 。直接算比较困难,考虑设元素 \([1,n]\) 种有 \(i\) 个元素出现在 \([n+1,2n]\) 中,则元素 \([1,n]\)\(n-i\) 个元素出现在 \([1,n]\) 中。于是,对于元素 \([1,n]\) ,在 \([1,n]\)\(n-i\) 个位置,在 \([n+1,2n]\)\(i\) 个位置,并任意排列,有 \(C_{n}^{n-i} \cdot C_{n}^{i} \cdot n!\) 种;对于元素 \([2n+1,3n]\) ,因为 \([n+1,2n]\)\(i\)\([1,n]\) 的元素,所以在 \([n+1,3n]\)\(2n-i\) 个位置可以选,其中中选 \(n\) 个,并任意排列,有 \(C_{2n-i}^n \cdot n!\) 种;剩下 \(n\) 个元素任意排列有 \(n!\) 种,共有 \(C_{n}^{n-i} \cdot C_{n}^{i} \cdot n! \cdot C_{2n-i}^n \cdot n! \cdot n!\) 种。 最终把 \(i \in [0,n]\) 累加一遍就是交集部分。

因此,最终有 \(2\cdot C_{2n}^n \cdot n!\cdot (2n)! - \sum_{i=0}^n (C_{n}^{n-i} \cdot C_{n}^{i} \cdot n! \cdot C_{2n-i}^n \cdot n! \cdot n!)\) 种。

  1. \(f(p) \leq 3\)

全排列即可,共有 \((3n)!\) 种。

最后,做差就可以得到 \(f(p) = 1,2,3\) 的种类数,分别加权和即可。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int M;

int qpow(int a, int k) {
    int ans = 1;
    while (k) {
        if (k & 1) ans = 1LL * ans * a % M;
        k >>= 1;
        a = 1LL * a * a % M;
    }
    return ans;
}
int fac[3000007], invfac[3000007];
void init(int n) {
    fac[0] = 1;
    for (int i = 1;i <= n;i++) fac[i] = 1LL * fac[i - 1] * i % M;
    invfac[n] = qpow(fac[n], M - 2);
    for (int i = n;i >= 1;i--) invfac[i - 1] = 1LL * invfac[i] * i % M;
}

int C(int n, int m) {
    if (n < m || m < 0) return 0;
    return 1LL * fac[n] * invfac[m] % M * invfac[n - m] % M;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n >> M;
    init(3 * n);
    int ans[4];
    ans[0] = 1;
    ans[1] = (1LL * 2 * fac[2 * n] % M - fac[n] + M) % M;
    ans[2] = 1LL * 2 * C(2 * n, n) % M * fac[n] % M * fac[2 * n] % M;
    for (int i = 0;i <= n;i++) {
        ans[2] = (ans[2] - 1LL * C(n, i) * C(n, n - i) % M * fac[n] % M * C(2 * n - i, n) % M * fac[n] % M * fac[n] % M + M) % M;
    }
    ans[3] = fac[3 * n];

    ans[1] = (ans[1] - ans[0] + M) % M;
    ans[2] = ((ans[2] - ans[1] + M) % M - ans[0] + M) % M;
    ans[3] = (((ans[3] - ans[2] + M) % M - ans[1] + M) % M - ans[0] + M) % M;
    int res = ((ans[1] + 1LL * 2 * ans[2] % M) % M + 1LL * 3 * ans[3] % M) % M;
    cout << res << '\n';
    return 0;
}