分治
即将整个问题分为若干个小问题后分而治之,最后逐步合并子问题的解以找到最终解
分治算法是一种找大规模问题与小规模问题关系的方法,故用递归来表示
实例:归并排序
归并排序是依靠将数组分为若干小数组分别排序,最后合并得到结果的方法实现排序的
步骤如下:
- 将数组分为两部分
- 递归分别将两个子数组排序
- 合并两子序列
时间复杂度:\(O(n\log n)\)
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
#include <iostream>
using namespace std;
int a[100001], temp[100001];
/*
步骤:
1.将数列划为两部分
2.递归地分别对两个子序列归并排序
3.合并两个子序列
*/
// 时间复杂度:O(nlogn)
void merge_sort(int l, int r){
// 只有一个元素
if (l == r) return ;
// 将数组分为[l,mid] [mid+1,r] 使用>>1 相当于/2 速度更快
int mid = (l+r) >> 1;
// 分数组并分别进行排序
merge_sort(l, mid);
merge_sort(mid+1, r);
// 合并
int i = l, j = mid+1, k = l;
// 当两部分数组都还有数
while (i<=mid && j<=r){
// 把小的加入数组tmp
if (a[i] <= a[j]) temp[k++]=a[i++];
else temp[k++]=a[j++];
}
// 两部分数组有一部分剩余的情况下,将剩余的一个个放到tmp中
while (i<=mid){
temp[k++]=a[i++];
}
while (j<=r){
temp[k++]=a[j++];
}
// tmp拷贝到原数组
for (int i = 1; i<=r; ++i) a[i] = temp[i];
}
int main(){
int n ;
scanf("%d", &n);
for (int i = i ; i <=n; ++i) scanf("%d", &a[i]);
merge_sort(1,n);
for (size_t i = 1; i <= n; i++)
{
if (i == n) printf("%d\n", a[i]);
else printf("%d", a[i]);
}
return 0;
}最大子数组问题
输入:数组a[1…n]
输出:(i,j,s)使得a[i…j]为最大子数组,且和为s
分治策略
- 分:取中点:最大子数组要不完全在左侧要不完全在右侧或者跨越重点
- 治:完全位于某一侧可以递归解决;跨越中点可以从终点向两侧扩张遍历解决
- 合:三种情况和取最大即可
时间复杂度:\(O(n\log n)\)
对于跨越中点的子数组:

代码:
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
// 使用分治求最大子段和
#include <iostream>
#include <algorithm>
#include <queue>
#include <string>
#include <utility>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <climits>
#define ios ios::sync_with_stdio(0); cin.tie(0);
#define endl '\n'
using namespace std;
using ll = long long;
const int N = 2e5+10;
int n;
int arr[N];
// 寻找贯穿左右数列的数列的最大值
// 核心思路:从中点往两边寻找,分别找左右最大相加即可
int findcrossmax(int l, int r, int mid){
int sum=0;
int left_big = INT_MIN; // 初始化要足够小,因为有负数最小为-1e4
for(int i = mid; i>=l;i--){ // 中间往左寻找
sum+=arr[i]; // 一个个求和
if (sum>left_big) left_big = sum; // 寻找最大左段和
}
sum=0;
int right_big = INT_MIN;
for(int i = mid+1; i<=r;i++){ // 中间往右寻找
sum+=arr[i]; // 一个个求和
if (sum>right_big) right_big = sum; // 寻找最大右段和
}
return left_big+right_big; // 返回最大两段和
}
// 寻找最大值
int findmax(int l, int r){
if (l==r) return arr[l]; // 分段达到最小,返回值
int mid = l+r>>1; // 选择中间值
int left_big = findmax(l,mid); // 递归寻找左端最大值
int right_big = findmax(mid+1,r); // 递归寻找右端最大值
int cross_big = findcrossmax(l,r,mid); // 寻找中段最大值
return max(cross_big, max(left_big,right_big)); // 返回三者最大值
}
int main(){
cin >> n;
for (int i =1;i<=n;i++) cin >> arr[i];
cout << findmax(1,n);
return 0;
}求逆序对
题目描述
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 \(a_i>a_j\) 且 \(i<j\) 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
输入格式
第一行,一个数 \(n\),表示序列中有 \(n\) 个数。
第二行 \(n\) 个数,表示给定的序列。序列中每个数字不超过 \(10^9\)。
输出格式
输出序列中逆序对的数目。
输入 #1
1
26 5 4 2 6 3 1输出 #1
111说明/提示
对于 \(25\%\) 的数据,\(n \leq 2500\)。
对于 \(50\%\) 的数据,\(n \leq 4 \times 10^4\)。
对于所有数据,\(n \leq 5 \times 10^5\)。
请使用较快的输入输出。
应该不会 \(O(n^2)\) 过 50 万吧 by chen_zhe。
- 分:取中点,逆序对存在于中点左侧,右侧以及跨中点部分
- 治:分别统计两侧的逆序对数量,跨中点部分统计如下
- 合:对左右以及左右merge过程产生逆序对数量求和,统计完后让原数组l到r有序
merge函数伪代码:
1
2
3
4
int f(l,r){
int m = (l+r)/2;
return f(l,m)+f(m+1,r)+'''左右merge过程中产生的逆序对数量'''
}如何统计跨中点部分的逆序对数量:
让i,j分别在两区间右侧开始向左遍历(此时左右区间已经统计完毕并复原)
若arr[i]>arr[j] 则证明该j位置与该j位置之前的位置的数均可以组成逆序对
若arr[i]<=arr[j],则让j–,继续判断,当j移动到左侧区间时,判定逆序对数为0
以上两种情况发生后让i–,继续统计逆序对
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
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int,int> PII;
const int N = 5e5 + 10;
const double pi = acos(-1.0);
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
ll arr[N];
ll temp[N];
// 查询跨左右区间的逆序对数
ll merge(int l, int r ,int m){
ll ans = 0;
for (int i = m,j=r;i>=l;i--){ // 从左右区间的右侧开始比较
while(arr[i]<=arr[j]&&j>=m+1) j--; // j不到左区间的情况下以及未发现逆序对时往左遍历
ans += j-m; // 若发现第一个逆序对(arr[i]>arr[j]&&i<j),由于已经排序,前面剩下的都可以组成逆序对
}
// 归并排序,将查询过的区间进行排序
int i = l, j = m+1, k = l;
while(i<=m && j<=r){
if (arr[i]<=arr[j])temp[k++]=arr[i++];
else temp[k++]=arr[j++];
}
while(i<=m) temp[k++]=arr[i++];
while(j<=r) temp[k++]=arr[j++];
for (int i = l;i<=r;i++) arr[i]=temp[i];
return ans;
}
// 分治,分为左右区间以及跨区间逆序对
ll f(int l, int r){
if (l==r) return 0;
ll m = (l+r)/2;
return f(l,m)+f(m+1,r)+merge(l,r,m);
}
void solve() {
int n;
cin >> n;
for (int i = 1;i<=n;i++) cin >> arr[i];
cout << f(1,n);
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int tomorin=1;
//cin >> tomorin;
while (tomorin--) solve();
return 0;
}