Codeforces Round #609 (div2) E. K Integers

题目大意是 : 给你 1~n 的排列, 你每次都可以交换相邻位置的数, 问对于每一个i (i = 1, 2 ... n) 要得到有序的1~i的连续排列所需要交换的最少次数分别是多少?

输入 : n ( n <= 2e5), 接下来一行n个数代表这个排列

输出 : 输出n个数, 分别表示要得到连续有序的1~i (i = 1, 2 ... n) 的排列需要最少的交换次数

样例输入

5

5 4 3 2 1

样例输出:

0 1 3 6 10

想法 :

  这题我在尝试乱搞后果断放弃, 然后去研究那些红黑名大佬的代码, 研究了一个小时终于差不多看懂了 ( 留下了菜鸡的泪水- -  以下均为个人理解, 如有不对, 欢迎指正~

  首先我们考虑i = n 的情况, 使这个排列变成有序的排列所需要的最少交换次数 (也即冒泡排序的最小交换次数) 就是这个排列的逆序对数, 比如样例输入的5 4 3 2 1要有序则需要交换最少 1+2+3+4 = 10次. 而求解逆序对可以用树状数组, 而且树状数组可以把每一个1~i的排列的局部逆序对都求出来.

  对于i < n的情况, 就没那么好直接求了 比如2 4 1 5 3, 要求1~3连续有序的最少交换次数就还要把位于1~3之间的4和5也考虑进来, 但我们依然可以把它转化成上面那种i = n的情况来做: 说的直接点就是把位于 1~i 之间的大于 i 的数都给挤出去, 让 1~i 彼此相邻 ( 此时它们的相对顺序没变 ) , 然后就可以转成上述 i = n的情况了, 答案就是 [把位于 1~i 之间的大于 i 的数都给挤出去所需要的最少交换次数] + [1~i (还是原来的相对顺序)排列的逆序对的个数].

  就以2 4 1 5 3 为例, 要求1~3连续有序的最少交换次数. 首先按上面那个步骤来, 把4 和 5 挤出去, 但怎么挤出去交换次数最少呢? 我们很容易想到, 把2和3都往中间挤, 4 和 5被挤出去的交换次数最少, 也即2次, 此时排列变为 4 2 1 3 5, 然后只需要看2 1 3了,  这个排列逆序对为1, 所以1~3的连续有序的最少交换次数就是2+1 = 3 次. 所以整个过程描述起来就是, 对于每个 i , 求出排列1~i 的局部逆序对(也即不包含比i大的数的逆序对), 二分树状数组找到1~i 这个排列的中间位置( 同样是不包含比i大的数), 然后分别计算中间位置以及中间位置前的每个<= i 的数往中间位置挤所需要的交换次数和中间位置后每个<= i 的数往中间位置挤需要的交换次数, 这个怎么算呢? 可以用每个数和中间位置的 (位置差-1) 来表示需要的交换次数, 这里可以用另外一个树状数组, 记录每个 <=i 的位置前缀和来实现, 具体可以看代码.

#include <bits/stdc++.h> 

using namespace std;

typedef long long ll;
const int maxn = 2e5+10;

int num[maxn];
ll cnt_sum[maxn], pos_sum[maxn]; // sum(cnt_sum,i) 表示前i个位置中已经出现了多少个比当前数要小的数的个数
int n;                // sum(pos_sum,i) 表示前i个位置中所有比当前数小的位置和

inline lowbit(int x) { return x & -x;  }

void add(ll* bit,int pos,int x) {
    while(pos <= n) {
        bit[pos] += x;
        pos += lowbit(pos);
    }
}

ll sum(const ll* bit,int pos) {
    ll ans = 0;
    while(pos > 0) {
        ans += bit[pos];
        pos -= lowbit(pos);
    }
    return ans;
}

int pos[maxn]; // 记录每个数的位置

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    for(int i=1;i<=n;++i) {
        cin >> num[i];
        pos[num[i]] = i;
    }       
    ll inv = 0;
    for(int i=1;i<=n;++i) {
        inv += i-1-sum(cnt_sum,pos[i]); // 1~i这个排列的局部逆序对
        add(cnt_sum,pos[i],1);
        add(pos_sum,pos[i],pos[i]);
        int l = 1, r = n, mid;
        while(l < r) { // 二分找到那个上文说过的中间位置
            mid = l+r+1 >> 1;
            if(sum(cnt_sum,mid-1)*2 <= i)   l = mid;
            else r = mid-1;
        }
        mid = l;
        ll pre_cnt_sum = sum(cnt_sum,mid), pre_pos_sum = sum(pos_sum,mid); // 计算mid及mid左边的所有<=i的个数以及位置和
        ll mov = pre_cnt_sum * mid - pre_pos_sum - pre_cnt_sum*(pre_cnt_sum-1)/2; // mid左边所有<=i的数移往mid所需要的交换次数
        ll aft_cnt_sum = i - pre_cnt_sum; // mid右边.....同理. 这个求交换次数的过程手算模拟一下会更好理解一点, 文字不太好描述
        mov += sum(pos_sum,n) - pre_pos_sum - aft_cnt_sum * mid - aft_cnt_sum*(aft_cnt_sum+1)/2;
        cout << inv+mov << " \n"[i == n];
    }
    return 0;
}
posted @ 2019-12-24 22:12  wgx666  阅读(365)  评论(0编辑  收藏  举报