OJ:归并排序与逆序对

基本问题

在数列a1,a2,⋯,an中,出现 i<j 且 ai>aj 的情况,则称 (ai, aj) 是数列中的一个逆序对。常见的问题是统计逆序对的个数。

  • 这个问题可以用归并解决。把数列分为左右两半,先分别统计左右两半内部的逆序对总数,再计算一左一右,且左大于右的情况。
  • 在归并的基础上,应用归并排序可以减少计算量。在左右都排好序的情况下,一左一右逆序对的个数不会受影响。在统计一左一右逆序对时,对于左侧的每一个元素,右侧比这个元素小的元素均可与其组成逆序对。
  • 在程序实现上,可以在左右设置双指针,左侧指针移动:对于左侧的每一个元素记录逆序对个数;右侧指针移动:通过指针下标确定逆序对个数。
  • 时间复杂度:O(nlogn),空间复杂度:额外O(n). 相比于硬遍历的O(n^2),对时间复杂度有大改善。

阅读资料:https://zh.wikipedia.org/wiki/%E9%80%86%E5%BA%8F%E5%AF%B9

模板题

在数列a1,a2,⋯,an中,出现 i<j 且 ai>aj 的情况,则称 (ai, aj) 是数列中的一个逆序对。统计逆序对的个数。
输入:共两行。
第一行,一个数字n,给出数列中元素的个数。
第二行,n个数字a1,a2,⋯,an,代表数列。
输出:一行,输出逆序对个数。
Sample Input 1

6
13 8 5 3 2 1

Sample Output 1

15

Sample Input 2

6
3 3 3 2 2 2

Sample Output 2

9
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <algorithm>
#include <stack>
#include <queue>
#include <vector>
#include <cmath>
#include <cstdio>
#include <bitset>
 
using namespace std;

const int N = 100000 + 5;
 
static int n; //数组长度
static int A[N]; //数组内容
static int tmpA[N]; //归并排序时使用的临时数组,用来记录排序结果并复制回去

void swap(int i, int j){
    // 交换数组A中i, j位置的元素
    int tmp = A[i];
    A[i] = A[j];
    A[j] = tmp;
}
 
int merge(int l, int r) {
    // merge:对[l, r]区间进行归并排序,并计算逆序对个数
    if(l >= r) return 0;
    if(l+1 == r){ // 两个数
        if(A[l] > A[r]){ //逆序
            swap(l, r); // 交换使有序
            return 1; // 逆序对个数为1
        }
        else return 0; // 顺序,逆序对个数为0
    }
    int mid = (l + r) / 2;
    // 左右分别归并排序并统计逆序对
    int result1 = merge(l, mid);
    int result2 = merge(mid + 1, r);
    // 左右目前都已经有序,统计左右之间的
    int pl = l, pr = mid + 1;
    int ptr = l; // 写入tmpA的指针
    int cnt = 0;
    // 对每个右侧元素统计逆序对
    while(pl <= mid && pr <= r) {
        if(A[pl] <= A[pr]){
            tmpA[ptr] = A[pl];
            pl ++;
        }
        else{
            cnt += mid - pl + 1; 
            // 比A[pr]大的数:从pl到mid,共mid - pl + 1个数
            tmpA[ptr] = A[pr];
            pr ++;
        }
        ptr ++;
    }
    // 一侧已经移动完了,一侧还没移动完
    // 没完的一侧的剩余元素是最大的,按顺序写回tmpA
    while(pl <= mid){
        tmpA[ptr] = A[pl];
        ptr ++;
        pl ++;
    }
    while(pr <= r){
        tmpA[ptr] = A[pr];
        ptr ++;
        pr ++;
    }
    // tmpA中存储[l, r]区间的有序结果,写回A
    for(int i=l; i<=r; i++){
        A[i] = tmpA[i];
    }
    return result1 + result2 + cnt;
} 
 
int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) 
        scanf("%d", &A[i]);
    int result = merge(1, n);
    printf("%d", result);
    return 0;
}

特殊定义逆序对 模板题

有一类题给逆序对一个不同的定义,然后仍然需要统计逆序对。

题目来源:2019夏 中科院计算所夏令营 大机试第2题

Problem Description

Recall the problem of finding the number of inversions. As in the course, we are given a sequence of n numbers and we define an inversion to be a pair i<j such that ai>aj.

We motivated the problem of counting inversions as a good measure of how different two orderings are. However, one might feel that this measure is too sensitive. Let’s call a pair a significant inversion if i<j and ai>3aj . Give an O(nlogn) algorithm to count the number of significant inversions between two orderings.

The array contains N elements (1<=N<=100,000). All elements are in the range from 1 to 1,000,000,000.

Input

The first line contains one integer N, indicating the size of the array. The second line contains N elements in the array.

  • 50% test cases guarantee that N < 1000.

Output

Output a single integer which is the number of pairs of significant inversions.

Sample Inout

6
13 8 5 3 2 1

Sample Output

6

分析

  • 条件不同了:
    • 左右排好序仍然有利于计算个数
    • 不能直接通过逆序对条件排序
  • 解决:仍然在归并排序的框架下完成计数,但是先统计逆序对个数,再正常归并排序。时间复杂度仍然是O(nlogn).
  • 注意坑:数据范围,按题目最大的N,最坏情况下会超过int范围。开long long.

代码

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <algorithm>
#include <stack>
#include <queue>
#include <vector>
#include <cmath>
#include <cstdio>
#include <bitset>
 
using namespace std;

const int N = 100000 + 5;
 
static int n, A[N], tmpA[N];
 
long long merge(int l, int r) {
    if(l >= r) return 0;
    if(l+1 == r){ // two numbers
        int result = 0;
        if(A[l] > 3 * A[r]) result = 1;
        if(A[l] > A[r]){
            int tmp = A[l];
            A[l] = A[r];
            A[r] = tmp;
        }
        return result;
    }
    int mid = (l + r) / 2;
    long long result1 = merge(l, mid);
    long long result2 = merge(mid + 1, r);
    // 默认左右都已经有序
    // 统计左右之间的
    int pl = l, pr = mid + 1;
    long long cnt = 0;
    while(pl <= mid && pr <= r) {
        if(A[pl] <= 3 * A[pr])
            pl ++;
        else{
            pr ++;
            cnt += mid - pl + 1;
        }
    }
    // merge sort
    pl = l, pr = mid + 1;
    int ptr = l;
    while(pl <= mid && pr <= r) {
        if(A[pl] < A[pr]){
            tmpA[ptr] = A[pl];
            pl ++;
        }
        else{
            tmpA[ptr] = A[pr];
            pr ++;
        }
        ptr ++;
    }
    while(pl <= mid){
        tmpA[ptr] = A[pl];
        ptr ++;
        pl ++;
    }
    while(pr <= r){
        tmpA[ptr] = A[pr];
        ptr ++;
        pr ++;
    }
    // write back
    for(int i=l; i<=r; i++){
        A[i] = tmpA[i];
    }
    return result1 + result2 + cnt;
} 
 
int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) 
        scanf("%d", &A[i]);
    int result = merge(1, n);
    /*
    for(int i = 1; i <= n; i++) 
        printf("%d ", A[i]);
    printf("\n");
    */
    printf("%lld", result);
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值