1.什么是分治算法?
分治,字⾯上的解释是「分⽽治之」,就是把⼀个复杂的问题分成两个或更多的相同的⼦问题,直到 最后⼦问题可以简单的直接求解,原问题的解即⼦问题的解的合并。
由于分治算法需要不断地将问题分为多个相同的子问题来解决,所以分治算法经常使用递归来实现,关于递归算法的相关理解可以参照:优快云
2.分治算法相关的问题解析:
在了解了分治算法相关的概念同时对递归算法有了一些理解之后,接下来我们一起来看一看接下来的这两个和分治算法有关的问题。
例一:
链接:P1908 逆序对 - 洛谷
事实上,分治是解决逆序对问题的经典解法。
接下来,我们开始分析这个问题:
第一步:
如果把整个序列 从中间 位置分成两部分,那么逆序对个数可以分成三部分:
• [l, mid] c1 区间内逆序对的个数 ;
• [mid+1,r] 区间内逆序对的个数 ;
• 从 [l, mid] 以及 [mid+1,r] 各选⼀个数,能组成的逆序对的个数 c1 +c2+c3。
那么逆序对的总数就是c1+c2+c3,其中求解c1、c2的方法就是借助递归。
第二步:
接下来的主要问题就是处理如何求出在两个区间中各取一个数从而形成的逆序对。
对于这个问题,如果采取暴力的对两个区间遍历,时间复杂度是非常恐怖的,此时我 们可以先对两个区间先排序,这样的话时间复杂度的优化就非常可观了,所以我们先借助库函数sort对两个区间排序,那么接下来的问题就是已知两个有序数组,如何求出左边选⼀个数,右边选⼀个数的情况下的 逆序对的个数。核⼼思想就是找到右边选⼀个数之后,左边区间内「有多少个⽐我⼤的」。
定义两个「指针」cur1,cur2扫描两个有序数组:此时会有下⾯三种情况:
1.a[cur1] ≤ a[cur2] a[cur1] 不会与 区间内任何⼀个元素构成逆序对cur1++
2. a[cur1] > a[cur2] :此时[cur1,mid]区间内所有元素都会与a[cur2]构成逆序对,逆序对个数增加 mid−cur1+1 ,此时cur2已经统计过逆序对了,cur2++。
接下来重复上面两步,就可以在O(n)的时间复杂度内完成第二步。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 5e5 + 10;
int n;
int b[N];
//分治递归
LL dfs(int l, int r)
{
//递归出口
if (l >= r) return 0;
LL ret = 0;
//分成两部分,分别求这两部分的逆序对
int midl = (l + r) / 2;
int midr = midl + 1;
ret += dfs(l, midl); ret += dfs(midr, r);
//借用临时数组,记录分别在左右区间各一个数字的逆序对
//排序
sort(b + l, b + midl + 1); sort(b + midr, b + r + 1);
int i = l, j = midr;
while (i <= midl && j <= r)
{
if (b[i] <= b[j])i++;
else
{
ret += midr - i;
j++;
}
}
return ret;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> b[i];
}
cout << dfs(1, n);
return 0;
}
例二:
链接:P1923 【深基9.例4】求第 k 小的数 - 洛谷
本题为经典的topk问题,最常用的解法是利用堆来维护数据流中topk的数,但由于这里讨论的是分治思想,所以我们用分治思想来解决,事实上本题的这种方法叫做快速搜索算法。
在快排中,当我们把数组「分成三块」之后: [l, left] [left + 1, right - 1] [right, r] ,我们可以通过计算每⼀个区间内元素的「个数」,进⽽推断出我们要找的元素是在哪一个区间里面。
代码:
#include <iostream>
using namespace std;
const int N = 5e6 + 10;
int n, k;
int a[N];
//递归搜索函数
int dfs(int left, int right, int k)
{
//递归出口
if (left >= right) return a[left];
//选取一个随机值,数组分三块
int p = a[left];
int l = left - 1, r = right + 1, i = left;
while (i < r)
{
if (a[i] < p) swap(a[i++], a[++l]);
else if (a[i] == p) i++;
else swap(a[i], a[--r]);
}
//[left,l] [l+1,r-1] [r,right]
//l-left+1 r-2 right-r+1
//判断k和区间长度的关系,进行递归
int a = l - left + 1, b = r - l - 1, c = right - r + 1;
if (k <= a) return dfs(left, l, k);
else if (k <= a + b) return p;
else return dfs(r , right, k - a - b);
}
int main()
{
cin >> n >> k; k++;
for (int i = 1; i <= n; i++) cin >> a[i];
cout << dfs(1, n, k);
return 0;
}
3.结语:
这就是分治算法的所有思考了,从上面的两个问题可以看出,分治算法实现并不难,但是要想想到用分治算法是比较难的,所以还需要以后继续积累这方面的经验,最后各位于晏、亦菲和我一起讨论,一起进步。