题目大意:给 \(n\) 个数的排列(互不相同),一次操作可以选择一个子序列,将子序列按照原来顺序移动至最前,其余元素按照原来顺序放在最后。问最小操作次数和一种可行的操作方案
input:
6
6 5 2 4 1 3
output:
3
6 5 2 4 1 3
6 4 1 5 2 3
6 1 2 3 4 5
1 2 3 4 5 6
一开始看的是 CTSC 2008 曹钦翔《数据结构的提炼与压缩》这篇论文。结果按照他的算法写,一直wa,花了不少时间,太气人了(也可能是我的问题因为我没看论文里的证明)。最后在discuss里才看到正解
网上似乎没什么题解,那就我来写一个吧
算法如下:
上述算法每一步操作是 \(O(n\log n)\) 的,足以过这题。当然事实上,先记录一下 \(pos[a[i]]=i\),即 \(i\) 在 \(a[1..n]\) 中的位置。如果 \(pos[i]>pos[i+1]\) 表示数字 \(i\) 在数字 \(i+1\) 的后面,我们就在 \(i\) 和 \(i+1\) 中间断开。这样就直接得到第 3 步得到的有序分割,可以 \(O(n)\) 处理每一步操作
又由于操作数为 \(O(\log n)\),总复杂度 \(O(n\log n)\)
那么为什么这么做是正确的呢?或者说怎么证明操作数 \(O(\log n)\)?感性地来说,就是第 3 步得到的有序分割(比如 \([1][2,3][4][5][6]\)),因为所有奇数位将会排到偶数位之前,所以可以看作奇数位和它后面相邻的子序列进行了合并。比如 \([1]\) 一定排到 \([2,3]\) 之前,那么在下次操作后 \([1,2,3]\) 一定是作为一个子序列出现的,\([4]\) 和 \([5]\) 同理。这样,有序分割的大小就会小一半。这个操作看似是归并排序反过来的操作,但是本质和归并排序是一样的思想,真是神奇
#include <bits/stdc++.h>
using namespace std;
#define repeat(i,a,b) for(int i=(a),_=(b);i<_;i++)
#define repeat_back(i,a,b) for(int i=(b)-1,_=(a);i>=_;i--)
const int N=200010; typedef long long ll; ll read(){ll x; if(scanf("%lld",&x)!=1)exit(0); return x;}
int n,a[N],b[N],f[N],A[N],pos[N];
// f[i]标记了a[i]是有序分割中第几个子序列的元素,pos含义见题解
void print(){
repeat(i,1,n+1)
printf("%d%c",a[i]," \n"[i==n]);
}
int getcnt(){ // 得到有序分割的大小
repeat(i,1,n+1)pos[a[i]]=i;
int cnt=1;
repeat(i,1,n+1){
if(i>1 && pos[i-1]>pos[i])cnt++;
f[pos[i]]=cnt;
}
return cnt;
}
void work(){ // 进行一次操作
getcnt();
int t=0;
repeat(i,1,n+1)if(f[i]%2==1)b[++t]=a[i];
repeat(i,1,n+1)if(f[i]%2==0)b[++t]=a[i];
copy(b+1,b+n+1,a+1);
}
void Solve(){
n=read();
repeat(i,1,n+1){
A[i]=a[i]=read();
}
int ans=0;
while(getcnt()!=1){
work();
ans++;
}
copy(A+1,A+n+1,a+1);
printf("%d\n",ans);
print();
repeat(i,0,ans){
work();
print();
}
}
signed main(){
//freopen("data.txt","r",stdin);
int T=1; //T=read();
repeat(ca,1,T+1){
Solve();
}
return 0;
}
ural 1568 Train Car Sorting 题解
原文:https://www.cnblogs.com/axiomofchoice/p/14263859.html