题解:洛谷 P1908 逆序对

【题目来源】

洛谷:P1908 逆序对 - 洛谷

【题目描述】

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 \(a_i>a_j\)\(i<j\) 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

【输入】

第一行,一个数 \(n\),表示序列中有 \(n\)个数。

第二行 \(n\) 个数,表示给定的序列。序列中每个数字不超过 \(10^9\)

【输出】

输出序列中逆序对的数目。

【输入样例】

6
5 4 2 6 3 1

【输出样例】

11

【核心思想】

  1. 问题分析:给定长度为 \(n\) 的序列,需要统计逆序对数量,即满足 \(i < j\)\(a_i > a_j\) 的有序对个数。若暴力枚举所有 \(i < j\) 的组合,时间复杂度为 \(O(n^2)\)。关键观察是:逆序对统计可以利用归并排序的分治过程树状数组的在线统计来优化。

  2. 算法选择

    • 解法一:归并排序统计:在归并排序的合并过程中,当左半部分元素 \(a_i > a_j\)(右半部分元素)时,左半部分从 \(i\)\(mid\) 的所有元素都与 \(a_j\) 构成逆序对,直接累加 \(mid - i + 1\)
    • 解法二:离散化 + 树状数组:将数值离散化后,从左到右遍历,用树状数组维护已出现元素的频次,查询比当前元素大的已出现元素数量
  3. 关键步骤(归并排序解法)

    • 递归分解:将区间 \([l, r]\) 二分,分别递归统计左右子区间的逆序对
    • 合并统计:合并两个有序子数组时:
      • \(a_i \leq a_j\)\(a_i\) 放入临时数组,无新增逆序对
      • \(a_i > a_j\)\(a_j\) 放入临时数组,新增逆序对数量 = \(mid - i + 1\)(左半部分 \(i\)\(mid\) 的所有元素均大于 \(a_j\)
    • 累加返回:逆序对总数 = 左子区间逆序对 + 右子区间逆序对 + 跨区间逆序对
  4. 关键步骤(树状数组解法)

    • 离散化:将原数组数值映射为 \(1\)\(cnt\) 的排名,保持大小关系不变
    • 在线统计:从左到右遍历每个元素 \(a_i\)
      • add(newval, 1):将当前元素加入树状数组
      • ans += query(n) - query(newval):查询已出现且比当前元素大的元素数量
    • 树状数组操作lowbit(x) = x \& -xadd 向上更新,query 向下累加前缀和
  5. 时间/空间复杂度

    • 归并排序解法:时间 \(O(n \log n)\),空间 \(O(n)\)
    • 树状数组解法:时间 \(O(n \log n)\),空间 \(O(n)\)
  6. 逆序对统计的核心思想

    • 分治统计跨区间对:归并排序中,左右子区间分别有序后,跨区间的逆序对可以通过双指针线性扫描 \(O(n)\) 统计,避免了 \(O(n^2)\) 的暴力枚举
    • 有序性利用:合并时左半部分和右半部分均已排序,因此当 \(a_i > a_j\) 时,\(i\)\(mid\) 的所有元素必然都大于 \(a_j\),直接批量累加
    • 离散化压缩值域:树状数组解法中,将大值域(\(10^9\))压缩到小范围(\(n\)),使得树状数组可以高效维护频次统计
    • 在线前缀和查询:树状数组通过 query(n) - query(newval) 快速得到"已出现且大于当前值"的元素数量,体现了前缀和思想的灵活应用
    • 适用于逆序对统计、区间逆序对、排列距离计算等场景,是归并排序和树状数组的经典应用

【算法标签】

普及- #归并排序

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int MAX_N = 500005;  // 定义数组最大长度
int n;                     // 数组长度
int a[MAX_N], b[MAX_N];    // a: 原始数组, b: 临时数组
int res;                   // 逆序对总数

// 归并排序并统计逆序对
void merge(int l, int r)
{
    if (l >= r) return;    // 递归终止条件:区间长度为1

    int mid = (l + r) / 2; // 计算中点
    merge(l, mid);         // 递归处理左半部分
    merge(mid + 1, r);     // 递归处理右半部分

    // 合并两个有序区间并统计逆序对
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
    {
        if (a[i] <= a[j])  // 左半部分元素小于等于右半部分
            b[k++] = a[i++];
        else               // 左半部分元素大于右半部分
        {
            b[k++] = a[j++];
            res += mid - i + 1;  // 统计逆序对数量
        }
    }

    // 处理剩余元素
    while (i <= mid) b[k++] = a[i++];
    while (j <= r) b[k++] = a[j++];

    // 将排序好的数据复制回原数组
    for (int i = l; i <= r; i++)
        a[i] = b[i];
}

signed main()
{
    // 加速输入输出
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    // 输入数组长度和元素
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    // 调用归并排序计算逆序对
    merge(1, n);

    // 输出逆序对总数
    cout << res;

    return 0;
}
// 使用acwing的模板二刷
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int N = 500005;  // 定义数组最大长度
int n;                 // 数组长度
int q[N], tmp[N];      // q: 原始数组, tmp: 临时数组

// 归并排序并统计逆序对数量
int merge_sort(int l, int r)
{
    if (l >= r) return 0;  // 递归终止条件:区间长度为1

    int mid = l + r >> 1;  // 计算中点
    int res = merge_sort(l, mid) + merge_sort(mid + 1, r);  // 递归处理左右子区间

    // 合并两个有序区间并统计逆序对
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j])  // 左半部分元素小于等于右半部分
            tmp[k++] = q[i++];
        else               // 左半部分元素大于右半部分
        {
            tmp[k++] = q[j++];
            res += mid - i + 1;  // 统计逆序对数量
        }
    }

    // 处理剩余元素
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];

    // 将排序好的数据复制回原数组
    for (int i = l, j = 0; i <= r; i++, j++)
        q[i] = tmp[j];

    return res;  // 返回当前区间的逆序对总数
}

signed main()
{
    // 输入数组长度和元素
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> q[i];

    // 调用归并排序并输出逆序对总数
    cout << merge_sort(1, n) << endl;

    return 0;
}
// 没有用老师的板子,参考B站其他老师的视频,更好理解
// 离散化+树状数组
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int N = 1000005;  // 最大数组大小

// 结构体:存储数值、原始位置和离散化后的新值
struct Node
{
    int val;     // 原始数值
    int pos;     // 原始位置(下标)
    int newval;  // 离散化后的新值
} a[N];

int tr[N];    // 树状数组,用于统计逆序对
int n;        // 数组长度
int ans;      // 逆序对总数

/**
 * 计算lowbit:获取x的最低位的1
 * @param x 输入数值
 * @return x的最低位的1所代表的值
 */
int lowbit(int x)
{
    return x & -x;  // 利用补码性质
}

/**
 * 树状数组单点更新操作
 * @param x 更新位置
 * @param c 增加的值
 */
void add(int x, int c)
{
    // 树状数组标准更新操作,向上更新所有相关节点
    for (int i = x; i <= n; i += lowbit(i))
        tr[i] += c;
}

/**
 * 树状数组前缀和查询操作
 * @param x 查询位置
 * @return 前x个位置的和
 */
int query(int x)
{
    int res = 0;
    // 树状数组标准查询操作,向下累加所有相关节点
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

/**
 * 比较函数1:按数值升序排列
 * 用于离散化处理
 */
bool cmp(Node x, Node y)
{
    return x.val < y.val;  // 数值小的排在前面
}

/**
 * 比较函数2:按位置升序排列
 * 用于恢复原始顺序
 */
bool cmp2(Node x, Node y)
{
    return x.pos < y.pos;  // 位置小的排在前面
}

signed main()
{
    // 输入数组长度
    cin >> n;

    // 读入数组并记录原始位置
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i].val;  // 读入数值
        a[i].pos = i;     // 记录原始位置
    }

    // 第一步:按数值升序排序,准备离散化
    sort(a + 1, a + n + 1, cmp);

    int cnt = 0;  // 离散化计数器

    // 离散化处理:将数值映射为连续的整数
    for (int i = 1; i <= n; i++)
    {
        // 如果当前数值与前一个不同,计数器加1
        if (a[i].val != a[i - 1].val)
            cnt++;

        // 为当前数值分配新的离散化值
        a[i].newval = cnt;
    }

    // 第二步:按原始位置排序,恢复数组的原始顺序
    sort(a + 1, a + n + 1, cmp2);

    // 第三步:使用树状数组统计逆序对
    for (int i = 1; i <= n; i++)
    {
        // 将当前数值的离散化值加入树状数组
        add(a[i].newval, 1);

        // 计算逆序对数量:
        // query(n) - query(a[i].newval) 返回比当前数值大的已处理元素数量
        ans += query(n) - query(a[i].newval);
    }

    // 输出逆序对总数
    cout << ans << endl;

    return 0;
}

【运行结果】

6
5 4 2 6 3 1
11
posted @ 2026-07-02 22:44  团爸讲算法  阅读(20)  评论(0)    收藏  举报