Divide and Conquer(分治法)

Divide and Conquer(分治法)

分治法是一种将大问题分解为若干个小问题,分别解决后再合并结果的算法设计思想。它通常包含三个步骤:

  1. 分解(Divide):将原问题划分为若干个规模较小的子问题。
  2. 解决(Conquer):递归求解各个子问题。若子问题足够小,则直接求解。
  3. 合并(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] 和最大)

分治思路

  1. 将数组从中间 mid 分为左右两部分。
  2. 最大子数组要么:
    • 完全在左半部分
    • 完全在右半部分
    • 跨越中点,包含了左右部分的一些元素
  3. 前两种情况递归求解,第三种情况需要计算从中点向左的最大后缀和从中点向右的最大前缀和,两者相加即为跨中点的最大子数组和。
  4. 三者取最大值。

时间复杂度 :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]]

分治思路

  1. 将建筑列表不断分成两半,直到每半只有 0 或 1 个建筑。
  2. 合并左右两个子天际线:类似于归并排序中合并两个有序数组,用双指针扫描两个子天际线,并维护当前高度(左右子天际线各自的最高建筑高度)。
  3. 因为天际线的关键点是当前最高高度发生变化的位置,合并时比较两个子天际线的当前高度,较高的那个决定轮廓,并在高度变化时记录新的关键点。
  4. 这是一个典型的分治算法,时间复杂度 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;
}

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),是解决复杂问题的一大利器。在面试中,如果直接想到最优解可能困难,但能给出一个正确的分治递归解,通常也是可以接受的起点。

相关推荐
Chase_______2 小时前
LeetCode 1343 题解:定长滑动窗口经典入门题,从暴力枚举到高效优化一文搞懂
算法·leetcode·职场和发展
样例过了就是过了2 小时前
LeetCode热题100 单词拆分
c++·算法·leetcode·动态规划·哈希算法
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:跳跳!
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·跳跳
MediaTea2 小时前
ML:决策树的基本原理与实现
人工智能·算法·决策树·机器学习·数据挖掘
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:独木桥
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·独木桥
忡黑梨2 小时前
eNSP_DHCP配置
c语言·网络·c++·python·算法·网络安全·智能路由器
陈壮实的搬砖日记2 小时前
白话生成式推荐二:MiniOneRec之RQ-VAE
算法
陈壮实的搬砖日记3 小时前
白话生成式推荐二:MiniOneRec之SFT
算法
她说彩礼65万3 小时前
C语言 动态内存管理
c语言·开发语言·算法
Irene19913 小时前
数据排序为什么默认升序
算法·排序