题解 CF1601C【Optimal Insertion】

发布时间 2023-07-25 16:37:53作者: caijianhong

特别鸣谢:https://www.luogu.com.cn/blog/484784/cf1601c#

problem

两个数组 \(a,b\) 长度分别为 \(n,m\)。将 \(b\) 的所有元素以任意顺序插入 \(a\) 的任意位置,使最终序列逆序对数量最小,并输出这个值。\(n,m\leq 10^6\)

solution

\(b\) 明显是排序成不降的最优,\(a\) 原来的逆序对个数无法改变,我们只用算 \(a~b\) 的逆序对。考虑将 \(a\) 按顺序插入排序后的 \(b\)

考虑一个简化版的问题,\(a_i\) 全偶数,\(b_i\) 全奇数,合起来是 \(n+m\) 的排列。设 \(last\) 为当前插入的 \(a_i\) 的位置最大的那一个(现在可以认为是上一次插入),现在插入 \(a_i\),如果 \(last<a_i\) 我们直接将 \(a_i\) 插入到使得它不产生逆序对的地方即可。但如果 \(last>a_i\),我们首先使得 \(a_i\) 插入到 \(last\) 后面,然后算一下现在 \(a_i\) 产生的逆序对个数(在这个简化版问题中是 \(last-a_i\))。但是呢如果我们就这样啥也不管,答案会越来越大;然而我们本来能通过调整前面插入位置使得 \(last\) 降低来让后面的 \(a_i\) 零贡献的;所以我们要在插入的时候尽可能减少 \(last\)。这就引出两个问题:

  1. 如果在左移 \(last\) 的过程中撞到其它已经插入的 \(a_i\),需要连带着它们一起移动吗?
  2. 左移 \(last\) 所增加的贡献,和后面插入产生的贡献,哪个更优?

为了解决这两个问题有人就开始用各种数据结构维护反悔贪心了。但是不需要。我们考虑第二个问题:将 \(last,a_i\) 绑在一起,左移到 \(a_i\) 的零贡献位置,在这个过程中 \(last\) 每一步都产生 \(1\) 个逆序对贡献,但是 \(a_i\) 同时有一个逆序对消失,这样的贡献为 \(0\),移动它们是没问题的。(同时保证了 \(last\) 在移动的时候一定是加贡献,形成循环论证)但对于第一个问题,我们有一个很厉害的想法:维护一个堆表示现在插入的数的位置。我移动 \(last\)\(a_i\),其实是假移动,你有需要的时候我再过去,我没有需要就认为它还在那里,每次取的 \(last\) 是这个堆中的最大值(而不是上一次插入了),将 \(last\)\(a_i\) 捆绑进行假移动。注意到当 \(last\) 后面的东西全部取完,就是 \(last\) 上一次假移动产生的贡献已经全部搞完,\(last\) 才会被第二次取出继续假移动,这样就正确了。

priority_queue<int> q;
q.push(a[1]);
for(int i=2;i<=(n/2);++i){
    if(q.top()>a[i]) ans+=q.top()-a[i],q.pop(),q.push(a[i]);
    q.push(a[i]);
}

现在回到我们的原题,发现一模一样就是要加一点边界,具体展开一下:我们必须保证我们取出的 \(last\) 左移的时候是要加贡献的(否则失去性质),于是在外面 q.push(a[i]) 时就必须让它飞到等于 \(a_i\) 的值前面,不妨认为 \(last\) 表示这个值插在 \(last\) 的前面。那么我们重复一遍算法过程,如果 \(last>a_i\),首先取出一个左移加贡献的 \(last\),然后 \(last,a_i\) 捆绑假左移,移动到第一个等于 \(a_i\) 的值时不再减贡献,然后 \(last\) 停下来进入堆,\(a_i\) 继续假左移移动到等于 \(a_i\) 的值前面,再进入堆。结束了!

Code

点击查看代码
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr,##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
typedef long long LL;
int n,m,b[1<<20],a[1<<20];
LL inversion(int l,int r){
	if(l==r) return 0;
	int mid=(l+r)>>1;
	LL ans=inversion(l,mid)+inversion(mid+1,r);
	static int tmp[1<<20]; int cnt=l;
	for(int i=l,j=mid+1;i<=mid||j<=r;){
		if(i<=mid&&(j>r||a[i]<=a[j])) tmp[cnt++]=a[i++];
		else ans+=mid-i+1,tmp[cnt++]=a[j++];
	}
	for(int i=l;i<=r;i++) a[i]=tmp[i];
	return ans;
}
int mian(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=m;i++) scanf("%d",&b[i]);
	sort(b+1,b+m+1);
	LL ans=0;
	priority_queue<int> q;
	for(int i=1;i<=n;i++){
		int x=upper_bound(b+1,b+m+1,a[i])-b,y=lower_bound(b+1,b+m+1,a[i])-b;
		if(!q.empty()&&q.top()>x) ans+=q.top()-x,q.pop(),q.push(x);
		q.push(y);
	}
	debug("ans=%lld\n",ans);
	printf("%lld\n",ans+inversion(1,n));
	return 0;
}
int main(){
	int Q; scanf("%d",&Q);
	while(Q--) mian();
	return 0;
}