分治算法

基本概念

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。

基本思想及策略

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

分治法适用的情况

分治法所能解决的问题一般具有以下几个特征:

  1. 该问题的规模缩小到一定的程度就可以容易地解决

  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

  3. 利用该问题分解出的子问题的解可以合并为该问题的解;

  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

分治和动态规划有共通也有不同,我i们来看如下两个定义。

最优子结构:如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子机构。

重叠子问题:用来求解原问题的递归算法反复地解同样的子问题,而不是总是在产生新的子问题。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。

当子问题相互独立时,能且只能使用分治。存在重叠子问题时,动态规划是更好的算法。 In a word, 分治法 —— 各子问题独立;动态规划 —— 各子问题重叠。

算法导论: 动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一个问题的两个子问题不共享资源,则它们就是独立的。对两个子问题俩说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,是重叠的,则它们是重叠的。

分治法的基本步骤

分治法在每一层递归上都有三个步骤:

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

step3 合并:将各个子问题的解合并为原问题的解。

它的一般的算法设计模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Divide-and-Conquer(P)

  1. if |P|≤n0

  2. then return(ADHOC(P))

  3. 将P分解为较小的子问题 P1 ,P2 ,…,Pk

  4. for i←1 to k

  5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi

  6. T ← MERGE(y1,y2,…,yk) △ 合并子问题

  7. return(T)

  其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

归并排序

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
//首先来看函数主体,实现了分割的部分
void mergesort(int a[],int n,int left,int right)
{
if(left+1<right)
{
int mid=(left+right)/2;
mergesort(a,n,left,mid);
mergesort(a,n,mid,right);
merge(a,n,left,mid,right);
}
}
//下面来写自己的merge函数!
//L,R是辅助数组!
void merge(int a[],int n,int left,int mid,int right)
{
int n1=mid-left,n2=right-mid;
for(int i=0;i<n1;i++)
L[i]=a[left+i];//储存左数列
for(int i=0;i<n2;i++)
R[i]=a[mid+i];//储存右数列
L[n1]=R[n2]=INF;
int i=0,j=0;
for(int k=left;k<right;k++)//合并
{
if(L[i]<=R[j])
a[k]=L[i++];
else
a[k]=R[j++];
}
}

分治法举例

题目与答案链接:https://leetcode-cn.com/problems/maximum-subarray/

对于一个区间 [l,r],我们可以维护四个量:

lSum 表示 [l,r] 内以 l 为左端点的最大子段和
rSum 表示 [l,r] 内以 r 为右端点的最大子段和
mSum 表示 [l,r] 内的最大子段和
iSum 表示 [l,r] 的区间和
以下简称 [l,m] 为 [l,r] 的「左子区间」,[m + 1, r] 为 [l,r] 的「右子区间」。我们考虑如何维护这些量呢(如何通过左右子区间的信息合并得到 [l,r] 的信息)?对于长度为 1 的区间 [i, i],四个量的值都和aia_i 相等。对于长度大于 1 的区间:

首先最好维护的是 iSum,区间 [l,r] 的 iSum 就等于「左子区间」的 iSum 加上「右子区间」的 iSum。
对于 [l,r] 的 lSum,存在两种可能,它要么等于「左子区间」的 lSum,要么等于「左子区间」的 iSum 加上「右子区间」的 lSum,二者取大。
对于 [l,r] 的 rSum,同理,它要么等于「右子区间」的 rSum,要么等于「右子区间」的 iSum 加上「左子区间」的 rSum,二者取大。
当计算好上面的三个量之后,就很好计算 [l,r] 的 mSum 了。我们可以考虑 [l,r] 的 mSum 对应的区间是否跨越 m——它可能不跨越 m,也就是说 [l,r] 的 mSum 可能是「左子区间」的 mSum 和 「右子区间」的 mSum 中的一个;它也可能跨越 m,可能是「左子区间」的 rSum 和 「右子区间」的 lSum 求和。三者取大。
这样问题就得到了解决。

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
class status {
constructor (lSum, rSum, mSum, iSum) {
this.lSum = lSum;
this.rSum = rSum;
this.mSum = mSum;
this.iSum = iSum;
}
}

const pushUp = (l, r) => {
const iSum = l.iSum + r.iSum;
const lSum = Math.max(l.iSum + r.lSum, l.lSum);
const rSum = Math.max(r.iSum + l.rSum, r.rSum);
const mSum = Math.max(l.mSum, r.mSum, l.rSum + r.lSum);
return new status(lSum, rSum, mSum, iSum);
}

const get = (nums, l, r) => {
if (l === r) {
return new status(nums[l], nums[l], nums[l], nums[l]);
}
const m = Math.floor((l + r) / 2);
const lsub = get(nums, l, m);
const rsub = get(nums, m + 1, r);
return pushUp(lsub, rsub);
}

var maxSubArray = function(nums) {
return get(nums, 0, nums.length - 1).mSum;
};

动规解法

1
2
3
4
5
6
7
8
var maxSubArray = function(nums) {
let pre = 0, max = nums[0];
for (let i = 0; i < nums.length; i++) {
pre = Math.max(pre + nums[i], nums[i]);
max = Math.max(pre, max)
}
return max;
};

参考文章:

https://zhuanlan.zhihu.com/p/45986027