Favorite Permutation (USACO Bronze 2024 US Open)
1-N的数字组成的排列,每次删除头尾中较大的数,并写下第二个(删除头时),或者倒数第二个数(删除尾时)。要求通过写下的数,来恢复原来的排列。
我们可以做一些系列观察:
- 一个写下来的数字,要不就是左边的数被删了,要不就是右边的数字被删了,而一个方向的邻居最多只能被删除一次,所以一个数字最多出现两次。
- 因为每次都删大的,永远删不到1,因此最后一个写下来的数字一定是1。
- 可以看到,其实是固定住1的位置之后,一会删左边,一会删右边,直到最后写下1结束。这样我们就会发现上面第一个规律还不够准确, 实际情况就是只有1会出现两次,其它数字最多出现一次。
- 没有出现过的数字,如果有两个,那么就是最边上的数字。对应的,此时1会出现两次。
- 如果1只出现了一次,那么1一定在边上,另外一边就是没有出现的数字。
到这里,我们就可以得到一个恢复原有排列的算法了:
- 通过没有出现的数字,确定原排列中最左最右的数字:如果两个没出现,那就一左一右,如果一个数字没出现,那就是这个数字和1是左右两侧的数字。
- 因为结果需要输出字典序最小的结果,所以我们把小的数字放左边。
- 然后我们会删除左右两边数字中较大的(第一步一定是右边),这时就可以把写下来的数字填入对应位置。
- 重复这个过程,就可以一步步恢复出整个数字序列了。
#include <cstdio>
#include <algorithm>
#include <functional>
#include <vector>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
int T, n;
int a[100005], b[100005];
bool f[100005];
int main() {
scanf("%d", &T);
for (int t = 0; t < T; t++) {
int ones = 0;
bool valid = true;
memset(f, 0, sizeof(f));
scanf("%d", &n);
for (int i = 0; i < n-1; i++) {
scanf("%d", &a[i]);
if (a[i] != 1 && f[a[i]]) // non-1 num can appear at most once
valid = false;
f[a[i]] = true;
if (a[i] == 1)
ones++;
}
if (a[n-2] != 1) valid = false; // the last num is always 1
if (ones > 2) valid = false; // 1 can appear at most twice
if (!valid) {
printf("-1\n");
continue;
}
vector<int> missing;
for (int i = 1; i <= n; i++)
if (!f[i])
missing.push_back(i);
int l = 0, r = n-1;
if (ones == 1) { // fill in outer-most nums
b[0] = 1;
b[n-1] = missing[0];
} else {
b[0] = min(missing[0], missing[1]);
b[n-1] = max(missing[0], missing[1]);
}
for (int i = 0; i < n-1; i++) { // fill in remaining nums one by one
if (b[l] > b[r]) {
b[l+1] = a[i];
l++;
} else {
b[r-1] = a[i];
r--;
}
}
for (int i = 0; i < n; i++)
printf("%d ", b[i]);
printf("\n");
}
return 0;
}