Divide and Conquer(分治法)
分治法是一种将大问题分解为若干个小问题,分别解决后再合并结果的算法设计思想。它通常包含三个步骤:
- 分解(Divide):将原问题划分为若干个规模较小的子问题。
- 解决(Conquer):递归求解各个子问题。若子问题足够小,则直接求解。
- 合并(Combine):将子问题的解合并为原问题的解。
分治法的特征
- 子问题与原问题形式相同,通常用递归实现。
- 子问题之间相互独立,否则会出现大量冗余计算(如斐波那契数列直接用分治效率低,需配合记忆化)。
- 在实践中,当子问题规模小到一定程度时(如
n < 10),会切换为简单的算法(如插入排序),这称为 Hybrid Algorithm。
分治法经常与二分 、归并排序 、快速排序等算法结合。在二分查找部分已经见到了"猜答案"的分治思想,而归并/快排本身就是分治法的实现。
本专题重点讲解两道题:
- 分治在数组上的应用:最大子数组和(53. Maximum Subarray)
- 分治结合扫描线的难题:天际线问题(218. The Skyline Problem)
另外还会提到分治在 BST 判定中的应用(98. Validate Binary Search Tree)。
例题一:最大子数组和(53. Maximum Subarray, Medium)
题目 :给定整数数组 nums,找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 :nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6(子数组 [4,-1,2,1] 和最大)
分治思路:
- 将数组从中间
mid分为左右两部分。 - 最大子数组要么:
- 完全在左半部分
- 完全在右半部分
- 跨越中点,包含了左右部分的一些元素
- 前两种情况递归求解,第三种情况需要计算从中点向左的最大后缀和 与从中点向右的最大前缀和,两者相加即为跨中点的最大子数组和。
- 三者取最大值。
时间复杂度 :O(n log n),空间复杂度 O(log n)(递归栈)。
(更优的 Kadane 算法 O(n),但这里重点讲解分治思想)
代码:
java
public int maxSubArray(int[] nums) {
return maxSubArray(nums, 0, nums.length - 1);
}
private int maxSubArray(int[] nums, int left, int right) {
if (left == right) return nums[left]; // 只剩一个元素
int mid = left + (right - left) / 2;
int leftMax = maxSubArray(nums, left, mid); // 左半部最大
int rightMax = maxSubArray(nums, mid + 1, right); // 右半部最大
int crossMax = crossMax(nums, left, mid, right); // 跨越中点最大
return Math.max(Math.max(leftMax, rightMax), crossMax);
}
private int crossMax(int[] nums, int left, int mid, int right) {
// 从中点向左的最大后缀和
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = mid; i >= left; i--) {
sum += nums[i];
leftSum = Math.max(leftSum, sum);
}
// 从中点向右的最大前缀和
int rightSum = Integer.MIN_VALUE;
sum = 0;
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
rightSum = Math.max(rightSum, sum);
}
return leftSum + rightSum; // 两者合起来就是跨中点的最大子数组和
}
分治思想帮助理解问题结构,并且可以推广到线段树等高级数据结构。
例题二:天际线问题(218. The Skyline Problem, Hard)
题目 :城市的天际线由若干矩形建筑组成,每个建筑用三元组 [left, right, height] 表示。返回所有转折点,即横向线段发生变化的点坐标,形成天际线的轮廓。
示例:
buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
输出: [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
分治思路:
- 将建筑列表不断分成两半,直到每半只有 0 或 1 个建筑。
- 合并左右两个子天际线:类似于归并排序中合并两个有序数组,用双指针扫描两个子天际线,并维护当前高度(左右子天际线各自的最高建筑高度)。
- 因为天际线的关键点是当前最高高度发生变化的位置,合并时比较两个子天际线的当前高度,较高的那个决定轮廓,并在高度变化时记录新的关键点。
- 这是一个典型的分治算法,时间复杂度 O(n log n)。
代码(较复杂,但思路清晰):
java
public List<List<Integer>> getSkyline(int[][] buildings) {
if (buildings.length == 0) return new ArrayList<>();
return merge(buildings, 0, buildings.length - 1);
}
private List<List<Integer>> merge(int[][] buildings, int left, int right) {
List<List<Integer>> res = new ArrayList<>();
if (left == right) {
int[] b = buildings[left];
res.add(Arrays.asList(b[0], b[2])); // 左上角
res.add(Arrays.asList(b[1], 0)); // 右下角
return res;
}
int mid = left + (right - left) / 2;
List<List<Integer>> leftSkyline = merge(buildings, left, mid);
List<List<Integer>> rightSkyline = merge(buildings, mid + 1, right);
return mergeSkylines(leftSkyline, rightSkyline);
}
private List<List<Integer>> mergeSkylines(List<List<Integer>> left, List<List<Integer>> right) {
List<List<Integer>> res = new ArrayList<>();
int i = 0, j = 0;
int leftH = 0, rightH = 0; // 当前左右天际线的高度
int curH = 0; // 当前合并后的高度
int x = 0;
while (i < left.size() && j < right.size()) {
int x1 = left.get(i).get(0);
int x2 = right.get(j).get(0);
if (x1 < x2) {
leftH = left.get(i).get(1);
x = x1;
i++;
} else if (x1 > x2) {
rightH = right.get(j).get(1);
x = x2;
j++;
} else {
leftH = left.get(i).get(1);
rightH = right.get(j).get(1);
x = x1;
i++;
j++;
}
int maxH = Math.max(leftH, rightH);
if (maxH != curH) {
curH = maxH;
res.add(Arrays.asList(x, curH));
}
}
// 剩余部分直接加入
while (i < left.size()) res.add(left.get(i++));
while (j < right.size()) res.add(right.get(j++));
return res;
}
分治在 BST 判定中的应用(98. Validate Binary Search Tree)
PDF 第 34 页还提到了二分查找树(BST)的验证,它也可以看作分治:
- 对于每个节点,其值必须处于
(min, max)区间内。 - 左子树的值范围变为
(min, root.val),右子树变为(root.val, max)。 - 递归检查左右子树是否满足条件,这就是分治:将大问题(整棵树)分解为小问题(子树),最后合并结果(左右都满足,且当前节点在界内)。
代码:
java
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return isValidBST(node.left, min, node.val) &&
isValidBST(node.right, node.val, max);
}
总结
| 应用 | 分治思想 | 合并关键 |
|---|---|---|
| 最大子数组和 | 二分后,最大和在左、右或跨中 | 计算跨中前缀/后缀和合并 |
| 天际线问题 | 将建筑列表二分,求局部天际线再合并 | 双指针扫描左右子轮廓,根据最大高度更新 |
| 验证BST | 子树是否BST,且节点值在范围内 | 左右都BST且当前节点值合法 |
分治法往往能将时间复杂度从暴力法的 O(n²) 降到 O(n log n),是解决复杂问题的一大利器。在面试中,如果直接想到最优解可能困难,但能给出一个正确的分治递归解,通常也是可以接受的起点。