C++信息学奥赛

分治算法详解

分而治之,化繁为简,高效解决复杂问题

分治算法在信息学奥赛中的地位

分治算法(Divide and Conquer)是一种重要的算法设计范式,它通过将复杂问题分解为规模较小的相似子问题,递归地解决子问题,最后合并子问题的解得到原问题的解。

在信息学奥赛中,分治算法是解决大型复杂问题的利器,尤其在处理排序、查找、矩阵运算、几何问题等方面表现出色。掌握分治算法不仅能帮助你解决特定问题,更能培养你将复杂问题分解为简单问题的思维能力。

分治算法基本概念

分治算法(Divide and Conquer)的核心思想是"分而治之",即将一个复杂的问题分解成两个或更多个相同或相似的子问题,直到子问题可以简单地直接求解,然后将子问题的解合并,得到原问题的解。

分治算法的特点

  • 问题可分解:原问题可以分解为若干个规模较小的相同类型子问题
  • 子问题独立:子问题之间相互独立,不包含重叠的子问题
  • 递归求解:子问题的解法与原问题相同,可以递归求解
  • 合并解:需要将子问题的解合并,得到原问题的解

分治算法的基本步骤

分解(Divide)

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

解决(Conquer)

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

合并(Combine)

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

分治与其他算法的对比

算法类型 核心思想 主要应用 时间复杂度
分治算法 分解为子问题,递归求解后合并 排序、查找、矩阵运算、几何问题 通常为O(n log n)
贪心算法 每步选择局部最优,不回溯 优化问题、区间问题、图论问题 通常为O(n log n)
动态规划 存储子问题结果,解决重叠子问题 最优子结构问题、计数问题 通常为O(n²)或O(n³)
回溯算法 尝试所有可能,失败则回溯 排列组合、子集问题、搜索问题 通常为指数级

分治算法的核心原理

分治算法的有效性基于问题的两个关键性质:问题可分解性和最优子结构性质。理解这些性质有助于我们判断一个问题是否适合用分治算法解决。

分治算法的适用条件

  • 1.
    问题规模缩小后更容易解决

    当问题规模足够小时,可以直接求解

  • 2.
    问题可以分解为形式相同的子问题

    原问题和子问题具有相同的结构和求解方法

  • 3.
    子问题的解可以合并为原问题的解

    存在有效的方法将子问题的解合并

  • 4.
    子问题相互独立

    子问题之间不包含重叠的子问题(否则更适合动态规划)

分治算法的时间复杂度分析

分治算法的时间复杂度通常可以用递归关系式表示,对于一个规模为n的问题:

T(n) = a·T(n/b) + f(n)

其中:

  • n:问题规模
  • a:子问题数量
  • n/b:每个子问题的规模(假设均匀分割)
  • f(n):分解和合并子问题的时间

可以使用主方法(Master Theorem)求解这类递归关系式:

  1. 若f(n) = O(n^log_b a-ε),则T(n) = Θ(n^log_b a)
  2. 若f(n) = Θ(n^log_b a),则T(n) = Θ(n^log_b a log n)
  3. 若f(n) = Ω(n^log_b a+ε),且a·f(n/b) ≤ c·f(n),则T(n) = Θ(f(n))

递归与分治的关系

分治算法通常与递归紧密相关,但两者并不等同:

递归是实现分治的常用手段

递归为分治算法提供了自然的实现方式,通过函数自身调用来处理规模逐渐缩小的子问题。但分治也可以用非递归方式实现(如使用栈)。

分治是一种算法设计思想

递归是一种编程技术,而分治是一种算法设计范式。分治关注如何将问题分解为子问题并合并结果,而递归关注如何通过自我调用来解决问题。

分治算法的递归实现框架

// 分治算法的递归实现框架
Result divideAndConquer(Problem problem) {
    // 基本情况:问题规模足够小,直接求解
    if (problem.size <= threshold) {
        return solveBaseCase(problem);
    }
    
    // 分解:将问题分解为子问题
    vector<Problem> subproblems = splitProblem(problem);
    
    // 解决:递归求解每个子问题
    vector<Result> subresults;
    for (auto& subproblem : subproblems) {
        subresults.push_back(divideAndConquer(subproblem));
    }
    
    // 合并:将子问题的解合并为原问题的解
    return combineResults(subresults);
}

分治算法经典例题解析

1

归并排序

问题描述:使用分治算法实现归并排序,对一个数组进行升序排序。归并排序是分治算法的经典应用,具有稳定的O(n log n)时间复杂度。

解题思路:

归并排序的核心思想是将数组不断二分,直到子数组长度为1,然后将两个有序子数组合并为一个有序数组。

  1. 分解:将数组分成两个规模大致相等的子数组
  2. 解决:递归地对两个子数组进行归并排序
  3. 合并:将两个已排序的子数组合并成一个有序数组
归并排序示意图

代码实现

// 归并排序的分治实现
#include <iostream>
#include <vector>
using namespace std;

// 合并两个有序数组
void merge(vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;  // 左子数组大小
    int n2 = right - mid;     // 右子数组大小
    
    // 创建临时数组
    vector<int> L(n1), R(n2);
    
    // 复制数据到临时数组
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
    
    // 合并临时数组到原数组
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    
    // 复制剩余元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

// 归并排序主函数
void mergeSort(vector<int>& arr, int left, int right) {
    // 基本情况:如果子数组长度为1或0,直接返回
    if (left >= right)
        return;
    
    // 分解:找到中间点
    int mid = left + (right - left) / 2;  // 避免溢出
    
    // 解决:递归排序左右子数组
    mergeSort(arr, left, mid);
    mergeSort(arr, mid + 1, right);
    
    // 合并:合并两个已排序的子数组
    merge(arr, left, mid, right);
}

// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    vector<int> arr = {12, 11, 13, 5, 6, 7};
    cout << "排序前的数组: ";
    printArray(arr);
    
    mergeSort(arr, 0, arr.size() - 1);
    
    cout << "排序后的数组: ";
    printArray(arr);
    
    return 0;
}

算法分析

  • 时间复杂度:O(n log n),在最好、最坏和平均情况下均为此值
  • 空间复杂度:O(n),需要额外的存储空间来合并数组
  • 稳定性:稳定排序,相等元素的相对顺序不会改变
  • 递归关系式:T(n) = 2T(n/2) + O(n),根据主方法,解为O(n log n)
2

快速排序

问题描述:使用分治算法实现快速排序,对一个数组进行升序排序。快速排序是实践中最常用的排序算法之一,平均时间复杂度为O(n log n)。

解题思路:

快速排序的核心思想是选择一个基准元素,将数组分为两部分,一部分所有元素小于等于基准,另一部分所有元素大于基准,然后递归地对这两部分进行排序。

  1. 分解:选择一个基准元素,将数组分为两个子数组
  2. 解决:递归地对两个子数组进行快速排序
  3. 合并:子数组已经有序,不需要显式合并
快速排序示意图

代码实现

// 快速排序的分治实现
#include <iostream>
#include <vector>
#include <algorithm> // 用于swap
using namespace std;

// 划分函数:选择基准并划分数组
int partition(vector<int>& arr, int low, int high) {
    // 选择最右边的元素作为基准
    int pivot = arr[high];
    
    // i是小于基准区域的边界
    int i = low - 1;
    
    // 遍历数组,将小于基准的元素放到左边
    for (int j = low; j <= high - 1; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }
    
    // 将基准放到正确的位置
    swap(arr[i + 1], arr[high]);
    return i + 1; // 返回基准的位置
}

// 快速排序主函数
void quickSort(vector<int>& arr, int low, int high) {
    // 基本情况:如果子数组长度为1或0,直接返回
    if (low >= high)
        return;
    
    // 分解:划分数组,获取基准位置
    int pi = partition(arr, low, high);
    
    // 解决:递归排序基准左右的子数组
    quickSort(arr, low, pi - 1);  // 排序左半部分
    quickSort(arr, pi + 1, high); // 排序右半部分
    // 不需要显式合并,因为子数组已经有序且基准在正确位置
}

// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    vector<int> arr = {10, 7, 8, 9, 1, 5};
    cout << "排序前的数组: ";
    printArray(arr);
    
    quickSort(arr, 0, arr.size() - 1);
    
    cout << "排序后的数组: ";
    printArray(arr);
    
    return 0;
}

算法分析

  • 平均时间复杂度:O(n log n)
  • 最坏时间复杂度:O(n²)(当数组已经有序或逆序时)
  • 空间复杂度:O(log n)(递归调用栈)
  • 稳定性:不稳定排序
  • 优化技巧:随机选择基准元素或三数取中法选择基准,避免最坏情况
3

二分查找

问题描述:使用分治算法实现二分查找,在一个有序数组中查找目标元素的位置。如果找到,返回其索引;否则,返回-1。

解题思路:

二分查找的核心思想是将有序数组不断二分,通过比较目标元素与中间元素的大小,缩小查找范围,直到找到目标元素或确定目标元素不存在。

  1. 分解:计算中间位置,将数组分为两部分
  2. 解决:
    • 如果中间元素等于目标,查找成功
    • 如果中间元素大于目标,在左半部分查找
    • 如果中间元素小于目标,在右半部分查找
  3. 合并:不需要合并步骤,找到目标后直接返回

代码实现

// 二分查找的分治实现
#include <iostream>
#include <vector>
using namespace std;

// 递归实现二分查找
int binarySearchRecursive(const vector<int>& arr, int left, int right, int target) {
    // 基本情况:查找范围为空,目标不存在
    if (left > right)
        return -1;
    
    // 分解:计算中间位置
    int mid = left + (right - left) / 2;  // 避免溢出
    
    // 解决:比较中间元素与目标
    if (arr[mid] == target)
        return mid;  // 找到目标,返回索引
    
    // 如果中间元素大于目标,在左半部分查找
    if (arr[mid] > target)
        return binarySearchRecursive(arr, left, mid - 1, target);
    
    // 否则,在右半部分查找
    return binarySearchRecursive(arr, mid + 1, right, target);
}

// 非递归实现二分查找
int binarySearchIterative(const vector<int>& arr, int target) {
    int left = 0;
    int right = arr.size() - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target)
            return mid;
        if (arr[mid] > target)
            right = mid - 1;
        else
            left = mid + 1;
    }
    
    return -1;  // 目标不存在
}

int main() {
    vector<int> arr = {2, 3, 4, 10, 40};
    int target = 10;
    
    // 递归方式查找
    int resultRecursive = binarySearchRecursive(arr, 0, arr.size() - 1, target);
    if (resultRecursive != -1)
        cout << "递归查找:元素 " << target << " 存在于索引 " << resultRecursive << endl;
    else
        cout << "递归查找:元素 " << target << " 不存在于数组中" << endl;
    
    // 非递归方式查找
    int resultIterative = binarySearchIterative(arr, target);
    if (resultIterative != -1)
        cout << "非递归查找:元素 " << target << " 存在于索引 " << resultIterative << endl;
    else
        cout << "非递归查找:元素 " << target << " 不存在于数组中" << endl;
    
    return 0;
}

算法分析

  • 时间复杂度:O(log n),每次查找都会将问题规模减半
  • 空间复杂度:递归实现为O(log n),非递归实现为O(1)
  • 适用条件:必须是有序数组,且支持随机访问
  • 递归关系式:T(n) = T(n/2) + O(1),根据主方法,解为O(log n)
4

最大子数组和(Kadane算法的分治实现)

问题描述:给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。这是LeetCode第53题的分治解法。

解题思路:

使用分治算法解决最大子数组问题,核心思想是最大子数组要么在左半部分,要么在右半部分,要么跨越左右两部分。

  1. 分解:将数组分为左右两个子数组
  2. 解决:
    • 递归求解左子数组的最大子数组和
    • 递归求解右子数组的最大子数组和
    • 求解跨越左右子数组的最大子数组和
  3. 合并:返回三个结果中的最大值

代码实现

// 最大子数组和的分治实现
#include <iostream>
#include <vector>
#include <climits> // 用于INT_MIN
#include <algorithm> // 用于max
using namespace std;

// 查找跨越中点的最大子数组和
int maxCrossingSum(const vector<int>& arr, int left, int mid, int right) {
    // 从中间向左查找最大和
    int sum = 0;
    int leftSum = INT_MIN;
    for (int i = mid; i >= left; i--) {
        sum += arr[i];
        if (sum > leftSum)
            leftSum = sum;
    }
    
    // 从中间向右查找最大和
    sum = 0;
    int rightSum = INT_MIN;
    for (int i = mid + 1; i <= right; i++) {
        sum += arr[i];
        if (sum > rightSum)
            rightSum = sum;
    }
    
    // 返回跨越中点的最大和
    return leftSum + rightSum;
}

// 分治算法求解最大子数组和
int maxSubarraySum(const vector<int>& arr, int left, int right) {
    // 基本情况:子数组只有一个元素
    if (left == right)
        return arr[left];
    
    // 分解:找到中间点
    int mid = left + (right - left) / 2;
    
    // 解决:递归求解左右子数组和跨越子数组
    int leftSum = maxSubarraySum(arr, left, mid);
    int rightSum = maxSubarraySum(arr, mid + 1, right);
    int crossSum = maxCrossingSum(arr, left, mid, right);
    
    // 合并:返回三个结果中的最大值
    return max({leftSum, rightSum, crossSum});
}

int main() {
    vector<int> arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    cout << "数组: ";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    
    int maxSum = maxSubarraySum(arr, 0, arr.size() - 1);
    cout << "最大子数组和: " << maxSum << endl; // 应该输出6,对应子数组[4,-1,2,1]
    
    return 0;
}

算法分析

  • 时间复杂度:O(n log n),每次递归将问题规模减半,合并步骤需要O(n)时间
  • 空间复杂度:O(log n),递归调用栈的深度
  • 对比Kadane算法:Kadane算法时间复杂度为O(n),但分治算法展示了分治思想的应用
  • 递归关系式:T(n) = 2T(n/2) + O(n),根据主方法,解为O(n log n)
5

最近点对问题

问题描述:给定平面上n个点,找到距离最近的一对点。这是计算几何中的经典问题,分治算法可以高效地解决这个问题。

解题思路:

最近点对问题的分治解法将平面上的点分为左右两部分,分别求解左右两部分的最近点对,然后检查是否存在跨越两部分的更近点对。

  1. 分解:将点集按x坐标排序,分为左右两个子集
  2. 解决:
    • 递归求解左子集的最近点对及距离d1
    • 递归求解右子集的最近点对及距离d2
    • 令d = min(d1, d2),检查在距离分割线d范围内的点是否存在更近的点对
  3. 合并:返回d和跨越点对距离中的最小值
最近点对问题示意图

代码实现

// 最近点对问题的分治实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <limits>
using namespace std;

// 定义点结构
struct Point {
    double x, y;
    Point(double x = 0, double y = 0) : x(x), y(y) {}
};

// 计算两点之间的距离
double distance(const Point& a, const Point& b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return sqrt(dx*dx + dy*dy);
}

// 按x坐标排序
bool compareX(const Point& a, const Point& b) {
    return a.x < b.x;
}

// 按y坐标排序
bool compareY(const Point& a, const Point& b) {
    return a.y < b.y;
}

// 暴力求解最近点对距离(用于小规模点集)
double bruteForce(const vector<Point>& points, int left, int right) {
    double minDist = numeric_limits<double>::max();
    for (int i = left; i <= right; i++) {
        for (int j = i + 1; j <= right; j++) {
            minDist = min(minDist, distance(points[i], points[j]));
        }
    }
    return minDist;
}

// 寻找跨越左右两部分的最近点对
double closestCrossing(const vector<Point>& points, int left, int mid, int right, double d) {
    vector<Point> strip;
    double midX = points[mid].x;
    
    // 收集距离中线小于d的点
    for (int i = left; i <= right; i++) {
        if (abs(points[i].x - midX) < d) {
            strip.push_back(points[i]);
        }
    }
    
    // 按y坐标排序
    sort(strip.begin(), strip.end(), compareY);
    
    // 检查每个点与后续最多6个点的距离
    double minDist = d;
    int n = strip.size();
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n && (strip[j].y - strip[i].y) < minDist; j++) {
            minDist = min(minDist, distance(strip[i], strip[j]));
        }
    }
    
    return minDist;
}

// 分治算法求解最近点对
double closestUtil(const vector<Point>& points, int left, int right) {
    // 基本情况:点的数量较少时,使用暴力法
    if (right - left + 1 <= 3) {
        return bruteForce(points, left, right);
    }
    
    // 分解:找到中间点
    int mid = left + (right - left) / 2;
    Point midPoint = points[mid];
    
    // 解决:递归求解左右两部分
    double leftDist = closestUtil(points, left, mid);
    double rightDist = closestUtil(points, mid + 1, right);
    
    // 找到左右两部分中的最小距离
    double d = min(leftDist, rightDist);
    
    // 检查跨越左右两部分的点对
    double crossDist = closestCrossing(points, left, mid, right, d);
    
    // 合并:返回三者中的最小值
    return min(d, crossDist);
}

// 计算最近点对距离
double closestPair(vector<Point> points) {
    // 先按x坐标排序
    sort(points.begin(), points.end(), compareX);
    return closestUtil(points, 0, points.size() - 1);
}

int main() {
    vector<Point> points = {
        Point(2, 3), Point(12, 30), Point(40, 50), 
        Point(5, 1), Point(12, 10), Point(3, 4)
    };
    
    cout << "点的数量: " << points.size() << endl;
    cout << "最近点对的距离: " << closestPair(points) << endl;
    
    return 0;
}

算法分析

  • 时间复杂度:O(n log n),排序耗时O(n log n),递归部分T(n) = 2T(n/2) + O(n)
  • 空间复杂度:O(n),用于存储排序后的点和中间条带
  • 关键优化:在检查跨越点对时,每个点最多需要与后续6个点比较,使合并步骤保持O(n)时间
  • 实际应用:地理信息系统、计算机图形学、模式识别等领域

分治算法解题策略与技巧

分治算法的应用范围广泛,但如何正确地设计和实现分治算法需要掌握一定的策略和技巧。以下是一些实用的经验总结:

如何判断问题是否适合分治算法

1

问题是否可分解

原问题能否分解为结构相似的子问题,且子问题规模更小

2

子问题是否独立

子问题之间是否存在重叠,如果存在大量重叠,可能更适合动态规划

3

是否存在有效的合并方法

能否将子问题的解有效合并为原问题的解,合并步骤的复杂度是否可接受

4

小规模问题是否容易解决

当问题规模足够小时,是否存在简单直接的解法

设计分治算法的技巧

1

合理分解问题

通常将问题分解为规模大致相等的子问题,以获得最佳的时间复杂度

2

选择合适的基本情况

基本情况的选择会影响算法效率,通常选择易于求解且规模较小的情况

3

优化合并步骤

合并步骤往往是分治算法的性能瓶颈,需要精心设计以降低复杂度

4

考虑预处理

如排序等预处理步骤可以显著简化分治过程,提高算法效率

5

注意边界条件

分治过程中容易出现边界错误,需要特别注意子问题的划分和索引处理

分治算法的常见错误与避免方法

错误类型:分解不合理

子问题规模差异过大,导致时间复杂度退化

避免方法:

  • 尽量将问题均匀分解为规模相近的子问题
  • 对于数组问题,通常选择中点作为分解点
  • 对于树问题,通常按子树自然分解

错误类型:合并步骤低效

合并子问题解的步骤复杂度太高,抵消了分治的优势

避免方法:

  • 精心设计合并算法,尽量将复杂度控制在O(n)
  • 利用问题特性减少合并时需要考虑的元素数量
  • 考虑预处理步骤,为合并创造有利条件

错误类型:递归边界处理不当

基本情况定义错误,导致无限递归或结果错误

避免方法:

  • 明确界定基本情况的条件
  • 对小规模问题进行充分测试
  • 注意处理空集、单元素等特殊情况

分治与其他算法的结合

分治算法常与其他算法技术结合使用,以解决更复杂的问题:

分治 + 排序

排序往往是分治算法的预处理步骤(如最近点对问题),或本身就是分治算法的应用(如归并排序、快速排序)

分治 + 贪心

在某些问题中,分治用于分解问题,而贪心用于解决子问题或合并子问题的解

分治 + 动态规划

对于某些复杂问题,可以先用分治分解问题,在子问题层面使用动态规划求解

分治 + 剪枝

在求解过程中,通过剪枝技术减少不必要的子问题计算,提高算法效率

分治算法常见模型

1. 排序与查找模型

分治算法在排序和查找领域有广泛应用,常见问题包括:

归并排序

将数组二分,递归排序后合并,稳定的O(n log n)排序算法

快速排序

选择基准元素划分数组,递归排序子数组,平均O(n log n)排序算法

二分查找

在有序数组中不断二分查找范围,O(log n)时间复杂度

选择问题

查找数组中第k小元素,平均O(n)时间复杂度

核心技巧

排序和查找问题的分治解法通常需要对数据进行合理划分,选择合适的划分点(如中点、基准元素)对算法效率至关重要。预处理(如排序)可以显著提升后续分治过程的效率。

2. 数组与序列问题模型

分治算法可以有效解决多种数组和序列相关问题:

最大子数组和

寻找具有最大和的连续子数组,分治解法时间复杂度O(n log n)

逆序对计数

统计数组中逆序对的数量,基于归并排序的分治解法时间复杂度O(n log n)

矩阵乘法(Strassen算法)

将矩阵分块相乘,时间复杂度优于传统的O(n³)算法

大数乘法(Karatsuba算法)

将大数分解为较小数相乘,减少乘法次数,时间复杂度O(n^log₂3)

核心技巧

数组和序列问题的分治解法关键在于如何将问题分解为子问题,并设计高效的合并步骤。对于涉及"跨越"子问题边界的情况(如最大子数组和),需要特别处理。

3. 几何问题模型

分治算法在计算几何中有重要应用:

最近点对问题

寻找平面上距离最近的一对点,分治解法时间复杂度O(n log n)

凸包问题

寻找包含所有点的最小凸多边形,分治解法时间复杂度O(n log n)

线段相交问题

判断平面上是否存在相交的线段,分治解法可高效解决

平面点集的中位数

寻找平面点集在x或y坐标上的中位数,可用于划分点集

核心技巧

几何问题的分治解法通常需要先对点进行排序(如按x或y坐标),然后分解为子问题。合并步骤往往需要利用几何性质减少计算量(如最近点对问题中只需检查每个点周围的6个点)。

4. 树与图问题模型

分治算法可以解决多种树和图相关问题:

树的遍历与分解

通过寻找树的重心分解树,解决路径查询等问题

矩阵链乘法

寻找矩阵相乘的最优顺序,分治结合动态规划的解法

快速傅里叶变换(FFT)

高效计算多项式乘法,基于分治思想的算法

图的连通分量

分治策略可用于并行计算图的连通分量

核心技巧

树和图问题的分治解法通常需要找到合适的分解点(如树的重心),使分解后的子问题规模显著减小。对于图问题,分治策略常与其他图算法(如并查集)结合使用。

分治算法练习题

1

逆序对计数

问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

提示:

  • 基于归并排序的分治思想
  • 在合并两个有序子数组时计数逆序对
  • 时间复杂度O(n log n)
2

寻找峰值元素

问题描述:峰值元素是指其值严格大于左右相邻值的元素。给你一个整数数组nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。你可以假设nums[-1] = nums[n] = -∞。

提示:

  • 使用二分查找的分治思想
  • 比较中间元素与其右侧元素,决定搜索方向
  • 时间复杂度O(log n)
3

不同的二叉搜索树

问题描述:给你一个整数n,求恰由n个节点组成且节点值从1到n互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。

提示:

  • 以每个数字为根节点,递归计算左右子树的组合数
  • 左子树的节点数为i-1,右子树的节点数为n-i
  • 可以使用动态规划优化分治的重复计算
4

Pow(x, n)

问题描述:实现pow(x, n),即计算x的n次幂函数。

提示:

  • 使用分治思想,将指数分解为n/2和n-n/2
  • 处理特殊情况:n为0、负数
  • 时间复杂度O(log n)
5

数组中的第K个最大元素

问题描述:在未排序的数组中找到第k个最大的元素。请注意,你需要找的是数组排序后的第k个最大的元素,而不是第k个不同的元素。

提示:

  • 基于快速排序的分治思想(快速选择算法)
  • 选择基准元素,将数组划分为两部分
  • 根据基准元素的位置决定继续在左半部分还是右半部分查找
  • 平均时间复杂度O(n),最坏情况O(n²)