20200810 T2 Dispatch Money【区间逆序对,分治套分治解决决策单调性】

题目描述

给一个长度为 n n n 的排列 a i a_i ai,把它划分为若干段,每划分一段有 X X X 的代价,划分完后段内需要排序,代价为区间逆序对个数。求最小总代价。

n ≤ 300000 , 1 ≤ X ≤ 1 0 9 n\le 300000,1\le X\le 10^9 n300000,1X109

时限 5s.

题目分析

暴力 O ( n 2 ) O(n^2) O(n2) DP: f i = f j + i n v e r s i o n _ p a i r ( j + 1 , i ) f_i=f_j+inversion\_pair(j+1,i) fi=fj+inversion_pair(j+1,i)

经验丰富的OI选手可以立马猜测决策单调性,证明也很简单,代价函数在区间越大的情况下增长越快,当在 i i i 处决策点 k k k 比决策点 j j j 劣时( k < j kk<j),在 i ′ > i i'>i i>i 处也必然比后者劣。

然而二分维护栈的做法需要在线求出区间逆序对个数,这是个 n n n\sqrt n nn 的问题,还需要乘个 log ⁡ \log log,300000显然跑不动。

而分治做法则可以与莫队类似,用树状数组维护有多少个数小于 x x x,当决策点移动时添加/删除区间前端/后端的点,根据分治的过程可以看出这样区间移动的次数是 O ( n log ⁡ n ) O(n\log n) O(nlogn) 级别的。

但是分治要建立在决策点区间的 f f f 都已知的前提下,于是需要在外层再套一个 cdq 分治。
总复杂度 O ( n log ⁡ 3 n ) O(n\log^3n) O(nlog3n),但是常数很小。

如果要严格写,每次撤销略麻烦,经过实际测试不如直接用类似莫队的写法快,即如果当前区间与询问区间不符则移动,非常好写。

Code:

#include
#define maxn 300005
#define LL long long
using namespace std;
const LL inf = 0x3f3f3f3f3f3f3f3fll;
int n,X,a[maxn];
LL f[maxn];
int arr[maxn];
void upd(int i,int v){
     for(;i<=n;i+=i&-i) arr[i]+=v;}
int qsum(int i){
     int s=0;for(;i;i-=i&-i) s+=arr[i];return s;}
LL ask(int x,int y){
     
	static int l=1,r=0; static LL inver=0;
	while(l>x) l--,inver+=qsum(a[l]),upd(a[l],1);
	while(r<y) r++,inver+=r-l-qsum(a[r]),upd(a[r],1);
	while(l<x) upd(a[l],-1),inver-=qsum(a[l]),l++;
	while(r>y) upd(a[r],-1),inver-=r-l-qsum(a[r]),r--;
	return inver;
}
void solve2(int l,int r,int ql,int qr){
     
	if(ql>qr) return;
	int mid=(ql+qr)>>1,p=-1; LL mn=inf,tmp;
	for(int i=l;i<=r&&i<mid;i++)
		if((tmp=f[i]+ask(i+1,mid))<mn) mn=tmp,p=i;
	f[mid]=min(f[mid],mn+X);
	solve2(l,p,ql,mid-1),solve2(p,r,mid+1,qr);
}
void solve1(int l,int r){
     
	if(l==r) return;
	int mid=(l+r)>>1;
	solve1(l,mid);
	solve2(l,mid,mid+1,r);
	solve1(mid+1,r);
}
int main()
{
     
	freopen("dispatch.in","r",stdin);
	freopen("dispatch.out","w",stdout);
	scanf("%d%d",&n,&X);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),f[i]=inf;
	solve1(0,n);
	printf("%lld\n",f[n]);
}

要卡常的话可以尝试把 upd 函数写成 ++ 和 --,以及在寻找决策点的循环根据端点位置判断一下顺着循环还是反着循环。

还有的优化是在 solve1 区间小于分块大小 S S S 时暴力做,预处理区间长度 ≤ S \le S S 的逆序对数,这个可以 O ( n S ) O(nS) O(nS) 预处理, w i , j = w i , j − 1 + w i + 1 , j − w i + 1 , j − 1 + [ a i > a j ] w_{i,j}=w_{i,j-1}+w_{i+1,j}-w_{i+1,j-1}+[a_i>a_j] wi,j=wi,j1+wi+1,jwi+1,j1+[ai>aj]

扩展

O ( n n ) O(n\sqrt n) O(nn ) 求区间逆序对个数。

离线做法:
莫队,考虑移动端点的时候不使用树状数组,而改为查询区间小于 x x x 的数的个数。先跑一遍莫队,得到所有这样 O ( n n ) O(n\sqrt n) O(nn ) 个询问。
差分一下就变成了查询一个前缀小于 x x x 的数的个数,对值域分块,相当于一个块前缀和一个块内前缀,从前往后扫描,每次加入一个数后 O ( n ) O(\sqrt n) O(n ) 修改,查询就是 O ( 1 ) O(1) O(1) 的。

在线做法:
详见 WerKeyTom_FTD的博客
大致思路是预处理块内贡献,块间贡献,需要算两个小区间之间的贡献时通过块内预先排好序的数组找到区间内排序后的情况,然后归并。

时空复杂度都是 O ( n n ) O(n\sqrt n) O(nn ) 的。

你可能感兴趣的:(分治(二分),DP优化)