归并分治

Posted by Marlin on May 4, 2025

归并分治的原理与补充说明

原理

  1. 思考一个大问题的答案是否等于小问题的答案之和
    • 将一个大问题分解为若干个小问题,分别求解这些小问题的答案。
    • 如果左右两部分的答案是有序的,并且跨越左右产生的答案可以高效计算,那么整个问题可以通过归并的方式解决。
  2. 计算“跨越左右产生的答案”
    • 如果加上左、右各自有序这个设定,会获得计算的便利性。例如,在归并排序中,左右两部分已经是有序的,只需要通过合并操作即可得到最终的有序序列。
  3. 如果以上两点成立,那么这个问题很可能被归并分治解决
    • 因为总有一些很牛的“出题人”,他们会设计一些问题,使得归并分治成为一种高效的解决方案。
  4. 求解答案的过程中只需要加入归并排序的过程即可
    • 为了使左、右各自有序,从而获得计算的便利性,归并排序的核心思想就是先递归地将数组分成两部分,然后对这两部分分别进行排序,最后再将它们合并成一个有序的数组。

补充

  1. 一些用归并分治解决的问题,往往也可以用线段树、树状数组等数据结构解决
    • 时间复杂度也都是最优解。这些数据结构会在扩展课程阶段讲到。
  2. 本节课讨论的目标都是归并分治的常规题,难度不大
    • 归并分治不仅可以解决简单问题,还可以解决较难的问题。只要符合上面说的特征,比如多维空间里任何两点间的最短距离问题,这个内容会在《高级算法》课程阶段讲解。顶级公司考这个问题的也很少,因为很难。但是这个问题本身并不冷门,来自《算法导论》原题。
  3. 还有一个常考的算法:“整块分治”
    • 这个算法会在《必修》课程阶段讲到。

赞美:精妙又美丽的思想传统

  • 归并分治的思想是一种非常优雅且高效的方法,它不仅适用于排序问题,还广泛应用于其他算法领域,如动态规划、区间查询等问题。通过将大问题分解为小问题,并利用小问题的有序性或独立性来高效求解,归并分治体现了算法设计中的智慧与美感。

归并分治-小和问题

问题描述

描述 数组小和的定义如下: \(\sum_{i=1}^{n} f_i\) 其中 $f_i$ 的定义是第 $i$ 个数的左侧小于等于 $s_i$ 的个数。

例如,数组 $s = [1, 3, 5, 2, 4, 6]$ ,在 $s[0]$ 的左边小于或等于 $s[0]$ 的数的和为 $0$ ; 在 $s[1]$ 的左边小于或等于 $s[1]$ 的数的和为 $1$ ;在 $s[2]$ 的左边小于或等于 $s[2]$ 的数的和为 $1+3=4$ ;在 $s[3]$ 的左边小于或等于 $s[3]$ 的数的和为 $1$ ; 在 $s[4]$ 的左边小于或等于 $s[4]$ 的数的和为 $1+3+2=6$ ;在 $s[5]$ 的左边小于或等于 $s[5]$ 的数的和为 $1+3+5+2+4=15$ 。所以 $s$ 的小和为 $0+1+4+1+6+15=27$ 给定一个数组 $s$ ,实现函数返回 $s$ 的小和

数据范围: \(0 < n \le 10^5\) \(|s_i| \le 100\)

输入描述: 第一行有一个整数N。表示数组长度 接下来一行N个整数表示数组内的数

输出描述: 一个整数表示答案

示例1 输入: 6 1 3 5 2 4 6

输出: 27

示例2 输入: 1 1

输出: 0

备注: \(1 \le N \le 10^5\) \(-100 \le arr_i \le 100\)


核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int merge(int l, int m, int r) {
    // 统计部分
    int ans = 0;
    for (int i = l, j = m + 1, sum = 0; j <= r; j++) {
        while (i <= m && arr[i] <= arr[j]) {
            sum += arr[i++];
        }
        ans += sum;
    }

   // 正常归并...

   return ans;
}

int smallSum(int l, int r){ // 求l~r的小和
   if(l == r){
      return 0;
   }
   int m = (l + r) / 2;
   return smallSum(l, m) + smallSum(m + 1, r) + merge(l, m, r);
}


归并分治-翻转对(Reverse Pairs)

问题描述

给定一个数组 nums,如果存在 i < jnums[i] > 2 * nums[j],我们就将 (i, j) 称作一个重要翻转对。

你需要返回给定数组中的重要翻转对的数量。

示例 1:

1
2
输入: [1,3,2,3,1]
输出: 2

示例 2:

1
2
输入: [2,4,3,5,1]
输出: 3

注意

  • 给定数组的长度不会超过 50000
  • 输入数组中的所有数字都在 32位整数 的表示范围内。

解法提示

可通过 归并排序树状数组(Fenwick Tree)线段树(Segment Tree) 实现高效求解。


核心代码:

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
int merge(int l, int m, int r){
   if(l >= r){
      return 0;
   }
   // 统计部分
   int ans = 0;
   for(int i = l, j = m + 1; i <= m; i++){
      while(j <= r && nums[i] > 2 * nums[j]){
         j++;
      }
      ans += j - m - 1;
   }

   // 正常归并...
   
   return ans;
}

int counts(int l, int r){
   if(l >= r){
      return 0;
   }
   int m = l + (r - l) / 2;
   return counts(l, m) + counts(m + 1, r) + merge(l, m, r);
}

int main(){
   //...
}