题目大意是 : 给你 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 << " "[i == n]; } return 0; }