CSP-S 提高级课程中心 / 算法进阶 / 第25讲
CSP-S 提高级 · 算法进阶 · s5-03

分治 算法与 递归合并

把一个啃不动的大问题,砍成若干个长得一样的小问题 —— 掌握「分解 / 解决 / 合并」三步法,从快速幂一路打通到平面最近点对。

3 步分治思想 6 个经典模型 4 个完整案例 O(n log n) 复杂度主线 12 道洛谷原题
本讲 · 第 25/52 讲分治算法
模块 01

情景导航

先用一个「人手不够」的真实困境,让你自己想到分治,再把这种直觉翻译成严谨的三步法。

故事分拣中心的百万包裹

双十一凌晨,某快递分拣中心一天要处理 100 万件包裹。站长把全体 200 名工人叫到传送带前,宣布第一个方案:

「所有包裹排成一队,从第 1 号工人开始,一件一件按省份分类。」——结果第 1 号工人面前堆起一座山,其他 199 人闲着看戏。全部分完需要 十几个小时,第二天的车早就开走了。

第二个方案登场了:先在传送带上按「华东 / 华北 / 华南 / 西部」分成 4 大堆,每堆 25 万件交给 4 个小组;每个小组内部再按省分成若干小堆;小组里的每个工人只负责一个城市。最后把各城市的包裹按车次汇总装车。

结果呢?同样是 100 万件包裹,不到 2 小时就装完了。更神奇的是:每个工人做的工作是完全一样的——「拿到一堆包裹,按一个维度再分堆,分不动了就自己处理」。

把「一个 100 万的大任务」变成「无数个相同的小任务」,再自下而上汇总 —— 这就是分治。

计算机里几乎所有的「高性能」都来自这一招:归并排序、快速排序、快速幂、线段树、CDQ 分治、快速傅里叶变换……它们的骨架,都是今天这 3 步。

💡 为什么分治能变快

方案一里,时间花在「同一层反复做同样的扫描」;方案二里,工作量被摊到了 log 层。规模从 n 变成 n/2 只需要 log₂n 次,而每一层的工作量加起来仍然是 O(n),于是总代价就是 O(n log n)。

分治不是「少做事」,而是「把事分成层次做」。关键在于:分解要均匀(否则层数不降)、合并要便宜(否则层数降了也没用)。

①生活类比
🍰

切蛋糕

一刀切两半、两刀切四块。每块形状都变小,切法却完全一样。

→ 分解 Divide
🏀

淘汰赛赛程表

两人先打、胜者再打。子比赛各自独立,最后决赛把两边结果合起来。

→ 解决 Conquer
🗂️

省→市→区档案室

找一份档案先看省、再看市、再看区,每层只看一个抽屉。

→ 递归结构
💰

银行点钞

100 张一捆、10 捆一沓、10 沓一箱。小额结果逐级汇总成大额。

→ 合并 Merge
②核心定义

分治算法(Divide and Conquer):把规模为 n 的原问题分解成若干个规模更小、形式相同的子问题,分别求解后,把子问题的解合并成原问题的解。

分治三步:① 分解(Divide)—— 把原问题切成若干子问题;② 解决(Conquer)—— 递归求解每个子问题,若子问题足够小则直接求解;③ 合并(Merge)—— 把子问题的答案拼成原问题的答案。

分治递归式:设把规模 n 分成 a 个规模 n/b 的子问题,分解与合并的总代价为 f(n),则 T(n) = aT(n/b) + f(n)。这个式子决定了算法的复杂度。

适用条件:① 问题可以缩小(有明确的规模参数);② 子问题相互独立(不重叠,否则应该用 DP/记忆化);③ 子问题形式相同(同一个递归函数能处理);④ 合并可行且不贵(否则分治白搭)。

📌 本讲核心知识地图

板块核心内容对应大纲
三步法分解 / 解决 / 合并 + 四条适用条件【6】分治算法
复杂度T(n)=2T(n/2)+O(n) → O(n log n),递归树逐层求和【6】分治算法
快速幂T(n)=T(n/2)+O(1) → O(log n),指数折半【6】分治算法
归并求逆序对跨中线贡献 mid-i+1,O(n log n)【6】分治算法
棋盘覆盖残缺棋盘分四块,每层 3 块补骨牌【6】分治算法
最大子段和左半 / 右半 / 跨中线三种情况【6】分治算法
平面最近点对按 x 分治 + 带状区域剪枝,O(n log n)【6】分治算法
③核心口诀
🔑 分治三步

大事化小、小事化了、小果合大;
分解不重叠,合并才便宜 —— 分而治之,合而为一。

🔑 复杂度心法

写出递归式 T(n)=aT(n/b)+f(n),画递归树逐层求和:
2T(n/2)+O(n) → 每层 O(n),共 log n 层 → O(n log n)。

模块 02

知识探究

先把三步法吃透,再用递归树算清复杂度,最后用 4 个完整案例打通 6 个经典模型。

1分治三步与适用条件 核心
1
✂️
分解 Divide

把规模 n 的问题切成 a 个规模 n/b 的子问题

2
🔁
解决 Conquer

递归求解;子问题足够小(如 size=1)时直接返回

3
🧩
合并 Merge

把 a 个子答案整理成原问题的答案

原问题规模 n
分解n → n/b
递归求解同一函数
递归边界size = 1
合并子答案 → 总答案
条件含义不满足会怎样
可缩小能明确写出「规模」并让它变小递归不收敛,栈溢出
子问题独立子问题之间没有交集大量重复计算 → 应改 DP / 记忆化搜索
形式相同子问题与原问题结构一致无法用同一个递归函数处理,只能写死特例
合并可行能高效地把子答案拼起来例如「分治求中位数」合并代价 O(n),反而更慢
🧠 分治(Divide & Conquer)
  • 子问题不重叠,各自独立求解
  • 自底向上合并:先算完子问题再拼
  • 典型:归并排序、快速幂、棋盘覆盖
  • 时间花费在分解与合并上
🧮 动态规划(DP)
  • 子问题大量重叠,需要填表复用
  • 自底向上递推 或 自顶向下记忆化
  • 典型:背包、最长上升子序列
  • 时间花费在状态转移上

一句话区分:把子问题的解「拼」起来的是分治,把子问题的解「查表复用」的是 DP。最大子段和这道题两种方法都成立,正是最好的对照例子。

2递归树与复杂度分析 高频

分治的复杂度不看代码,看递归式。最常用的两条结论,建议直接背下来:

递归式含义解典型算法
T(n) = 2T(n/2) + O(n)分两半,合并要扫一遍O(n log n)归并排序、逆序对、最近点对
T(n) = T(n/2) + O(1)只递归一半,合并 O(1)O(log n)快速幂、二分查找
T(n) = 2T(n/2) + O(1)分两半,合并不花钱O(n)求数组最大值
T(n) = T(n-1) + O(1)每次只减 1(不是分治)O(n)普通线性递归,会退化成链
T(n) = 2T(n-1) + O(1)每次只减 1 且分支加倍O(2ⁿ)朴素子集枚举,必须剪枝
n n/2 n/2 n/4 n/4 n/4 n/4 第0层代价 cn 第1层代价 2·c(n/2)=cn 第2层代价 4·c(n/4)=cn 第log n层共 n 个叶,代价 Θ(n)
图:T(n)=2T(n/2)+cn 的递归树 —— 每一层代价都是 cn,共 log₂n+1 层
📈 递归树怎么读
  1. 每个节点写上它承担的规模,根是 n,往下不断除以 2。
  2. 每一层的所有节点规模加起来仍然是 n(这是分治均匀的关键)。
  3. 若合并代价与规模成正比(O(规模)),则每层代价恒为 cn。
  4. 层数 = 从 n 除以 b 直到 1 的次数 ≈ log_b n。
  5. 总代价 = 层数 × 每层代价 = O(n log n)。

反过来看:若合并代价是 O(1)(比如「求最大值」),则每层代价按 2 倍增长,最底层的 n 个叶子共 Θ(n) 就主导了总量,答案只有 O(n)。

口诀:看每层代价的增长速度,谁最大谁说话。

案例案例1:分治快速幂 入门必修

题目:计算 ab mod p,其中 1 ≤ a, b, p ≤ 10⁹,且 不保证 p 是质数。

🔍 分析:为什么可以用分治

直接乘 b 次要 O(b) 时间,b 到 10⁹ 就超时了。观察到:

ab = (ab/2)²  (b 为偶数)  |   ab = (a⌊b/2⌋)² · a  (b 为奇数)

也就是说:算 ab 只需要算一次 a⌊b/2⌋,再把结果平方。规模从 b 变成 b/2,递归式 T(b) = T(b/2) + O(1) → O(log b)。

易错提醒:中间结果 t * t 有可能溢出 long long,所以要先取模;p = 1 时答案必须为 0(写成 1 % p)。

C++快速幂(递归版 + 迭代版)
// 版本一:递归版,最贴合「分治三步」的描述
long long qpow(long long a, long long b, long long p) {
    if (b == 0) return 1 % p;          // 递归边界:a^0 = 1(p=1 时取 0)
    long long t = qpow(a, b >> 1, p);      // 分解 + 解决:只递归一次
    t = t * t % p;                        // 合并:平方回来
    if (b & 1) t = t * a % p;             // b 是奇数,再乘一个 a
    return t;
}

// 版本二:迭代版(二进制分解),竞赛中最常用
long long qpow2(long long a, long long b, long long p) {
    long long res = 1 % p;
    a %= p;
    while (b) {
        if (b & 1) res = res * a % p;      // 这一位是 1,乘进去
        a = a * a % p;                      // 底数不断平方:a → a^2 → a^4 → a^8 ...
        b >>= 1;                            // 看下一位
    }
    return res;
}

🔍 代码逐段解析 · 快速幂

递归版第 2 行 —— if (b == 0) return 1 % p; 是递归边界:a⁰ = 1,但写成 1 % p 是为了处理 p = 1 的边界(任何数 mod 1 = 0)。

递归版第 3~4 行 —— qpow(a, b >> 1, p) 只递归一次(规模减半),拿到 a^⌊b/2⌋ 后平方;t = t * t % p 边乘边取模防止溢出。

递归版第 5 行 —— b 为奇数时指数多出 1,补乘一个 a;注意此时 t、a 都已小于 p,乘积不超过约 10¹⁸,long long 装得下。

迭代版第 9~10 行 —— res = 1 % p; a %= p; 两个初始化各管一个坑:res 管 p=1,a 管 a≥p。

迭代版循环体 —— b 的每个二进制位从低到高扫描:该位为 1 就把当前的 a(即 a 的 2ᵏ 次幂)乘进答案;无论位是否为 1,a 都要平方、b 右移一位。

易错点 —— ① 循环里先乘 res 再平方 a,两步顺序不能颠倒(平方必须每轮都做);② 忘写 a %= p 时若 a 本身很大,第一次乘法就可能溢出。

1
1
0
1
b=13 的二进制 1101:从右往左,1 的位对应 a⁸、a⁴、a¹ 要乘进答案
⚡ 迭代版快速幂演示:计算 2¹³(b=13=1101₂)
0初始:res = 1,a = 2,b = 13 = 1101₂。目标:把 b 的每个 1 位对应的 2 的幂乘进 res。
1第 1 轮:b 末位是 1 → res = res × a = 1 × 2 = 2(乘入 a¹);然后 a = 2² = 4,b = 6 = 110₂。
2第 2 轮:b 末位是 0 → res 不动,仍为 2;a = 4² = 16(即 a⁴),b = 3 = 11₂。
3第 3 轮:b 末位是 1 → res = 2 × 16 = 32(乘入 a⁴,此时 res = 2⁵);a = 16² = 256(a⁸),b = 1。
4第 4 轮:b 末位是 1 → res = 32 × 256 = 8192(乘入 a⁸,即 2⁵ × 2⁸ = 2¹³);a = 65536,b = 0。
5b = 0 循环结束,输出 res = 8192 = 2¹³。验证:13 = 8 + 4 + 1,恰好乘入了 a⁸、a⁴、a¹ 三个因子。✔
核心:底数 a 每轮平方一次,b 的二进制位决定「乘」还是「不乘」——只做了 4 轮而不是 13 次乘法。
运行结果
输入: 2 10 1000
输出: 24          (2^10 = 1024,1024 mod 1000 = 24)

输入: 3 1000000000 998244353
输出: 227238784   (递归深度仅约 30 层,瞬间完成)

输入: 5 7 1
输出: 0           (1 % p = 0,正确!)

💡 递归版与迭代版的等价关系

迭代版用二进制看 b:把 b 拆成 2 的幂之和,b = 13 = 1101₂ = 8+4+1,于是 a13 = a⁸·a⁴·a。这与递归版的「除以 2 再平方」本质完全一致,只是把递归栈换成了循环变量。

考试提示:递归版好写、好讲思路;迭代版常数更小,且不会有递归开销,正式比赛推荐迭代版。

案例案例2:归并求逆序对 经典

题目:给定序列 a[1..n],求满足 i < j 且 a[i] > a[j] 的数对个数。n ≤ 5×10⁵。

🔍 分析:暴力 O(n²) 必然超时,怎么分?

把序列按位置从中间劈成左半 [l, mid] 和右半 [mid+1, r],那么所有逆序对只可能是三种情况之一:

① 两个数都在左半 → 递归左半解决;② 两个数都在右半 → 递归右半解决;③ 一个在左半、一个在右半(跨中线) → 这是分治要额外处理的部分。

关键技巧:在归并排序的「合并」步骤里,左右两段已经各自有序。用双指针 i、j 合并时,若 a[i] > a[j],说明左半从 i 到 mid 的 每一个元素都 > a[j](因为左半升序),直接一次性贡献 (mid - i + 1) 个逆序对!

复杂度:与归并排序完全相同,T(n)=2T(n/2)+O(n) → O(n log n),而暴力是 O(n²)。

🔍 跨中线贡献的 5 个瞬间(数组 5 3 8 1,mid=2)
0原始数组 [5, 3 | 8, 1],切点在 mid=2。先递归把左半排成 [3, 5]、右半排成 [1, 8](左右内部的逆序对已经统计完毕)。
1合并开始:i 指向左半的 3,j 指向右半的 1。比较 3 > 1 → 左半的 3 和 5 都比 1 大,一次贡献 mid-i+1 = 2-1+1 = 2 对,ans = 2。
2取走 1,j 右移到 8。比较 3 和 8:3 < 8 → 顺序正确,不产生逆序对,取走 3,i 右移到 5。
3比较 5 和 8:5 < 8 → 顺序正确,取走 5,i 已经越过 mid,左半取空。
4把右半剩余的 8 直接搬到临时数组,本轮合并结束,ans 保持 2。全数组逆序对为 (5,3)、(5,1) 共 2 对。
要点:只有在「取右半元素」时才产生逆序对;取左半元素说明左边更小,顺序正确。
C++归并排序求逆序对 — 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, a[MAXN], tmp[MAXN];
long long ans = 0;   // 逆序对最多 n(n-1)/2,n=5e5 时约 1.25e11,必须 long long

void mergeSort(int l, int r) {
    if (l >= r) return;                 // 递归边界:只有一个元素,天然有序
    int mid = l + ((r - l) >> 1);          // 防溢出的中点写法
    mergeSort(l, mid);                     // ① 左半
    mergeSort(mid + 1, r);                 // ② 右半
    int i = l, j = mid + 1, k = l;          // ③ 合并
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) {
            tmp[k++] = a[i++];             // 左边小(或相等),正常取走
        } else {
            tmp[k++] = a[j++];             // 右边小:说明 a[i..mid] 全都 > a[j]
            ans += mid - i + 1;            // ★ 全部一次性计入
        }
    }
    while (i <= mid) tmp[k++] = a[i++];     // 左半剩余
    while (j <= r)   tmp[k++] = a[j++];     // 右半剩余
    for (int t = l; t <= r; t++) a[t] = tmp[t]; // 写回原数组
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    mergeSort(1, n);
    cout << ans << "\n";
    return 0;
}
🔍 代码逐段解析 · 归并求逆序对
1全局定义:a[] 存数据、tmp[] 是合并用的临时数组(必须开在全局,大小同 a);ans 用 long long,因为 n=5×10⁵ 时逆序对可达约 1.25×10¹¹。
2if (l >= r) return; —— 递归边界:区间只剩 0 或 1 个元素时天然有序、没有内部逆序对,直接返回。写成 l > r 会死循环。
3int mid = l + ((r - l) >> 1); —— 防溢出中点写法;先递归 mergeSort(l, mid) 与 mergeSort(mid+1, r),两半各自有序、内部逆序对已统计完。
4三指针合并:i 扫左半、j 扫右半、k 从 l 开始写 tmp(注意 k 的起点是 l 不是 0)。
5if (a[i] <= a[j]) —— 相等时取左半!这既保证计数不把相等元素算成逆序对,又保证归并排序稳定。
6else { tmp[k++] = a[j++]; ans += mid - i + 1; } —— ★ 核心:a[i..mid] 全部 > a[j],一次性累加 mid-i+1 对,避免逐个统计导致退化。
7两个收尾循环 —— 把没有取完的左半或右半剩余元素整体搬进 tmp(剩余部分不会产生新的逆序对)。
8写回 + main —— 合并结果从 tmp[l..r] 写回 a[l..r];main 里先读入再 mergeSort(1, n)(下标从 1 开始),最后直接输出全局 ans。
易错点:① ans 忘开 long long(溢出成负数);② k 从 0 而不是 l 开始写 tmp,写回时错位。
运行结果
输入: 5
      5 3 8 1 2
输出: 7        (逆序对为 (5,3)(5,1)(5,2)(3,1)(3,2)(8,1)(8,2) 共 7 对)

输入: 6
      6 5 4 3 2 1
输出: 15       (完全逆序时是 C(6,2)=15,正好取到最大值)

输入: 3
      1 2 3
输出: 0        (已升序,没有逆序对)

💡 相等元素为什么用 a[i] <= a[j]?

如果写成 a[i] < a[j],相等时会走 else 分支并统计 mid-i+1,把「相等」也算成逆序对,答案偏大。严格逆序对要求 a[i] > a[j],所以相等必须由左半先取走,保证排序的稳定性。

顺带记住:「归并排序天然稳定」正是因为它把相等元素优先保留在左半。

3经典模型:棋盘覆盖 分治模板题

问题:在一个 2k × 2k 的棋盘中,恰有 一个方格残缺(不能覆盖)。用若干张「L 型骨牌」(占 3 个格子的拐角形)把其余格子全部盖住,且不能重叠。输出覆盖方案。

♟ 四分过程:2×2 → 4×4 → 8×8
1分解:无论棋盘多大,先沿正中间横竖各切一刀,把 2k×2k 分成 4 个 2k-1×2k-1 的小棋盘:左上、右上、左下、右下。
2观察:那个残缺方格必然只落在 4 块中的一块里。这一块是「本来就残缺的子问题」,直接递归。
3造残缺:剩下 3 块都没有残缺格,不能直接递归。于是我们在中心点交汇处放 1 张 L 型骨牌,正好各占这 3 块的一个角格 —— 于是这 3 块瞬间都变成了「有一个残缺格」的标准子问题!
4递归:4 个子棋盘全部变成同构问题,递归下去。递归式 T(n) = 4·T(n/2) + O(1),解为 O(n²) —— 恰好等于需要填的格子数,已经最优。
5边界:当棋盘缩到 2×2 时,剩下的 3 个格子恰好一张 L 骨牌盖住,直接输出并返回。这就是分治的停止条件。
核心洞察:「没有残缺就人为造一个残缺」 —— 这是棋盘覆盖最漂亮的一步。
2^k × 2^k 残 残缺格所在的块 → 直接递归 其余 3 块在中点各补 1 个角格 → 变成 4 个同构子问题 T(n) = 4·T(n/2) + O(1) = O(n²) —— 恰好每个格子一次 已是最优下界,无法再快
图:残缺棋盘分四块 + 中心补 L 骨牌
♟ 三个必须想清楚的问题
  • 为什么切中间? 只有对半切,4 块的规模才相等(n/2),递归树才平衡,复杂度才不退化。
  • 为什么补一张骨牌? 把「3 个没有残缺的块」一次性改造成「有残缺的块」,让 4 个分支共用同一段递归代码。
  • 怎么表示残缺? 用一个特殊数字标记(本讲用 0 表示不可覆盖格),骨牌按放置顺序编号写进棋盘。

这类「补一个特殊元素让所有子问题同构」的技巧,在分治里非常常见。

案例案例3:棋盘覆盖完整代码 递归结构

输入:k 与残缺格坐标 (dr, dc)。输出:2k×2k 的覆盖矩阵,0 表示残缺格,其余数字表示骨牌编号。

C++棋盘覆盖 — 完整代码
#include <bits/stdc++.h>
using namespace std;
int board[1 << 8][1 << 8];   // k ≤ 7 时 128×128 足够
int tile = 0;                      // 骨牌编号,每放一张 +1

// (tr,tc) 子棋盘左上角;(dr,dc) 该子棋盘中的残缺格;size 边长
void cover(int tr, int tc, int dr, int dc, int size) {
    if (size == 1) return;              // 边界:只剩一格,无需覆盖
    int t = ++tile;                     // 本层要用的骨牌编号
    int s = size >> 1;                   // 半边长

    // ---- 左上块 ----
    if (dr < tr + s && dc < tc + s) {     // 残缺格就在左上块
        cover(tr, tc, dr, dc, s);
    } else {                            // 否则把左上块的右下角格标为残缺
        board[tr + s - 1][tc + s - 1] = t;
        cover(tr, tc, tr + s - 1, tc + s - 1, s);
    }

    // ---- 右上块 ----
    if (dr < tr + s && dc >= tc + s) {
        cover(tr, tc + s, dr, dc, s);
    } else {
        board[tr + s - 1][tc + s] = t;      // 右上块的左下角格
        cover(tr, tc + s, tr + s - 1, tc + s, s);
    }

    // ---- 左下块 ----
    if (dr >= tr + s && dc < tc + s) {
        cover(tr + s, tc, dr, dc, s);
    } else {
        board[tr + s][tc + s - 1] = t;      // 左下块的右上角格
        cover(tr + s, tc, tr + s, tc + s - 1, s);
    }

    // ---- 右下块 ----
    if (dr >= tr + s && dc >= tc + s) {
        cover(tr + s, tc + s, dr, dc, s);
    } else {
        board[tr + s][tc + s] = t;          // 右下块的左上角格
        cover(tr + s, tc + s, tr + s, tc + s, s);
    }
}

int main() {
    int k, dr, dc; cin >> k >> dr >> dc;
    int n = 1 << k;
    board[dr][dc] = 0;                 // 0 表示不可覆盖的残缺格
    cover(1, 1, dr, dc, n);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) cout << board[i][j] << " \n"[j == n];
    }
    return 0;
}
🔍 代码逐段解析 · 棋盘覆盖
1全局定义:board[128][128] 开在全局自动清零(0 恰好表示「未覆盖 / 残缺」);tile 是骨牌编号计数器,每进入一层递归 ++tile 取一个新编号。
2if (size == 1) return; —— 递归边界:1×1 的子棋盘里那唯一的格子就是残缺格,无需再放骨牌。
3int s = size >> 1; —— 分解:把子棋盘横竖各切一半,得到 4 个 s×s 的小块;参数 (tr,tc) 是子棋盘左上角,(dr,dc) 是本子棋盘坐标系下的残缺格。
4四个 if/else(以左上块为例)—— 残缺格落在该块内(dr < tr+s && dc < tc+s)就直接递归;否则把该块的靠中心角格(左上块的右下角 board[tr+s-1][tc+s-1])标为骨牌 t,并把它当作「人造残缺格」递归。
5同一编号 t 出现 3 次 —— 恰好是没有残缺格的那 3 块各补的 1 个角格,它们围绕中心相邻,天然构成一张 L 型骨牌。
6main —— 读入 k 与残缺格 (dr,dc),先 board[dr][dc] = 0 标记真残缺格,再从 cover(1, 1, dr, dc, 1<<k) 整盘开始递归;输出时 " \n"[j == n] 是「行末不带尾随空格」的技巧。
易错点:① 递归时传给子问题的人造残缺格坐标必须换算成子棋盘自己的左上角坐标系;② 四个块的角格下标(tr+s-1 与 tr+s、tc+s-1 与 tc+s)极易写混。
运行结果(k=2, 残缺格 (1,1))
输入: 2 1 1
输出:
 0  2  3  3
 2  2  1  3
 4  1  1  5
 4  4  5  5

说明:编号 1~5 共 5 张骨牌,覆盖 15 个格子,加 1 个残缺格正好 16 格。
     编号相同的 3 个格子连起来就是一个 L 形,可自行在纸上连线验证。

💡 怎么验证答案对不对

逐张骨牌检查:每个编号 恰好出现 3 次,且这 3 格能连成一个 L 形。也可以数总数:骨牌张数 = (n² − 1) / 3。本例 n=4,(16−1)/3 = 5 张,与输出一致。

4分治求最大子段和 与 DP 对比

问题:给定数组 a[1..n](可含负数),求连续子段的和的最大值。

🔍 分治思路:最大子段只可能落在三个位置

取中点 mid,把数组分成左半 [l, mid] 与右半 [mid+1, r]:

① 完全在左半 → 递归 solve(l, mid);② 完全在右半 → 递归 solve(mid+1, r);③ 横跨 mid → 这是分治必须单独算的部分。

怎么算情况③? 跨中线的子段一定形如「mid 向左延伸的一段 + mid+1 向右延伸的一段」。所以:

s1 = max( a[mid]+a[mid-1]+…+a[i] ),i 从 mid 递减到 l
s2 = max( a[mid+1]+…+a[j] ),j 从 mid+1 递增到 r
情况③答案 = s1 + s2

两个循环各扫 O(n/2),所以合并代价 O(n),递归式 T(n)=2T(n/2)+O(n) → O(n log n)。

对比:DP 做法 O(n)(维护 f[i] = max(f[i-1], 0) + a[i]),比这更快。那为什么还学分治?因为它展示了「跨中线」这一通用套路,是 CDQ 分治的思想源头。

-2
11
-4
13
-5
-2
6
mid=4:左侧 [−2,11,−4,13],右侧 [−5,−2,6]
📐 跨中线最大值怎么算(数组 −2 11 −4 13 −5 −2 6)
0mid = 4,左半 [-2, 11, -4, 13]、右半 [-5, -2, 6]。先递归,已知:左半内部最优 = 20(即 11−4+13),右半内部最优 = 6。
1向左伸的后缀:从 a[4]=13 起往左累加 —— 13 → 13−4=9 → 9+11=20 → 20−2=18。最大值 s1 = 20(取的是 11,−4,13 这段)。
2向右伸的前缀:从 a[5]=−5 起往右累加 —— −5 → −5−2=−7 → −7+6=−1。最大值 s2 = −1(取的是 −5,−2,6 整段)。
3跨中线候选 = s1 + s2 = 20 + (−1) = 19。它与左半 20、右半 6 三者一起取 max。
4max(左 20, 右 6, 跨中 19) = 20。验证:整体最优子段正是 [11, −4, 13],与输出样例一致。✔
5两个延伸循环各只扫半边,合并代价 O(r−l),递归式 T(n)=2T(n/2)+O(n) → O(n log n)。
要点:s1、s2 的初值必须是 −∞,否则全负数时会混入「空段」的 0。
✅ 分治版

复杂度:O(n log n)

核心:左 / 右 / 跨中线 三选最大

额外空间:递归栈 O(log n)

价值:「跨中线」模板可迁移到 CDQ 分治、点分治

⚡ DP 版

复杂度:O(n)

核心:f[i] = max(f[i-1], 0) + a[i],边扫边取 max

额外空间:O(1)(滚动变量)

价值:考场首选,能过 n = 10⁷

C++分治版最大子段和 — 完整代码(含 DP 对照)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
const long long NEG = 0-((1LL << 62));
int n, a[MAXN];

// 分治版:返回区间 [l,r] 的最大子段和
long long solve(int l, int r) {
    if (l == r) return a[l];            // 边界:只有一个元素
    int mid = l + ((r - l) >> 1);
    long long L = solve(l, mid);          // 情况①:完全在左半
    long long R = solve(mid + 1, r);      // 情况②:完全在右半
    // 情况③:跨中线的子段 —— mid 向左的最大后缀 + mid+1 向右的最大前缀
    long long s1 = NEG, sum = 0;
    for (int i = mid; i >= l; i--) { sum += a[i]; s1 = max(s1, sum); }
    long long s2 = NEG; sum = 0;
    for (int i = mid + 1; i <= r; i++) { sum += a[i]; s2 = max(s2, sum); }
    return max(max(L, R), s1 + s2);
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cout << solve(1, n) << "\n";

    // ---- DP 版对照(O(n),考场推荐)----
    long long f = NEG, best = NEG;
    for (int i = 1; i <= n; i++) {
        f = max(f, 0LL) + a[i];           // 前面的和若为负,不如重新开始
        best = max(best, f);
    }
    cout << best << "\n";
    return 0;
}

🔍 代码逐段解析 · 分治最大子段和(含 DP 对照)

solve 第 1~2 行 —— 边界 l == r 时返回 a[l] 本身:单元素区间「非空子段」只有它自己。取 mid 用防溢出写法后递归左右两半。

s1 循环 —— i 从 mid 倒扫到 l,sum 累加出每个「以 mid 结尾向左伸的后缀和」,s1 记录最大值;初值 NEG(负无穷)。

s2 循环 —— i 从 mid+1 正扫到 r,同理求「右半最大前缀」;两者相加即跨中线最优,与 L、R 取 max 返回。

DP 对照段 —— f = max(f, 0) + a[i]:前缀累计为负就归零重来;f、best 初值均为 NEG,保证全负数组输出最大单元素而非 0。

易错点 —— ① DP 初值给 0 会在全负数组输出 0(题目要求子段非空);② s1+s2 若在两段各自为 NEG 时相加会「负上负」出错——本题 l==r 已在边界返回,两段都非空,不会触发,但改模板时要留意。

运行结果
输入: 7
      -2 11 -4 13 -5 -2 6
输出: 20
      20

解释:最佳子段为 [2,4] = 11 + (-4) + 13 = 20。两种方法结果一致。

输入: 5
      -3 -1 -7 -2 -5
输出: -1
      -1

解释:全为负数时,答案是最大的单个元素 -1。DP 版必须把 f、best 初值设为 NEG
      (不能设 0),否则会错误地输出 0(空段)。
5平面最近点对:O(n log n) 的剪枝艺术 提高难点

问题:平面上 n 个点,求最近两点之间的距离。暴力两两枚举 O(n²),n = 10⁵ 时完全不可行。

🔍 分治四步走

① 分解:把所有点按 x 坐标排序,取中位线 x = mid_x 把点集劈成左右两半(各约 n/2 个点)。

② 解决:递归求 dL = 左半最近距离,dR = 右半最近距离,令 d = min(dL, dR)。

③ 合并(难点):最近点对可能一个在左、一个在右。朴素做法要枚举所有跨线点对,仍是 O(n²)。剪枝来了: 一个点若要参与跨线点对并且距离 < d,它的 x 必须落在 [mid_x − d, mid_x + d] 的带状区域内 —— 距离中线更远的点,横坐标差已经 ≥ d 了,不可能更近。

④ 再剪枝:把带状区域内的点按 y 坐标排序,对每个点只检查它后面 y 差 < d 的若干点。重要的几何结论:在 d×2d 的矩形里最多只能放下 6 个「两两距离 ≥ d」的点,所以每个点最多只需比较 6 个邻居。于是合并代价降为 O(n)。

复杂度:T(n) = 2T(n/2) + O(n) → O(n log n)(若每层都重新排序为 O(n log n),可用归并思想把排序融入合并,做到严格 O(n log n))。

(0,0)
(1,1)
(2,2)
(3,4)
(5,5)
按 x 排序后的 5 个点,mid 在 (2,2):左半 {(0,0),(1,1)},右半 {(3,4),(5,5)}
📍 带状区域剪枝演示(5 个点:例1)
1排序分解:按 x 排序得 (0,0),(1,1),(2,2),(3,4),(5,5),mid 取 (2,2)。左半 {(0,0),(1,1)},右半 {(3,4),(5,5)}。
2递归求解:dL = |(0,0)−(1,1)| = √2 ≈ 1.414;dR = |(3,4)−(5,5)| = √5 ≈ 2.236。当前 d = min = √2。
3带状筛选:只保留 |x − 2| < 1.414(即 x ∈ (0.586, 3.414))的点:(1,1)、(2,2)、(3,4) 入带,(0,0) 与 (5,5) 被剪掉——它们离中线太远,不可能构成更近的跨线点对。
4按 y 有序逐点比较:(1,1) 与 (2,2) 距离 √2,不更近;(2,2) 与 (3,4) 距离 √5,不更近;查 (1,1) 时 y 差到 3 > d 直接停止。
5结论:跨线没有更近的点对,答案保持 d = √2 ≈ 1.4142,即 (0,0) 与 (1,1)。与运行结果一致。✔
要点:剪枝省掉的是「离中线远的点」的全部跨线比较——5 个点只比了 2 次,而不是把左右两半两两枚举的 2×3 = 6 次。
C++平面最近点对 — 完整代码(归并式合并)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
struct P { double x, y; } p[MAXN], t[MAXN];
int n;

bool cmpx(const P &A, const P &B) { return A.x < B.x; }
bool cmpy(const P &A, const P &B) { return A.y < B.y; }
inline double dis(const P &A, const P &B) {
    double dx = A.x - B.x, dy = A.y - B.y;
    return sqrt(dx * dx + dy * dy);
}

// 返回 [l,r] 内的最近距离,同时把 [l,r] 按 y 归并成有序,供上一层用
double solve(int l, int r) {
    if (l == r) return 1e18;             // 只有一个点,不存在点对
    if (l + 1 == r) {                    // 只有两个点,直接算
        double d = dis(p[l], p[r]);
        if (p[l].y > p[r].y) swap(p[l], p[r]);
        return d;
    }
    int mid = l + ((r - l) >> 1);
    double mx = p[mid].x;                 // 先记下中位线,归并后 p[mid] 会变
    double d = min(solve(l, mid), solve(mid + 1, r));

    // ---- 合并:按 y 归并(分治内部维护有序,避免每层重新排序)----
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) t[k++] = (p[i].y <= p[j].y) ? p[i++] : p[j++];
    while (i <= mid) t[k++] = p[i++];
    while (j <= r)   t[k++] = p[j++];
    for (int q = l; q <= r; q++) p[q] = t[q];

    // ---- 带状区域剪枝:只取 |x - mx| < d 的点(此时 p[l..r] 已按 y 有序)----
    int m = 0;
    for (int q = l; q <= r; q++) if (fabs(p[q].x - mx) < d) t[m++] = p[q];
    for (int a = 0; a < m; a++)
        for (int b = a + 1; b < m && t[b].y - t[a].y < d; b++)   // y 差超过 d 立刻停
            d = min(d, dis(t[a], t[b]));
    return d;
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%lf%lf", &p[i].x, &p[i].y);
    sort(p, p + n, cmpx);                 // 只有初始需要按 x 排序
    printf("%.4f\n", solve(0, n - 1));
    return 0;
}
🔍 代码逐段解析 · 平面最近点对
1结构与比较器 —— p[] 存点、t[] 是归并缓冲;cmpx/cmpy 两个比较器,主程序只在最开始按 x 排一次,之后各层靠归并维护 y 有序。
2两个边界 —— 1 个点返回 1e18(表示不存在点对);2 个点直接算距离,并顺手按 y 交换两个点,保证向上层返回时区间已按 y 有序。
3double mx = p[mid].x; —— ★ 关键一行:必须先保存中位线再做后面的 y 归并,因为归并会打乱 p[mid] 位置,之后 p[mid].x 就不再是中线了。
4y 归并段 —— 与归并排序完全同构:双指针把 [l,mid] 与 [mid+1,r] 按 y 合并回 p[l..r],使「带内点已按 y 有序」这一前提成立。
5带内筛选 —— fabs(p[q].x - mx) < d 的点依次拷进 t[0..m-1];此时 t 中点按 y 升序。
6双层检查 —— 外层枚举每个带内点 a,内层 b 从 a+1 开始,一旦 t[b].y - t[a].y >= d 立刻 break——由几何结论保证内层最多比较常数次(约 6 次)。
7main —— 读入 n 与 n 个点,sort(p, p+n, cmpx) 初始按 x 排序,调用 solve(0, n-1)(注意本程序下标从 0 开始),按 %.4f 输出。
易错点:① 忘存 mx 就做归并,中线错位 → 带选错 → 答案错;② 内层循环条件必须带 t[b].y - t[a].y < d,否则合并退化 O(n²)。
运行结果
输入: 5
      0 0
      3 4
      1 1
      5 5
      2 2
输出: 1.4142     (最近点对 (0,0) 与 (1,1),距离 √2 ≈ 1.4142)

输入: 3
      0 0
      10 0
      5 0.5
输出: 5.0249    ((5,0.5) 到两端点的距离相同,均为 √25.25 ≈ 5.0249)

规模测试:n = 100000 时,暴力 O(n²) 约 5×10⁹ 次运算必然超时,
          本算法约 1.7×10⁶ 次,可在 1s 内轻松通过。

💡 为什么「每个点最多比 6 次」

设想以中线为界、宽 d、高 2d 的矩形。把它切成 6 个 (d/2)×(2d/3) 的小矩形,每个小矩形的对角线长度 = √((d/2)² + (2d/3)²) ≈ 0.83d < d。

如果同一个小矩形里有两个点,它们距离就 < d,与「d 已经是当前最近距离」矛盾。所以一个小矩形最多 1 个点,整个矩形最多 6 个点。这就是 O(1) 剪枝的理论依据。

考场小技巧:循环写成 for (int b = a + 1; b < m && t[b].y - t[a].y < d; b++) 就够了,不必真的限制「6 次」,因为 y 差 < d 的窗口内点数天然很少。

模块 03

实践应用

完整题目 + 样例 + 多角度分析 + 代码 + 运行结果。

题1逆序对(归并分治)
B分治 / 归并洛谷 P1908

题目描述

猫猫 TOM 和小老鼠 JERRY 最近在玩一个游戏:给定一个长度为 n 的序列 a,求序列中逆序对的总数。逆序对定义为 i < j 且 a[i] > a[j] 的有序对 (i, j)。

输入格式:第一行一个整数 n;第二行 n 个整数 a[1..n]。

输出格式:一行一个整数,表示逆序对总数。

数据范围:n ≤ 5×10⁵,1 ≤ a[i] ≤ 10⁹。时间限制 1s,空间限制 125MB。

输入样例:6
6 5 4 3 2 1
  输出样例:15

🔍 方法对比

方法一 · 暴力 双重循环枚举所有 (i, j),a[i] > a[j] 就计数。复杂度 O(n²),n = 5×10⁵ 时约 1.25×10¹¹ 次比较,必然超时。

方法二 · 归并分治 递归二分 + 合并时统计跨中线贡献,复杂度 O(n log n),约 10⁷ 次操作,轻松通过。

方法三 · 树状数组 离散化后从右往左插入,统计比当前值小的已插入个数。也是 O(n log n),代码稍长(见本讲 C 组练习)。

关键:答案最大约 n(n−1)/2 ≈ 1.25×10¹¹,必须用 long long,int 会溢出成负数。

C++P1908 逆序对 — 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, a[MAXN], tmp[MAXN];
long long ans;

void msort(int l, int r) {
    if (l >= r) return;
    int mid = l + ((r - l) >> 1);
    msort(l, mid); msort(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];
        else { ans += mid - i + 1; tmp[k++] = a[j++]; }
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r)   tmp[k++] = a[j++];
    for (int t = l; t <= r; t++) a[t] = tmp[t];
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    msort(1, n);
    cout << ans << "\n";
    return 0;
}

🔍 代码逐段解析 · P1908 逆序对

全局区(第 1~5 行) —— a 与 tmp 同开 5×10⁵+5;ans 是 long long 全局变量,自动清零,递归函数直接往里累加,免传引用。

msort 合并段 —— 与「案例2」同构:a[i] <= a[j] 取左,否则取右并 ans += mid - i + 1;相等走左边保证稳定。

main —— 关同步加速读入(n 达 5×10⁵);下标从 1 开始读入与递归,输出 ans 换行。

易错点 —— ① 用 cin 不关同步可能被卡常;② tmp 数组若开在函数内(栈上)会爆栈,必须开全局。

运行结果
输入: 6
      6 5 4 3 2 1
输出: 15

输入: 5
      1 3 2 5 4
输出: 2      (只有 (3,2) 和 (5,4) 两对)

输入: 1
      7
输出: 0

💡 换个角度想

如果题目改成「求逆序对的值之和」(即每对 a[i]+a[j] 的和),归并里的 ans += mid-i+1 就要改写成对左半区间的求和,可以再用一个前缀和加速。这正是本讲「分治 + 前缀和」的组合套路。

题2最大子段和(分治 vs DP)
B分治 / DP洛谷 P1115

题目描述

给出一个长度为 n 的整数序列 a,选出其中连续且非空的一段,使得这段的和最大。输出这个最大的和。

输入格式:第一行一个整数 n;第二行 n 个整数。

输出格式:一行一个整数,表示最大子段和。

数据范围:1 ≤ n ≤ 2×10⁵,−10⁴ ≤ a[i] ≤ 10⁴。1s / 256MB。

输入样例:7
-2 11 -4 13 -5 -2 6
  输出样例:20

🔍 方法一(O(n log n),分治)

每次取中点,分别递归求左半、右半的答案,再 O(n) 求出「跨中线」的最大值(mid 向左最大后缀 + mid+1 向右最大前缀),三者取 max。

🔍 方法二(O(n),DP)

设 f[i] 为「以 i 结尾的最大子段和」,则 f[i] = max(f[i-1], 0) + a[i]。含义:如果前面累计为负,不如从 i 重新开始。答案 = max(f[i])。空间可压缩为一个变量。

推荐 DP 更快更短;但要会分治,因为「跨中线」是 CDQ 分治等高级算法的核心。

易错:初值必须设为 −∞ 而不是 0!否则全负数数组会输出 0(空段),而题目要求子段非空。

C++两种解法对照(同一份代码输出两次)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
const long long NEG = LONG_LONG_MIN / 4;
int n, a[MAXN];

long long divConq(int l, int r) {
    if (l == r) return a[l];
    int mid = (l + r) >> 1;
    long long L = divConq(l, mid), R = divConq(mid + 1, r);
    long long s1 = NEG, sum = 0;
    for (int i = mid; i >= l; i--) { sum += a[i]; s1 = max(s1, sum); }
    long long s2 = NEG; sum = 0;
    for (int i = mid + 1; i <= r; i++) { sum += a[i]; s2 = max(s2, sum); }
    return max(max(L, R), s1 + s2);
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cout << "分治: " << divConq(1, n) << "\n";

    long long f = NEG, best = NEG;
    for (int i = 1; i <= n; i++) { f = max(f, 0LL) + a[i]; best = max(best, f); }
    cout << "DP  : " << best << "\n";
    return 0;
}

🔍 代码逐段解析 · P1115 分治 vs DP

NEG 常量 —— LONG_LONG_MIN / 4:取「足够小但参与加减不会溢出」的负无穷替身,这是处理 −∞ 初值的安全写法。

divConq —— 边界 l == r 返回 a[l];左右递归后用两个 O(n) 循环求「mid 向左最大后缀 s1」与「mid+1 向右最大前缀 s2」,max(max(L,R), s1+s2) 返回。

DP 三行 —— f = max(f, 0) + a[i] 实现「前面的和为负就重新开始」;best 记录全程最大 f。滚动变量,空间 O(1)。

易错点 —— ① divConq 的 mid 用 (l+r)>>1 在本题范围内安全,但数据更大时要换防溢出写法;② 两种方法输出前都别把初值设成 0。

运行结果
输入: 7
      -2 11 -4 13 -5 -2 6
输出: 分治: 20
      DP  : 20

输入: 4
      -5 -2 -9 -1
输出: 分治: -1
      DP  : -1
题3快速幂取模(含大指数)
A分治 / 数论洛谷 P1226

题目描述

给你三个整数 a、b、p,求 ab mod p,并按要求输出「a^b mod p = 结果」的形式。

输入格式:一行三个整数 a b p。

输出格式:一行,形如 a^b mod p=s。

数据范围:1 ≤ a, b, p ≤ 10⁹。1s / 128MB。

输入样例:2 10 9  输出样例:2^10 mod 9=7

🔍 分析

暴力连乘 b 次,b = 10⁹ 时必然超时。分治做法把指数折半:ab = (a⌊b/2⌋)² ×(b 为奇数时再乘 a),递归深度只有 ⌈log₂ 10⁹⌉ = 30 层,O(log b)。

三个必须处理的坑:① 中间乘法要用 long long(a ≈ 10⁹ 时 a×a ≈ 10¹⁸,接近 long long 上限但不会溢出);② p 可能等于 1,此时答案必须是 0,所以写 1 % p;③ a 要先对 p 取模。

C++P1226 快速幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;

long long qpow(long long a, long long b, long long p) {
    long long res = 1 % p;
    a %= p;
    while (b > 0) {
        if (b & 1) res = (__int128)res * a % p;   // 更保险:用 __int128 防溢出
        a = (__int128)a * a % p;
        b >>= 1;
    }
    return res;
}

int main() {
    long long a, b, p; cin >> a >> b >> p;
    cout << a << "^" << b << " mod " << p << "=" << qpow(a, b, p) << "\n";
    return 0;
}

🔍 代码逐段解析 · P1226 快速幂

qpow 初始化两行 —— res = 1 % p 处理 p = 1(答案必须是 0);a %= p 保证后续乘法都在 [0, p²) 范围内。

while (b > 0) 循环 —— 逐位扫描 b:末位为 1 时 res = (__int128)res * a % p,用 __int128 做中间乘法再取模,防溢出最保险;每轮 a = (__int128)a * a % p 底数平方、b >>= 1 右移。

main 输出格式 —— P1226 要求输出形如 a^b mod p=s,注意本题样例格式里 mod 与等号之间没有空格,照题面拼串即可。

易错点 —— ① 输出格式与题面不一致(多/少空格)会白白丢分;② p = 1 时若 res 初值写 1 会输出 1,正确答案是 0。

运行结果
输入: 2 10 9
输出: 2^10 mod 9=7

输入: 1000000000 1000000000 1000000000
输出: 1000000000^1000000000 mod 1000000000=0

输入: 7 1 1
输出: 7^1 mod 1=0

💡 考场上到底要不要用 __int128

当 p ≈ 10⁹ 时,a × a 最坏约 (10⁹)² = 10¹⁸ < 9.2×10¹⁸,long long 刚好装得下,一般 long long 就够了。

但如果 p 达到 10¹⁸(比如某些数论题),乘法就会溢出,此时必须用 __int128 或「龟速乘」(用加法版的快速幂,也是分治思想,O(log n))。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心复杂度
分治三步分解 → 解决 → 合并;子问题必须独立、同构—
复杂度分析写递归式 T(n)=aT(n/b)+f(n),画递归树逐层求和—
快速幂ab = (ab/2)²,奇数多乘一个 a;初值 1 % pO(log b)
归并求逆序对取右半元素时 ans += mid - i + 1O(n log n)
棋盘覆盖分四块,中心补一张 L 骨牌使 4 块同构O(4ᵏ)
最大子段和左 / 右 / 跨中线(左最大后缀 + 右最大前缀)分治 O(n log n) / DP O(n)
平面最近点对按 x 分治,只查 |x−mid| < d 的带内点,每点最多比 6 次O(n log n)
数据类型逆序对、子段和都要 long long;递归边界先写—
C++分治万能骨架模板
#include <bits/stdc++.h>
using namespace std;

// ---------- 通用分治模板:把 [l, r] 上的问题递归求解 ----------
ReturnType solve(int l, int r) {
    // ① 递归边界:规模小到可以直接算出答案
    if (l == r) return 直接答案;

    // ② 分解:取中点(或按值域 / 坐标分),保证两边规模尽量相等
    int mid = l + ((r - l) >> 1);

    // ③ 解决:递归处理两个(或多个)子问题
    ReturnType A = solve(l, mid);
    ReturnType B = solve(mid + 1, r);

    // ④ 合并:处理「跨中点」的额外情况,再与 A、B 取最优
    ReturnType C = 跨中线的答案;      // 通常是 O(规模) 的一次扫描
    return 综合(A, B, C);
}

// ---------- 使用前必做的三件事 ----------
// 1) 写出递归式,估算是否会超时(T(n)=2T(n/2)+O(n) → O(n log n))
// 2) 检查递归边界是否覆盖「size = 1」和「size = 2」两种情况
// 3) 检查中间量是否溢出(逆序对用 long long,乘法用 __int128)

🔍 代码逐段解析 · 分治万能骨架

① 递归边界 —— 模板里写的是 l == r(区间剩 1 个元素);若你的合并逻辑依赖「两段都非空」,边界足够;若子问题可能为空段(如某些区间统计),要额外补 l > r 的处理。

② 分解 mid —— 统一写 l + ((r - l) >> 1);分治「按值域/坐标分」时把这一行换成对应的切分方式即可,但必须保证两半规模尽量均衡。

③ 解决 A、B —— 两个递归调用顺序无要求(子问题独立);注意若用全局累加器(如逆序对的 ans),函数可不返回值。

④ 合并 C —— 「跨中线」扫描是分治的灵魂,常见形态:双指针归并(逆序对/最近点对)、从 mid 向两侧延伸(最大子段和)、造同构子问题(棋盘覆盖)。

使用前三件事(注释区) —— 先算递归式防超时、再检查边界覆盖 size=1/2、最后检查中间量溢出,三步缺一不可。

②拓展案例
拓展1分治 + 归并

拓展 1:区间和的「顺序对」计数(跨中线贡献再上一层)

给定数组 a[1..n],统计满足 i < j 且 a[i] < a[j] 的数对个数(即「顺序对」)。n ≤ 2×10⁵。

思路一(归并改造):把逆序对的判断从 a[i] > a[j] 改成 a[i] < a[j],此时取左半元素时说明左半的 a[i] 小于右半剩余的 a[mid+1..r],一次性贡献 r - j + 1 对。

思路二(总数减法):顺序对 + 逆序对 + 相等对 = C(n, 2)。若没有相等元素,直接算总对数减去逆序对即可,省一次写法改造。

// 思路一核心改动:把贡献挪到「取左半」的分支
while (i <= mid && j <= r) {
    if (a[i] < a[j]) { ans += r - j + 1; tmp[k++] = a[i++]; }   // ★
    else tmp[k++] = a[j++];
}
运行结果
输入: 4
      4 3 2 1
输出: 0      (完全逆序,没有任何顺序对)

输入: 4
      1 2 3 4
输出: 6      (C(4,2)=6,全部都是顺序对)
拓展2分治 / 数论

拓展 2:龟速乘 —— 当乘法会溢出时用加法分治

求 a × b mod p,其中 1 ≤ a, b, p ≤ 10¹⁸。此时 a×b 会远超 long long,不能直接乘。

思路:把乘法拆成加法,用与快速幂完全相同的折半结构:a×b = (a×⌊b/2⌋)×2(b 偶),= (a×⌊b/2⌋)×2 + a(b 奇)。每一步只做加法和取模,全程不溢出,复杂度 O(log b)。

long long mul(long long a, long long b, long long p) {
    long long res = 0;
    a %= p;
    while (b) {
        if (b & 1) res = (res + a) % p;   // 加法代替乘法,绝不溢出
        a = (a + a) % p;                    // a 翻倍
        b >>= 1;
    }
    return res;
}
// 有了龟速乘,快速幂也可以写成「乘法换成 mul」,支持 p 到 10^18

🔍 代码逐段解析 · 龟速乘

初始化 —— res = 0; a %= p; 加法版的「乘法单位元是 0」,a 先落到 [0, p) 内。

循环体 —— 与快速幂逐位对应:b 末位为 1 时 res = (res + a) % p(加法代替乘法,两数均 < p,和 < 2p 绝不溢出);a = (a + a) % p 每轮把 a 翻倍,对应「底数平方」。

复杂度与代价 —— O(log b) 次加法;比 __int128 慢但可移植性最好,p 接近 10¹⁸ 时的标准做法。

易错点 —— 翻倍那行写成 a *= 2 不取模会在 b 很大时溢出;两处 % p 一个都不能省。

运行结果
输入: 1000000000000000000 1000000000000000000 1000000007
输出: 2401        (全程无溢出,结果正确;结果必然小于模数 10⁹+7)

对比:直接写 a * b % p 会因为乘法溢出得到完全错误的答案。
拓展3分治 / 第 k 小

拓展 3:分治求第 k 小(快速选择思想,平均 O(n))

给定 n 个数(n ≤ 10⁷),求第 k 小的数。排序要 O(n log n),有没有更快的方法?

思路:借用快速排序的 partition:随机选一个基准值,把数组分成「小于基准」「等于基准」「大于基准」三段。若第 k 小落在「小于」段,只递归左段;落在「大于」段,递归右段并调整 k;否则直接返回基准值。

复杂度:每次只需要递归一半,期望规模 n + n/2 + n/4 + … = 2n,即 平均 O(n)。这是「分治只递归一半」的经典应用,也是下一讲快速排序的近亲。

// 三路划分 + 只递归一侧
int kth(int *a, int l, int r, int k) {
    int p = a[l + rand() % (r - l + 1)];   // 随机基准,防退化
    int lo = l, hi = r, i = l;
    while (i <= hi) {
        if (a[i] < p) swap(a[lo++], a[i++]);
        else if (a[i] > p) swap(a[i], a[hi--]);
        else i++;
    }
    if (k < lo) return kth(a, l, lo - 1, k);      // 落在左段
    if (k > hi) return kth(a, hi + 1, r, k);      // 落在右段
    return a[k];                                  // 落在 [lo,hi] 的等于段
}
运行结果
输入: 10 3
      9 8 7 6 5 4 3 2 1 0
输出: 2

解释:升序后为 0 1 2 3 ... 9,第 3 小(从 1 开始数)即下标 2 的元素,值为 2。
规模测试:n = 10⁷ 时平均约 2×10⁷ 次操作,1s 内可过;排序则需要约 2.3×10⁸ 次。
③思考与启发

💡 思考 1:合并代价如果大于 O(n) 会怎样?

例如把「分解 + 合并」的代价提高到 O(n²),递归式变成 T(n) = 2T(n/2) + O(n²),答案会是多少?还值得用分治吗?

查看参考思路

递归树每层代价 = 2^k · c(n/2^k)² = c·n²/2^k,是等比递减的(公比 1/2)。

求和 c·n²(1 + 1/2 + 1/4 + …) = 2c·n² = O(n²),由根节点主导。

结论:合并太贵的分治没有意义 —— 复杂度与直接暴力同阶,还多了递归开销。这正对应「适用条件④:合并必须便宜」。

推广:递归树三定律 —— 若每层代价等比递增,叶子主导;相等,乘层数;递减,根主导。这就是主定理的直观版本。

💡 思考 2:为什么分治天然适合并行计算?

在「分拣中心」的故事里,4 个小组可以同时开工。从算法的角度看,这说明了分治的什么性质?

查看参考思路

分治要求子问题相互独立、无数据依赖,所以子问题可以真正同时执行。这是「多核加速」与「分布式计算」的理论基础(MapReduce 的 Map 阶段本质就是分治的分解步骤)。

反过来,动态规划因为子问题互相依赖(f[i] 依赖 f[i-1]),很难并行。

但代价也在:分治需要额外的合并阶段,而合并天生是串行的。分治的效率上限由「串行的合并部分」决定,这就是并行计算中的 Amdahl 定律。

💡 思考 3:什么时候应该改用记忆化搜索 / DP?

如果分治过程中出现了「同一个子问题被反复计算」,会发生什么?怎么补救?

查看参考思路

一旦子问题重叠,朴素分治会退化成指数级(例如斐波那契递归 T(n) = T(n-1)+T(n-2) → O(1.618ⁿ))。

补救方案一:加一个 memo 数组记录「算过的子问题答案」,这就是记忆化搜索(本模块第 8 讲,s5-08)。它把分治骨架保留下来,只多了一个「先查缓存」的动作。

补救方案二:把子问题的依赖顺序理清,改成自底向上填表的动态规划。

判断口诀:子问题不重叠 → 分治;子问题重叠 → 记忆化 / DP。二者不是对立,而是同一棵递归树的两种记账方式。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

⚡高频错误诊断
易错1

递归边界写错,导致死循环

❌ 错误
归并里写 if (l > r) return;,当区间长度是 2 时 mid=l,递归 solve(l,mid) 与 solve(mid+1,r) 会无限重复。
✅ 正确
写 if (l >= r) return; —— 区间只剩 1 个元素时直接返回,能正确终止。边界要覆盖 size=1。
口诀:递归边界先写清,size 为一马上停!
易错2

逆序对计数用 int 溢出

❌ 错误
用 int ans 统计。n = 5×10⁵ 完全逆序时 ans ≈ 1.25×10¹¹,远超 int 上限 2.1×10⁹,答案变成负数。
✅ 正确
用 long long ans = 0;。经验:凡是「数对数」的题,先想会不会超过 2×10⁹。
口诀:数对统计用 long long,十倍规模不翻车!
易错3

最大子段和初值给 0

❌ 错误
long long best = 0; 遇到全负数数组会输出 0,但题目要求子段非空,正确答案应该是最大那个负数。
✅ 正确
初值设为 −∞:long long NEG = LLONG_MIN / 4;,保证任何合法子段和都比它大。
口诀:最大子段初值负无穷,全负数组也不空!
易错4

跨中线只在「一半」里找,漏掉真正的解

❌ 错误
认为「最优子段要么全在左半、要么全在右半」,只写 max(L, R),忘记跨中线的情况。
✅ 正确
必须补上第三种:左半最大后缀 s1 + 右半最大前缀 s2,再取三者最大。逆序对、最大子段和、最近点对的「合并」步骤都是这个套路。
口诀:左边右边加跨中,三种情况一个不漏!
易错5

快速幂在 p = 1 时输出 1

❌ 错误
初始写成 long long res = 1;,当 p = 1 时循环不改变 res,最后输出 1 —— 但任何数 mod 1 都应该是 0。
✅ 正确
写 long long res = 1 % p;,或特判 if (p == 1) return 0;。取模类题目永远先想「模数为 1 怎么办」。
口诀:初值取 1 再模 p,p 等于一不出错!
📋速查表
模型分解方式合并 / 关键式子复杂度
快速幂指数折半t = t*t % p;b 奇再乘 aO(log b)
龟速乘乘数折半res = (res+a) % p;a = (a+a) % pO(log b)
归并求逆序对按下标二分取右半时 ans += mid - i + 1O(n log n)
棋盘覆盖按 2×2 四块分中心补 1 张 L 骨牌使 4 块同构O(n²)
最大子段和按下标二分max(左, 右, 左最大后缀 + 右最大前缀)O(n log n)
平面最近点对按 x 坐标二分只查 |x−mid| < d 的带内点,按 y 扫描O(n log n)
第 k 小三路划分,只递归一侧按 k 落在哪段决定递归方向平均 O(n)
递归树求和—层数 × 每层代价,或看主导项—
📌笔记卡
📐 必记概念
  • 分治三步:分解 / 解决 / 合并
  • 递归式 T(n)=aT(n/b)+f(n)
  • 子问题必须不重叠且同构
  • 2T(n/2)+O(n) → O(n log n)
  • 跨中线 = 分治必补的第三类情况
💻 必背模板
  • 快速幂:指数折半 + 奇数补乘
  • 归并逆序对:ans += mid - i + 1
  • 边界统一写 if (l >= r) return;
  • 中点统一写 l + ((r - l) >> 1)
  • 累加量一律 long long
⚠️ 考场提醒
  • 先写递归边界,再写递归体
  • 先估复杂度,再决定写不写分治
  • 前缀和 / 排序只在最外层做一次
  • p = 1、全负数、n = 1 三个边界必测
  • 递归深度慎防爆栈(必要时改迭代)
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1编程题

用分治求数组最大值:给定 n 个数,用「二分后各求一半」的方式求出最大值。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
int a[100005];
int mx(int l, int r) {
    if (l == r) return a[l];
    int mid = (l + r) >> 1;
    return max(mx(l, mid), mx(mid + 1, r));
}
int main() {
    int n; cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cout << mx(1, n) << endl;
}

🔍 逐段解析:mx(l,r) 边界 l==r 返回 a[l];否则取 mid 递归两半、max(...) 合并——分治骨架的标准 4 行。main 读入后调用 mx(1, n)。

思路:区间只有一个元素时直接返回该元素;否则取中点,分别求左右半区最大值,再取较大者合并。

考点:递归式 T(n) = 2T(n/2) + O(1),递归树每层合并 O(1),最底层 n 个叶子共 O(n),故总复杂度 O(n)。

易错:把边界写成 l > r 而没有处理单元素;或忘记在合并时取 max 而是直接相加。

A2编程题

快速幂:输入 a、b、p,输出 ab mod p(a, b, p ≤ 10⁹)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
long long qpow(long long a, long long b, long long p) {
    long long res = 1 % p; a %= p;
    while (b) {
        if (b & 1) res = res * a % p;
        a = a * a % p; b >>= 1;
    }
    return res;
}
int main() {
    long long a, b, p; cin >> a >> b >> p;
    cout << qpow(a, b, p) << endl;
}

🔍 逐段解析:函数内三件套——res = 1 % p(防 p=1)、a %= p(防溢出)、循环内「末位为 1 乘 res → a 平方 → b 右移」三步固定顺序。main 直接输出返回值。

思路:把指数 b 按二进制拆开,底数不断平方,遇到 1 就乘进答案,相当于 O(log b) 次乘法完成原来 O(b) 次的工作。

易错:初始写成 res = 1 导致 p=1 时输出 1;或 a %= p 漏写,a 本身大于 p 时结果偏大。

A3编程题

归并排序:给定 n 个数,输出从小到大排好序的序列(用分治实现,不许调用 sort)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
int n, a[100005], tmp[100005];
void ms(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) >> 1;
    ms(l, mid); ms(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];
    for (int t = l; t <= r; t++) a[t] = tmp[t];
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    ms(1, n);
    for (int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n];
}

🔍 逐段解析:ms(l,r) 四部分——边界 l>=r、递归两半、三指针合并(k 从 l 起、tmp 全局开)、写回 a[t]=tmp[t]。输出行 " \n"[i==n] 控制行末无尾随空格。

思路:先递归把左右两半各自排好,再用双指针把两个有序序列合并成一个有序序列。

易错:合并时用 a[i] < a[j] 会破坏稳定性;写回时忘记从 l 开始,用 0 开始写回会覆盖已排好的数据。

A4编程题

二分查找:在升序数组中查找 x 第一次出现的位置和最后一次出现的位置(用分治二分)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
int n, a[100005], x;
int main() {
    cin >> n >> x;
    for (int i = 1; i <= n; i++) cin >> a[i];
    int lo = 1, hi = n, first = -1, last = -1;
    while (lo <= hi) {                       // 找第一个 >= x 的位置
        int mid = (lo + hi) >> 1;
        if (a[mid] >= x) { first = mid; hi = mid - 1; } else lo = mid + 1;
    }
    lo = 1; hi = n;
    while (lo <= hi) {                       // 找最后一个 <= x 的位置
        int mid = (lo + hi) >> 1;
        if (a[mid] <= x) { last = mid; lo = mid + 1; } else hi = mid - 1;
    }
    if (first == -1 || a[first] != x) cout << "-1 -1" << endl;
    else cout << first << " " << last << endl;
}

🔍 逐段解析:两个独立的二分循环——第一个在 a[mid] >= x 时记录 mid 并收缩右端(找第一个 ≥ x 的位置),第二个在 a[mid] <= x 时记录 mid 并收缩左端(找最后一个 ≤ x 的位置);最后用 a[first] != x 判断「根本不存在」。

思路:二分找下界和上界,是「只递归一半」的经典分治。a[first] != x 用于区分「没找到」和「找到但恰好是 -1 位置」。

易错:循环条件写成 lo < hi 会导致某些情况漏判;中点写 (lo + hi) / 2 在 lo、hi 很大时可能溢出(本题不会,但习惯要好)。

🟡 B组 · 提高(4题)

B1分治 / 逆序对

给定序列,求逆序对总数(n ≤ 5×10⁵)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
int n, a[500005], tmp[500005];
long long ans;
void ms(int l, int r) {
    if (l >= r) return;
    int mid = l + ((r - l) >> 1);
    ms(l, mid); ms(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];
        else { ans += mid - i + 1; tmp[k++] = a[j++]; }
    }
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];
    for (int t = l; t <= r; t++) a[t] = tmp[t];
}
int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    ms(1, n); cout << ans << endl;
}

🔍 逐段解析:与案例2完全同构——全局 ans 用 long long;合并时 a[i] <= a[j] 取左、否则 ans += mid - i + 1 取右;k 从 l 起、最后写回 a[l..r]。不需要离散化。

考试提示:a[i] 可达 10⁹,但本题不需要离散化 —— 归并统计只比较大小,不当下标用。若改用树状数组统计,则必须先离散化。

B2分治 / 最大子段和

求最大子段和(子段非空,n ≤ 2×10⁵)。要求用分治实现。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
const long long NEG = LLONG_MIN / 4;
int n, a[200005];
long long dc(int l, int r) {
    if (l == r) return a[l];
    int mid = (l + r) >> 1;
    long long L = dc(l, mid), R = dc(mid + 1, r);
    long long s1 = NEG, s = 0;
    for (int i = mid; i >= l; i--) { s += a[i]; s1 = max(s1, s); }
    long long s2 = NEG; s = 0;
    for (int i = mid + 1; i <= r; i++) { s += a[i]; s2 = max(s2, s); }
    return max(max(L, R), s1 + s2);
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cout << dc(1, n) << endl;
}

🔍 逐段解析:dc(l,r)——边界返回 a[l];递归左右后,s1 循环从 mid 向左累加求最大后缀、s2 循环从 mid+1 向右累加求最大前缀,返回 max(max(L,R), s1+s2)。s1、s2 初值 NEG 是本题命门。

易错:s1、s2 初值必须给 −∞;若给 0,全负数时会错误地取到「左后缀 + 右前缀 = 0」这种空段组合。

B3分治 / 计数

统计「顺序对」:满足 i < j 且 a[i] < a[j] 的数对个数。

查看参考答案

思路:在归并合并阶段,当取走左半元素 a[i] 时,说明 a[i] 小于右半剩余的 a[j..r],一次贡献 r - j + 1 对。

while (i <= mid && j <= r) {
    if (a[i] < a[j]) { ans += r - j + 1; tmp[k++] = a[i++]; }
    else tmp[k++] = a[j++];
}

另一种做法:若干元素互不相同,则顺序对数 = C(n,2) − 逆序对数,直接复用逆序对的代码即可,更省事。

易错:把 a[i] < a[j] 写成 a[i] <= a[j],会把相等元素也算成顺序对;或忘记 mid = a + (b-a)/2 这类防溢出写法(本题 n 不大,但习惯要好)。

B4分治 / 数学

计算 1 + 2 + … + n(n ≤ 10⁹)用分治思路实现,体会 T(n)=T(n/2)+O(1)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
long long sum(long long l, long long r) {
    if (l == r) return l;
    long long mid = l + (r - l) / 2;
    return sum(l, mid) + sum(mid + 1, r);
}
int main() {
    long long n; cin >> n;
    cout << sum(1, n) << endl;              // 递归版 O(n),会超时,仅作演示
    cout << n * (n + 1) / 2 << endl;      // 公式版 O(1)
}

🔍 逐段解析:sum(l,r) 是「区间加法」形态——边界 l==r 返回 l,否则左右递归后相加合并(合并 O(1) 但两侧都递归,T(n)=2T(n/2)+O(1)=O(n),叶子多达 n 个故不省时);公式版注意 n(n+1) 要 long long。

结论:本例题的递归式其实是 T(n) = 2T(n/2) + O(1) = O(n),并不比循环快。这说明「分治不等于加速」——只有把问题缩成「只递归一侧」(如二分、快速幂)才会得到 log 级复杂度。

易错:n 到 10⁹ 时递归深度约 30 层但叶子数达 10⁹ 个,实际会超时;公式版注意 n(n+1) 会超过 int,必须 long long。

🔴 C组 · 挑战(4题)

C1初赛真题

某分治算法满足 T(n) = 2T(n/2) + O(n),T(1) = O(1),则 T(n) 的渐进复杂度是?

A. O(n)    B. O(n log n)    C. O(n²)    D. O(log n)

查看参考答案

答案:B. O(n log n)

递归树每层代价恒为 cn(子问题规模之和始终是 n),层数为 log₂n + 1,故总代价 = cn(log n + 1) = O(n log n)。

考点:递归树求和法。若合并代价是 O(1),答案是 O(n);若合并代价是 O(n²),答案是 O(n²)(根主导)。

易错:把 2T(n/2) 的「2」误当成系数直接乘,得到 O(n log n) 以外的答案;或忘记 log 的底数不影响复杂度等级。

C2综合分析

棋盘覆盖:证明「补一张 L 骨牌」后,4 个子棋盘都恰好有 1 个不可覆盖格。

查看参考答案

证明:设原棋盘为 2ᵏ×2ᵏ,沿中线分成 4 块 2ᵏ⁻¹×2ᵏ⁻¹ 的子棋盘。原残缺格只存在于其中一块,记为块 A。

在四块交汇的中心点处放置 1 张 L 型骨牌,它占用的 3 个格子分别是:左上块的右下角、右上块的左下角、左下块的右上角(或对称的 3 个角)。

于是:块 A 仍有原残缺格(1 个);另外 3 块各被骨牌占去 1 个角格,那个被占的格子「不能再被覆盖」,等价于残缺格。故 4 块各有恰好 1 个不可覆盖格。

结论:4 个同构子问题成立,可以递归调用同一个函数。骨牌总数 = (4ᵏ − 1) / 3,可用数学归纳法证明每层消耗 1 张骨牌、共 log₂(4ᵏ) = 2k 层。

易错:以为 L 骨牌是「任选 3 个相邻格」,实际上必须盖住中心点周围 3 个不同的子棋盘各一角,否则不满足同构要求。

C3综合编程

平面最近点对:给定 n(n ≤ 10⁵)个点,输出最近两点间的距离(保留 4 位小数)。

查看参考答案

思路:按 x 排序 → 分治求左右半最近距离 d → 合并时只检查 |x − mid_x| < d 的带状区域内的点,且这些点按 y 有序时每个点最多与后面 6 个点比较。

double solve(int l, int r) {
    if (l == r) return 1e18;
    if (l + 1 == r) { double d = dis(p[l], p[r]); if (p[l].y > p[r].y) swap(p[l], p[r]); return d; }
    int mid = (l + r) >> 1; double mx = p[mid].x;
    double d = min(solve(l, mid), solve(mid + 1, r));
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) t[k++] = (p[i].y <= p[j].y) ? p[i++] : p[j++];
    while (i <= mid) t[k++] = p[i++];
    while (j <= r)   t[k++] = p[j++];
    for (int q = l; q <= r; q++) p[q] = t[q];
    int m = 0;
    for (int q = l; q <= r; q++) if (fabs(p[q].x - mx) < d) t[m++] = p[q];
    for (int a = 0; a < m; a++)
        for (int b = a + 1; b < m && t[b].y - t[a].y < d; b++) d = min(d, dis(t[a], t[b]));
    return d;
}

🔍 逐段解析:与案例5同构——先存 mx = p[mid].x,递归两半得 d,y 归并让区间有序,再筛 |x−mx|<d 的带内点,按 y 升序双循环检查(内层靠 y 差剪枝)。主程序还需 sort(p, p+n, cmpx) 初始排序与 %.4f 输出。

易错:必须先保存 mx = p[mid].x 再做 y 归并,否则归并后 p[mid] 已不是中位点,中线位置算错;带内点判断要用 < d 而不是 <= d(结果一样,但前者更快)。

C4分治 / 递推

用分治计算 2ⁿ(n ≤ 10¹⁸)的最后 3 位数字。
进一步:若要求同时输出 n 很大时的 2ⁿ mod 1000,注意 n = 1 时的位数不足问题。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
long long qpow(long long a, long long b, long long p) {
    long long r = 1 % p; a %= p;
    while (b) { if (b & 1) r = r * a % p; a = a * a % p; b >>= 1; }
    return r;
}
int main() {
    long long n; cin >> n;
    if (n < 3) cout << (1LL << n) << endl;      // 2,4 位数不足 3 位,直接输出
    else printf("%03lld\n", qpow(2, n, 1000));  // 补足 3 位前导零
}

🔍 逐段解析:qpow 同 A2(res=1%p、a%=p、逐位平方);main 里 n < 3 特判直接输出 2、4(此时不足 3 位,补零反而不对),其余用 %03lld 输出带前导零的 3 位结果。

思路:求末 3 位等价于 mod 1000,直接用快速幂,n 到 10¹⁸ 也只需约 60 次迭代。

易错:n = 1、2 时 2ⁿ = 2、4,不足 3 位,若强行按 %03lld 输出会变成 002、004 与题意不符,需要特判;此外 2ⁿ 与 10 不互质,不能用欧拉定理降幂,必须老老实实快速幂。

🏆 洛谷原题精选(12道)

以下 12 道题都与分治思想相关,按难度从入门到提高排列,建议按顺序刷完。

1
P1226
【模板】快速幂普及-
2
P1115
最大子段和普及-
3
P1010
幂次方普及-
4
P1030
求先序排列普及-
5
P1087
FBI树普及-
6
P1498
南蛮图腾普及-
7
P1908
逆序对提高
8
P1228
地毯填补问题提高
9
P1257
平面上的最接近点对提高
10
P1966
火柴排队提高
11
P1962
斐波那契数列提高
12
P1045
麦森数普及+