目录
写在前面
仁王真好玩
大太刀真好玩
下辈子我还要玩大太刀
[](https://pic.imgdb.cn/item/63b7fdb4be43e0d30ec2dccd.jpg)
顺带吐槽一下,这什么排列专场,BCDE 全是排列也挺牛逼的。
A
有 \((k-1)! + (k-2)! = k!(k-2)!\),答案即 \(k-1\)。
//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
//=============================================================
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
return f * w;
}
//=============================================================
int main() {
int T = read();
while (T --) {
int n = read();
printf("%d\n", n - 1);
}
return 0;
}
B
找到原排列中以 \(1\) 为第一个元素的、最长的连续的上升子序列,设找到的子序列长度为 \(m\)。然后对其他 \(n-m\) 个元素按照升序进行操作,操作次数即 \(\lceil\frac{n-m}{k}\rceil\)。
//By:Luckyblock
/*
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 1e5 + 10;
//=============================================================
int p[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
return f * w;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int T = read();
while (T --) {
int n = read(), k = read();
for (int i = 1; i <= n; ++ i) p[i] = read();
int maxp = 0;
for (int i = 1; i <= n; ++ i) {
if (p[i] == maxp + 1) ++ maxp;
}
printf("%d\n", (int) ceil(1.0 * (n - maxp) / k));
}
return 0;
}
C
\(t\) 组数据,每组数据给定一长度为 \(n\) 的数列 \(a\),满足 \(1\le a_i\le n\)。
要求构造两个长度为 \(n\) 的排列 \(p,q\),满足:\(\max(p_i, q_i) = a_i\)。
\(1\le t\le 10^4, 1\le n\le 2\times 10^5, 1\le a_i\le n, \sum n\le 2\times 10^5\)。
2S,256MB。
先手玩几组数据猜猜结论:
- 如果一个数在 \(a\) 中出现 3 次及以上,则无解。
- 如果一个数在 \(a\) 中出现 1 次,则可以令 \(p,q\) 对应位置上均为该数,再考虑其他位置就相当于规模为 \(n-1\) 的子问题,对其他位置的构造没有影响。
- 之后仅需考虑在 \(a\) 中出现了 2 次的数,和未在 \(a\) 中出现过的数。
设某个在 \(a\) 中出现了 2 次的数为 \(x\),在 \(a\) 中出现的位置为 \(i,j\)。对于 \(p_i,q_i,p_j,q_j\),一种显然的构造方法是找到一个在 \(a\) 中未出现过的小于 \(x\) 的数 \(y\),令 \(p_i=x,q_i=y\),\(p_j=y,q_j=x\) 即可。为了使得尽可能有解,较小的 \(x\) 应当对应较小的 \(y\)。则可考虑记录下这两种数并将它们分别升序排序,按顺序配对构造即可。如果过程中无法合法地配对,则无解。
复杂度 \(O(n\log n)\) 级别。
//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 2e5 + 10;
//=============================================================
int a[kN], cnt[kN], pos[2][kN], p[kN], q[kN];
int todo[kN], use[kN];
bool vis[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
return f * w;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int T = read();
while (T --) {
int n = read(), flag = 0;
for (int i = 1; i <= n; ++ i) cnt[i] = vis[i] = 0;
for (int i = 1; i <= n; ++ i) {
a[i] = read();
cnt[a[i]] ++;
if (cnt[a[i]] == 1) pos[0][a[i]] = i;
if (cnt[a[i]] == 2) pos[1][a[i]] = i;
if (cnt[a[i]] == 3) flag = 1;
}
if (flag == 1 || cnt[n] == 0) {
printf("NO\n");
continue;
}
int todonum = 0, usenum = 0;
for (int i = 1; i <= n; ++ i) {
if (cnt[a[i]] == 1) {
p[i] = q[i] = a[i];
vis[a[i]] = 1;
} else if (cnt[a[i]] == 2) {
if (vis[a[i]] == 0) todo[++ todonum] = a[i];
vis[a[i]] = 1;
}
}
for (int i = 1; i <= n; ++ i) {
if (!vis[i]) use[++ usenum] = i;
}
std::sort(todo + 1, todo + todonum + 1);
for (int i = 1, j = 1; i <= todonum; ++ i, ++ j) {
int x = todo[i], y = use[j];
if (j > usenum || y >= x) flag = 1;
p[pos[0][x]] = x, q[pos[0][x]] = y;
p[pos[1][x]] = y, q[pos[1][x]] = x;
}
printf("%s\n", flag ? "NO" : "YES");
if (!flag) {
for (int i = 1; i <= n; ++ i) printf("%d ", p[i]);
printf("\n");
for (int i = 1; i <= n; ++ i) printf("%d ", q[i]);
printf("\n");
}
}
return 0;
}
D
\(t\) 组数据,每组数据给定一长度为 \(n\) 的排列 \(p\)。可以进行任意次操作,每次操作可以选定排列中的两个位置,并将两个位置上的数进行交换。求至少进行多少次交换,可使排列中有且仅有 1 个逆序对。
\(1\le t\le 10^4, 2\le n\le 2\times 10^5, 1\le p_i\le n, \sum n\le 2\times 10^5\)。
1S,256MB。
赛时写了个半假不真的东西最后十分钟尝试 rush 一波真是笑死我了
模型转化挺妙的。
只有一个逆序对,则最终排列的形态一定是类似下述形式的:
\[1,2,\dots,i-1,\underline{i+1,i},i+2,\dots,n-1,n \]直接考虑如何排成这样有些让人摸不着头脑,不妨考虑如何通过交换使得排列有序。更进一步地,可以想到把原排列的 \(i\) 和 \(i+1\) 的位置进行交换,再考虑如何使得排列有序,就等价于使原排列有且仅有 1 个逆序对。考虑枚举 \(i\),问题变为如何快速求得使排列有序的代价。
手玩几组数据找找结论:
- 仅需考虑无序的元素。
- 每次操作至多使得两个元素有序。
- 把一个元素放到有序的位置只需直接把该元素和对应位置的元素交换,仅需要一次操作。
- 由 3,4 如果某次操作不能使至少一个元素有序,那这次操作 不优于直接把一个元素交换到有序的位置。
- 如果这次操作使得之后一次操作能使两个元素有序,那这两次操作和进行两次结论 3 的操作结果是等价的。
- 即使这次操作后使得之后至多两次操作能使两个元素有序,这次操作也不优于直接把一个元素交换到有序的位置。
可以证明这种情况仅会在这种结构中出现:[2, 3, 4, 1]
。但手玩一下可以发现:[2, 3, 4, 1]->[2, 1, 4, 3]->[1, 2, 4, 3]->[1, 2, 3, 4]
和[2, 3, 4, 1]->[3, 2, 4, 1]->[4, 2, 3, 1]->[1, 2, 3, 4]
所需的操作次数相同。
- 由上,最优的策略是连续进行多次直接把某元素放到对应位置的操作。如果我们选择元素时采取这样的顺序:\(i\rightarrow a_i\rightarrow a_{a_i}\rightarrow a_{a_{a_{i}}}\rightarrow \dots\) 可以发现最后一次交换时,一定会把某个元素放到位置 \(i\) 上,形成了一种环状结构,而且使这个环上的所有元素有序的代价恰好是环上元素数 \(-1\)。
设环状结构的个数为 \(c\),使整个排列有序的代价恰好是 \(n-c\)。
由上,考虑建立图论模型,令节点 \(i\) 向 \(a_i\) 连边,构造一个 \(n\) 个节点 \(n\) 条边的有向图。由于每个节点出度均为 1,且 \(p\) 是一个排列,元素和下标均是一一对应的关系,则各节点的入度也均为 1,则该图一定是由数个简单环构成的。
再考虑枚举 \(i\) 并交换 \(i\) 和 \(i+1\),发现仅影响到了两条边:原来连向 \(i\) 的边连向了 \(i+1\),原来连向 \(i+1\) 的边连向了 \(i\)。再手玩一下可以发现,如果 \(i,i+1\) 原来在一个连通块中,则交换后环的数量 \(+1\),否则 \(-1\)。
则可在建图后进行 dfs,\(O(n)\) 地预处理连通块信息即可 \(O(1)\) 地求得每次交换后使排列有序的代价。总复杂度 \(O(n)\) 级别。
//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 2e5 + 10;
//=============================================================
int n, ans, a[kN], circle, bel[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
return f * w;
}
void Dfs(int u_) {
if (bel[u_]) return ;
bel[u_] = circle;
Dfs(a[u_]);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int T = read();
while (T --) {
n = read();
circle = 0, ans = n;
for (int i = 1; i <= n; ++ i) a[i] = read(), bel[i] = 0;
for (int i = 1; i <= n; ++ i) {
if (!bel[i]) {
++ circle;
Dfs(i);
}
}
for (int i = 1; i < n; ++ i) {
if (bel[i] == bel[i + 1]) {
ans = std::min(ans, n - circle - 1);
} else {
ans = std::min(ans, n - circle + 1);
}
}
printf("%d\n", ans);
}
return 0;
}
E
对于一个长度为 \(3 n\) 的排列 \(p\),定义两种操作:
- 将排列的前 \(2 n\) 个元素升序排序。
- 将排列的后 \(2 n\) 个元素升序排序。
容易证明对于所有的排列,总存在一种操作的方案使排列变为有序的。记 \(f(p)\) 为使得排列 \(p\) 变为有序的最少的操作次数。给定两个参数 \(n,M\),对于所有长度为 \(3 n\) 的排列 \(p\),求 \(f(p)\) 的和对 \(M\) 取模的值。
\(1\le n\le 10^6, 10^8\le M\le 10^9\),保证 \(M\) 为质数。
1.5S,1024MB。
大力容斥捏。
手玩几组可以发现,至多进行三次操作:\(1\rightarrow 2\rightarrow 1\) 即可使得排列有序。正确性显然,经过操作 \(1\) 后即可使所有 \(>2n\) 的元素均位于 \([n+1,3n]\) 中,经过操作 \(2\) 后即可使 \([2n+1,3n]\) 有序,再来一次操作 \(1\) 即可。
之后分类讨论 \(f(p)\) 的取值:
- \(f(p)=0\),\(p\) 为有序排列。
- \(f(p)=1\),则 \(p\) 中 \([1,n]\) 有序,或者 \([2n+1, 3n]\) 有序。
玩仁王去了,先摸了。
写在最后
- 没思路就手玩是很好很好的习惯。
- 转化为图论模型不一定是有非常显然的点连向点的性质,有与图相似的结构都可以尝试往图论模型上靠。比如:上一场的两个元素只能选一个,考虑有向边;这一场的环形结构……
- 组合数学做就行了,怕个卵。
- 我已经等不及要玩仁王了。
- 我已经等不及要玩仁王了
- 最后就是我已经等不及要玩仁王了————