CF2073K Book Sorting 题解
题意:
给出一个长为 \(n\) 的排列。每一次可以选择以下的任一操作进行:
- 交换相邻的两个数
- 将排列中一个数挪到序列开头
- 将排列中一个数挪到序列结尾
求使得排列有序的最小总操作次数。
\(n\le 5\times 10^5\)
首先,我们可以发现, 2、3 操作是不受排列的影响的,也就是无论排列长什么样都可以对任意数进行 2、3 操作。
而且每个数至多会进行一次 2 或 3 操作(2,3 里选一个)。因此我们可以考虑对于哪些数进行 2 操作、哪些数进行 3 操作。
我们发现,如果我们选定了哪些数进行 2 操作,那么我们一定可以将这些数 排好序 并放在开头。因为进行 2 操作的顺序任意。
所以有一个贪心策略:
我们会选定 \((x,y)\) ,对于 \(a_i\le x\) 的 \(i\) 位置进行 2 操作,对于 \(a_j\ge y\) 的 \(j\) 位置进行 3 操作。
先把所有 2,3 操作执行完,使得选中的那些数有序。然后对于剩下的数,用 1 操作解决。
可以发现这样一定是最优的。
那么现在问题在于,如何找出 \((x,y)\) 满足 \(x+(n-y+1)+inv(x+1,y+1)\) 的值最小。其中 \(inv(l,r)\) 表示值在 \([l,r]\) 的数之间的逆序对个数。
那么暴力做法可以 \(O(n^2)\) 求。如何优化?
因为值在 \([l,r]\) 的数太过于分散,不好计算逆序对,所以考虑转变为在值域上计算逆序对。
怎么做呢?设原序列为 \(a\),我们建立序列 \(id\),满足 \(id_{a_i}=i\)。
你会发现原序列中是逆序对的位置在 \(id\) 序列中也是逆序对。而且值在 \([l,r]\) 之间的 \(a\) 的下标都在 \(id\) 数组的 \([l,r]\) 内。
于是我们就将 \(a_i\in[l,r]\) 的 \(i\) 之间的逆序对转化为了 \(id\) 中在 \([l,r]\) 的逆序对。
所以设 \(inv'(l,r)\) 表示 \(id\) 数组中 \([l,r]\) 的逆序对个数。当 \(l>r\) 时, \(inv'(l,r)=0\)。
则选 \((x,y)\) 的权值为 \(x+(n-y+1)+inv'(x+1,y-1)\)。
我们发现, \((i,j)\) 的权值满足四边形不等式。
证明:
设有 \(i,j\in[1,n]\) 满足 \(i+1\le j-1\)。
要证 \((i,j)+(i+1,j-1)\ge (i+1,j)+(i,j-1)\)。
只要证 \((i,j)-(i,j-1)\ge (i+1,j)-(i+1,j-1)\)
将其拆开,只要证
\[[i+(n-j+1)+inv'(i,j)]-[i+(n-j+2)+inv'(i,j-1)]\\ \ge \\ [i+1+(n-j+1)+inv'(i+1,j)]-[i+1+(n-j+2)+inv'(i+1,j-1)] \]发现两边有很多可以消掉,只要证
\[inv'(i,j)-inv'(i,j-1)\ge inv'(i+1,j)-inv'(i+1,j-1) \]二者实际上在比较 \(j\) 在 \(inv'(i,j)\) 里贡献大还是 \(inv'(i+1,j)\) 里贡献大。因为 \(inv'(i,j)\) 为 \([l,r]\) 逆序对个数,而 \(id_i\) 可能大于 \(id_j\),贡献可能多 \(1\),因此前者是 \(\ge\) 后者的。
所以综上得证。
于是对于 \(x\) 来说,最优的 \(y\) 具有决策单调性,也就是当 \(x\) 递增时,最优的 \(y\) 单调不降。
于是就可以用分治去做。但是过程中我们需要求一段区间的逆序对个数,也就是需要快速求 \(inv'(l,r)\)。
神犇 @JDScript0117 的解法是用主席树维护。
但得益于主席树的常数,我们还有更快的做法,就是用莫队+树状数组去动态维护。可以发现,左右端点的移动次数都不超过 \(O(n\log n)\) 次,开两棵树状数组,一棵维护前缀和,一棵维护后缀和。
简略证明莫队指针挪动次数在 \(O(n\log n)\):
先考虑左端点,每一次的左端点都是 \(mid+1\),所以在分治过程中移动次数在 \(O(n\log n)\)。
右端点的证明过程有点类似,在计算 \(x=mid\) 时,设决策区间为 \([L,R]\) ,则在这一层中,右端点的挪动次数是 \(O(R-L)\) 的,所以右端点移动总次数可以近似所有决策区间长度之和。所有决策区间之和为 \(O(n\log n)\)。因此右端点移动次数为 \(O(n\log n)\)。
于是就可以做到 \(O(n\log ^2n)\) 将这道题做完了。
主席树跑了 2000ms 左右,但是莫队只跑了 1000ms \(\sim\) 1300ms。
代码:
#include<bits/stdc++.h>
#include<cstring>
using namespace std;
const int N=1e6+5;
#define int long long
#define lowbit(x) (x&(-x))
int sum[N],n,a[N],id[N],ans=1e18,ql=1,qr=0,now,sum2[N];
//sum,sum1分别是维护前、后缀和的树状数组
//ql,qr 是莫队的指针 ,now为当前区间 [ql,qr] 的逆序对个数
int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
void update(int pos,int w)
{
for(int i=pos;i>0;i-=lowbit(i)) sum[i]+=w;
for(int i=pos;i<=n;i+=lowbit(i)) sum2[i]+=w;
}
int query1(int pos)
{
int res=0;
for(int i=pos;i<=n;i+=lowbit(i)) res+=sum[i];
return res;
}
int query2(int pos)
{
int res=0;
for(int i=pos;i>0;i-=lowbit(i)) res+=sum2[i];
return res;
}
void get(int l,int r)//莫队
{
if(qr<l||ql>r)//如果完全不在区间内,还不如直接清空,跳到目标区间去
{
for(int i=ql;i<=qr;i++) update(id[i],-1);
now=0;
for(int i=l;i<=r;i++) now+=query1(id[i]),update(id[i],1);
ql=l,qr=r;
return;
}
while(qr>r) update(id[qr],-1),now-=query1(id[qr]),qr--;
while(qr<r) qr++,now+=query1(id[qr]),update(id[qr],1);
while(ql>l) ql--,now+=query2(id[ql]),update(id[ql],1);
while(ql<l) update(id[ql],-1),now-=query2(id[ql]),ql++;
}
void solve(int l,int r,int L,int R)//分治
{//表示当 x 在 [l,r] 内时,最优的 y 在 [L,R] 内
if(l>r) return;
int mid=(l+r)>>1,res=n,k=mid;
for(int i=max(mid+1,L);i<=R;i++)//直接求最优决策点
{
get(mid+1,i);
int w=now+mid+(n-i);
if(w<res) k=i,res=w;
}
ans=min(ans,res);
solve(l,mid-1,L,k);//向下递归
solve(mid+1,r,k,R);
}
signed main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),id[a[i]]=i;
solve(0,n,1,n);
printf("%d\n",ans);
return 0;
}

浙公网安备 33010602011771号