题目
测试链接:暂无。
题目来源:模拟赛 T2。
题目背景
小 G 喜欢玩排列。
题目描述
现在他手头有两个 \(n\) 的排列。\(n\) 的排列是由 \(0,1,2,\cdots ,n-1\) 这 \(n\) 个数字组成的。对于一个排列 \(p\),\(\operatorname{Order}(p)\) 表示 \(p\) 是字典序第 \(\operatorname{Order}(p)\) 小的排列(从 \(0\) 开始计数)。对于小于 \(n!\) 的非负数 \(x\),\(\operatorname{Perm}(x)\) 表示字典序第 \(x\) 小的排列。
现在,小 G 想求一下他手头两个排列的和。两个排列 \(p\) 和 \(q\) 的和为 \(sum=\operatorname{Perm}((\operatorname{Order}(p)+\operatorname{Order}(q))\bmod{n!})\)
输入格式
输入文件第一行一个数字 \(n\),含义如题。
接下来两行,每行 \(n\) 个用空格隔开的数字,表示小 G 手头的两个排列。
输出格式
输出一行 \(n\) 个数,用空格隔开,表示两个排列的和。
评测限制 and 数据范围
评测时间限制 \(1000\ \textrm{ms}\),空间限制 \(128\ \textrm{MiB}\)。
- 对于 \(40\%\) 的数据,\(1\le n\le 10\);
- 对于另外 \(30\%\) 的数据,保证第二个排列 \(q\) 满足 \(\operatorname{Order}(q)\le 10^5\);
- 对于 \(100\%\) 的数据,\(1\le n\le 5\times 10^3\)。
分析
这道题的数据范围十分具有迷惑性,我们可能需要一点数学基础才能避开这个坑。
\(40\ \texttt{pts}\)
这一档分的特点就是 \(n!\) 比较小,我们可以利用这一点做康托展开。
先展开以后再做加法,就像题目中说的一样。
复杂度 \(\Theta(n!)\)。
Other \(30\ \texttt{pts}\)
可以对其中一个进行康托展开,另一个利用其展开的结果做 next_permutation
即可。
复杂度:\(\Theta(\operatorname{Order}(q))\)
\(100\ \texttt{pts}\)
注意到本题的难点是无法直接完全康托展开,否则比较难运算。
那么,我们可不可以保留康托展开的部分,而直接进行高精度运算呢?是可以的。
我们来感性理解一下康托展开:
- 一个排列的字典序可以由一个表达式决定:
\[\huge{\sum\limits_{i=0}^{n-1} i!\times p_i\text{在剩余部分的排名}} \]
- 一个排列对应着一个 \(\left< n,n-1,n-2,\ldots ,2,1\right>\) 进制的数(不妨称其为 \(\operatorname{Order2}(p)\)),即第 \(i\) 位 满 \(i\) 进一。
就像 \(p=\left< 1,4,2,3\right>\) 对应 \(\left< 0,2,0,0\right>\) 一样。这个数就对应着 \(\operatorname{Order}(p)\)。
注意到 \(\operatorname{Order2}(p)\) 也是一个高精度数,那么我们是不是可以直接对其进行加减法呢?答案是肯定的。这就是正解。
\(\mathcal{E}xtra\ score\)
当然,\(n\le 5000\) 绝对不是这道题的极限。我们可以将这道题的数据继续往上拉。
为什么复杂度是 \(\Theta(n^2)\) 呢?实际上是因为康托展开的速度很慢。每一次重新计算排名就要重新赋值。
就是注释那里:
for (int i = 0; i < n; i++)
rk[i] = i;
for (int i = 0; i < n; i++)
{
ptr[i] = rk[tmp];
for (int j = tmp + 1; j < n; j++) // Here!!!!!!!
rk[j]--;
}
显然,我们可以使用一个线段树对其进行优化,可以进一步降低复杂度至 \(\Theta(n\log n)\)。当然,这样可能也并没有太多必要(总之变得更毒瘤了)。
Code
这里使用的还是复杂度为 \(\Theta(n^2)\) 普通方法,有兴趣的可以试试看线段树优化。
#include <cstdio>
#include <cstring>
using namespace std;
const int max_n = 5000;
int na[max_n], nb[max_n], rk[max_n];
void make_num(int n, int *ptr)
{
int tmp;
for (int i = 0; i < n; i++)
rk[i] = i;
for (int i = 0; i < n; i++)
{
scanf("%d", &tmp);
ptr[i] = rk[tmp];
for (int j = tmp + 1; j < n; j++)
rk[j]--;
}
}
int main()
{
int n, car = 0;
scanf("%d", &n);
make_num(n, na);
make_num(n, nb);
for (int i = n - 1; i >= 0; i--)
{
na[i] += nb[i] + car;
if (na[i] >= n - i)
na[i] -= n - i, car = 1;
else
car = 0;
}
for (int i = 0; i < n; i++)
rk[i] = i;
for (int i = 0; i < n; i++)
{
printf("%d ", rk[na[i]]);
for (int j = na[i] + 1; j < n; j++)
rk[j-1] = rk[j];
}
putchar('\n');
return 0;
}
后记
这道题是一道很有意思的题,将一个排列问题变成了一道高精度模板。如果可以的话还可以加上线段树优化,但会让很多人感到很难受。
实际上我们也很有必要来想一想出题人为什么没有加上线段树这一档分。其实是为了题目的连续性。
一道好题不应该是拼凑的,而要有一气呵成的感觉。否则这道题就不会有人愿意做。