Codeforces 1269E. K Integers(逆序对,树状数组+二分)

题目链接:http://codeforces.com/contest/1269/problem/E

Codeforces 1269E. K Integers

题目大意

给定一个1到n的排列,定义一个移动操作为:交换相邻的两个元素。

现在定义一个函数f(x),表示在原排列中,通过交换操作,形成一个1,2,3....x的排列的子串,需要的最小操作步骤。

子串意味着这个排列必须是相邻的。

现在你需要求出f(1),f(2),f(3)......f(n)。

思路

我们考虑相邻的情况。

ans{1 2 3}=0

ans{1 3 2}=1;

ans{2 1 3}=1;

ans{3 2 1}=3;

ans{3 1 2}=2;

ans{4 1 2 3}=3;

。。。

我们发现,在元素相邻的情况下,答案一定是与逆序对有关的(因为最后排列不存在逆序对),并且每一次操作都可以消除一对逆序对,所以,相邻的答案就是逆序对的个数

现在考虑不相邻的情况,如何移动才能使操作步数最少呢?答案应该是先将i个元素移动到一块儿,变成相邻的情况,再交换逆序对,这样才是最优的,因为这样操作,对于中间的每个不是当前排列的元素,他只会越来越往外面走,而不是混在i个元素中,造成操作的浪费。

所以思路就明确了起来:

首先我们将所有元素移动到一块儿,移动到哪儿合适呢?位置mid应该是左右两边的元素个数尽量相等,这样我们就可以二分中间位置mid了;

其次,说到逆序对,就需要用到树状数组,将元素插入到pos[i]的位置,这样就可以O(logn)的计算当前i元素造成的逆序对个数了(这样就可以累加);

然后,我们需要计算将元素尽量向中间点mid移动的花费,我们可以分为两部分:mid左边的部分,设个数为cnt(可以用树状数组O(log)求),假设cnt=2,如下图:

Codeforces 1269E. K Integers(逆序对,树状数组+二分)_第1张图片

pos2移动到mid,需要花费mid-pos2;pos1移动到mid-1,需要花费mid-pos1-1;

如果还有pos3,pos4,花费也是同样计算的。假设位置pos之和为sum,花费为

ans1+=mid*cnt-sum-cnt*(cnt-1)/2

这就是左边的花费。

右边的同理,尽量往mid+1这个位置移动,假设右边的所有位置之和为sum,个数为cnt,花费为

ans1+=sum-cnt*(mid+1)-cnt*(cnt-1)/2

最后,别忘了逆序对的花费,逆序对可以O(n)递推,每次求的复杂度为O(logn).

最后复杂度大概是O(nlog^2n)。

具体细节可以看代码:

#include 
#define int long long
using namespace std;

const int maxn=2e5+10;
int sum1[maxn];//数的个数
int sum2[maxn];//数的下标之和
int n;
int lowbit(int x){
    return x&-x;
}
void add(int *sum,int x,int v){
    while(x<=n){
        sum[x]+=v;
        x+=lowbit(x);
    }
}
int query(int *sum,int x){
    int res=0;
    while(x>0){
        res+=sum[x];
        x-=lowbit(x);
    }
    return res;
}
int a[maxn];
int pos[maxn];
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        pos[a[i]]=i;
    }
    int ans1=0;//逆序对的和
    for(int i=1;i<=n;i++){
        ans1+=i-1-query(sum1,pos[i]);//查询在i之前且比i小的数,逆序对
        //将i插入树状数组
        add(sum1,pos[i],1);
        add(sum2,pos[i],pos[i]);
        int mid,l=1,r=n;//二分需要靠拢的最中间的位置
        while(l<=r){
            mid=(l+r)>>1;
            if(query(sum1,mid)*2<=i){
                l=mid+1;
            }
            else{
                r=mid-1;
            }
        }
        //printf("debug %lld\n",mid);
        //printf("debug %lld\n",ans1);
        int ans2=0;
        //将mid左边的数靠拢到mid附近的花费
        int cnt=query(sum1,mid),sum=query(sum2,mid);
        ans2+=mid*cnt-sum-cnt*(cnt-1)/2;
        //将mid右边的数靠拢到mid附近的花费
        cnt=i-cnt,sum=query(sum2,n)-sum;
        ans2+=sum-cnt*(mid+1)-cnt*(cnt-1)/2;
        printf("%lld ",ans1+ans2);
    }
    puts("");
    return 0;
}

 

你可能感兴趣的:(补题,思维题,线段树)