分治法经典问题-逆序对个数

da384706369225833bf989c64af0ddd8.png

编程算法

3cf50a325b6c1be2169bc27eecef7236.png

本栏目跟大家分享刷题、竞赛、面试中的编程算法的内容。例如算法、数据结构的知识点,代码模板,题目聚合列表,好题分享等等。

在下面的文章中,我们详细总结了分治法的内容。

文章: 分治
链接: https://chengzhaoxi.xyz/59d3f8e5.html

分治法有一个重要应用是归并排序。关于归并排序的算法细节和代码模板,可以参考下面这篇文章: 

文章: 数组排序
链接: https://chengzhaoxi.xyz/34417.html

一般写排序不会写归并排序,但是很多分治算法都基于归并排序的思想:分的阶段进行排序,合的阶段做归并同时借助有序的特性更高效地统计目标信息。

这种思想最经典的问题是逆序对的个数。本文我们就来看一下逆序对的问题。

逆序对问题在 leetcode 上有一个变种翻转对问题,此前写过题解,参考下面这篇文章。

文章: 翻转对题解
链接: https://chengzhaoxi.xyz/1675.html

逆序对问题

题目: 107. 超快速排序
链接: https://www.acwing.com/problem/content/109/

在这个问题中,您必须分析特定的排序算法——超快速排序。

该算法通过交换两个相邻的序列元素来处理 n 个不同整数的序列,直到序列按升序排序。

对于输入序列 9 1 0 5 4,超快速排序生成输出 0 1 4 5 9。

您的任务是确定超快速排序需要执行多少交换操作才能对给定的输入序列进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
输入格式
输入包括一些测试用例。
每个测试用例的第一行输入整数 n,代表该用例中输入序列的长度。
接下来 n 行每行输入一个整数 ai,代表用例中输入序列的具体数据,第 i 行的数据代表序列中第 i 个数。
当输入用例中包含的输入序列长度为 0 时,输入终止,该序列无需处理。

输出格式
对于每个需要处理的输入序列,输出一个整数 op,代表对给定输入序列进行排序所需的最小交换操作数,每个整数占一行。

数据范围
0<=n<500000,
一个测试点中,所有 n 的和不超过 500000。
0<=ai<=999999999

输入样例:
5
9
1
0
5
4
3
1
2
3
0
输出样例:
6
0

算法: 分治

我们首先写出分治法的【分解-解决-合并】框架

1
2
3
4
5
6
7
8
分解: 将原序列 x=a[l..r] 分为 x1=a[l, (l+r)/2], x2=a[(l+r)/2 + 1, r] 两部分

解决: 如果原序列 x 的长度为 1,则直接返回此序列和 0 即可

合并: 左边部分的逆序对个数为 n1, 排序后的序列 y1
      右边部分的逆序对个数为 n2, 排序后的序列 y2
      对 y1, y2 做 2 路归并,归并后的序列为 y,并顺便统计逆序对个数 n3
      返回 y 和 n1 + n2 + n3

这里的一个问题就是在二路归并的时候如何顺便统计逆序对个数。

我们考察合并阶段在进行二路归并过程,如下图

47241f79de08ac33071cddbcb2a41414.png

当前 y1 序列进行到 i,y2 序列进行到 j。此时问: y1 中有多少比 y2[j] 大,并将其添加到对 n3 的贡献中。

由于 y1[0..i-1], y2[0..j-1] 已经进入 tmp,并且小于等于 y1[i], y2[j],因此 y1[0..i-1] 中是不会对 n3 有贡献的。

所以就要看 y1[i..m1-1] 中有多少大于 y2[j],根据 y1[i] 与 y2[j] 的大小关系分类讨论。

  • 如果 y1[i] > y2[j],则按二路归并的流程将 y2[j] 压进 tmp,并对 n3 贡献 m1 - i。

  • 如果 y1[i] <= y2[j],则按二路归并的流程将 y1[i] 压进 tmp,此时 y2[j] 对 n3 的贡献还不确定。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <vector>

using namespace std;

using ll = long long;

ll merge_sort(vector<ll>& a, int l, int r)
{
    // 解决
    if(l == r)
        return 0;
    // 分解
    int mid = (l + r) / 2;
    ll n1 = merge_sort(a, l, mid);
    ll n2 = merge_sort(a, mid + 1, r);
    // 合并
    int i = l;
    int j = mid + 1;
    ll n3 = 0;
    vector<int> tmp(r - l + 1);
    int k = 0;
    while(i <= mid && j <= r)
    {
        if(a[i] > a[j])
        {
            n3 += mid + 1 - i;
            tmp[k++] = a[j++];
        }
        else
            tmp[k++] = a[i++];
    }
    while(i <= mid)
        tmp[k++] = a[i++];
    while(j <= r)
        tmp[k++] = a[j++];
    for(int i = l; i <= r; ++i)
        a[i] = tmp[i - l];
    return n1 + n2 + n3;
}

int main()
{
    vector<ll> a;
    while(true)
    {
        int n;
        cin >> n;
        if(n == 0)
            break;
        a.assign(n, -1);
        for(int i = 0; i < n; ++i)
            cin >> a[i];
        ll ans = merge_sort(a, 0, n - 1);
        cout << ans << endl;
    }
}

总结

本文我们了解到在分治法的【分解-解决-合并】的框架中,在合并阶段我们可以基于已经解决的子问题做一些额外的统计。对于逆序对问题,就是二路归并同时借助有序的特性更高效地统计目标信息。分治法的思想还是挺优雅的,具体做的时候往【分解-解决-合并】的框架上套即可。分治法有一些更经典的问题,例如 FFT、矩阵乘法、大整数乘法、平面最近点对等。以后有时间再继续分享。

leetcode 题目列表系列快写完了,以后会多写一些这种平时遇到的好题。大家刷 leetcode 的可以关注一下下面的仓库,里面已经有了 1700 多题的代码和对应的题目列表梳理。后续如果刷了新题或者参加了周赛,里面的代码会同步更新。

https://github.com/FennelDumplings/leetcode-maxed_out

195643ab30fd9a7b41be427e88e2b9ed.gif

● 一道组合数学题-马拦过河卒,很精彩

● 【概率面试题连载】24. 选票盒

● 【动态规划的优化】2.哈希表优化DP

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
分治个数c的实现如下: 1. 将数组a分成两个子数组a1和a2,分别对a1和a2递归地求个数c1和c2,然后合并a1和a2并计算跨越a1和a2的个数c3。 2. 合并a1和a2时,设置两个指针i和j分别指向a1和a2的起始位置,然后将较小的元素放入新数组b中,并将指针向后移动,直到其中一个子数组遍历完毕。此时,将另一个子数组的剩余元素全部放入b中。 3. 计算跨越a1和a2的个数c3时,设置两个指针i和j分别指向a1和a2的末尾位置,并将它们向前移动,同时设置一个计数器count用于计算跨越a1和a2的个数。比较a1[i]和a2[j]的大小,如果a1[i]>a2[j],则说明a2[j]和a1[i+1]到a1[n-1]都构成了对,因为a1[i+1]到a1[n-1]都比a2[j]大,此时将count加上i+1的值,并将a1[i]放入新数组b中,然后将指针i向前移动;否则,将a2[j]放入新数组b中,并将指针j向前移动。 4. 返回c1+c2+c3作为个数。 代码实现如下: ``` int mergeSort(vector<int>& a, int left, int right) { if (left >= right) return 0; int mid = (left + right) / 2; int c1 = mergeSort(a, left, mid); int c2 = mergeSort(a, mid + 1, right); vector<int> b(right - left + 1); int i = left, j = mid + 1, k = 0, c3 = 0; while (i <= mid && j <= right) { if (a[i] <= a[j]) b[k++] = a[i++]; else { b[k++] = a[j++]; c3 += mid - i + 1; } } while (i <= mid) b[k++] = a[i++]; while (j <= right) b[k++] = a[j++]; for (int i = left; i <= right; i++) a[i] = b[i - left]; return c1 + c2 + c3; } int countInversePairs(vector<int>& nums) { return mergeSort(nums, 0, nums.size() - 1); } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值