经典算法思想:分治策略

283 阅读7分钟

一.分治策略基本思想: 在算法设计中,分治策略是一种将问题分解成更小的子问题,然后递归地解决这些子问题,并将它们的解合并起来得到原始问题的解的方法。 这种策略通常包括三个步骤:

  1. 分解(Divide):将原始问题分解成若干个规模较小的子问题。
  2. 解决(Conquer):递归地解决这些子问题。如果子问题足够小,则直接求解。
  3. 合并(Combine):将子问题的解合并起来,得到原始问题的解。

bb85d4f3a9452f1caeb8d6d5e4af968.jpg

二.分治法使用场景:

  1. 该问题的规模缩小到一定的程度就可以容易的解决。
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解。
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

第一条特征是绝大多数问题可以满足的,问题的复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提。它是大多数问题可以满足的,此特征反映了递归思想的应用。第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条,而不具备第三条特征,则可以考虑使用贪心法或者动态规划法。第四条关系到分治法的效率,如果各个子问题是不独立的则分治法要做寻多不必要的工作,重复的解决公共的子问题,此时虽然可用分治法,但一般使用动态规划法较好。

三.改进分治法的途径

分治算法比起通常的蛮力算法在效率上确实有了明显的改进.但分治策略也不是处处有效的,对有些问题,简单的分治算法对提高求解效率没用,原因主要是在于分治算法的递归调用。首先是产生的子问题个数较多,减少子同题个数是降低时间复杂度的有效途径,怎样减少子问题个数?一种可行的办法是寻找子问题之间的依赖关系,如果个子问题的解可以用其他子问题的解通过简单的运算得到,那么在用到这个子问题的解时,不必重新递归计算,而是通过组合其他子问题的解来得到,这样就可以有效减少子问题的个数,从而提高算法效率。其次,递归过程内的工作量过多也是影响算法效率的一个重要因素如果在算法设计时,尽量把某些工作提到递归过程之外,作为预处理,从而有效减少道归内部的调用工作量,也是提高算法效率的一个有效途径。所以概括为以下两点:①通过代数变换减少子问题个数 ②利用预处理减少递归内部的计算量。

四.典型事例

1.插入排序

插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

掘金.gif 用C语言进行解释:

#include <stdio.h>

void insertionSort(int arr[], int n) {
    int i, key, j;
    for (i = 1; i < n; i++) {
        key = arr[i];
        j = i - 1;

        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;

        // 输出中间结果
        for (int k = 0; k < n; k++) {
            if (k == n - 1) {
                printf("%d", arr[k]);
            } else {
                printf("%d ", arr[k]);
            }
        }
        printf("\n");
    }
}

int main() {
    int n;
    scanf("%d", &n);
    int arr[n];
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }

    insertionSort(arr, n);

    return 0;
}
  1. 在 main 函数中:

    • 从标准输入中读取数组长度 n
    • 动态分配数组 arr 来存储待排序的元素;
    • 依次读取输入的元素,并存储在数组 arr 中;
    • 调用 insertionSort 函数对数组 arr 进行排序。
  2. insertionSort 函数实现了插入排序算法,其中:

    • 外层循环从第二个元素开始(i = 1),依次将元素插入到已排序的部分中;

    • 内层循环将当前元素与已排序部分的元素进行比较,找到合适的位置插入当前元素;

    • 每次插入操作完成后,输出当前数组的中间结果。

2.汉诺塔

汉诺塔是一个源于印度古老传说的益智玩具。据说大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘,大梵天命令僧侣把圆盘移到另一根柱子上,并且规定:在小圆盘上不能放大圆盘,每次只能移动一个圆盘。当所有圆盘都移到另一根柱子上时,世界就会毁灭。请编写程序,输入汉诺塔圆片的数量,输出移动汉诺塔的步骤。

image.png

用C语言进行解决的代码演示

#include <stdio.h>

void hanoi(int n, char start, char end, char temp) {
    if (n == 1) {
        printf("1: %c -> %c\n", start, end);
        return;
    }
    hanoi(n - 1, start, temp, end);
    printf("%d: %c -> %c\n", n, start, end);
    hanoi(n - 1, temp, end, start);
}

int main() {
    int discs;
    char start, end, temp;

    scanf("%d", &discs);
    scanf(" %c %c %c", &start, &end, &temp);

    hanoi(discs, start, end, temp);

    return 0;
}

在这里void hanoi(int n, char start, char end, char temp): 这是一个递归函数,用于将�n个圆盘从起始柱子start经过辅助柱子temp移动到目标柱子end。如果只有一个圆盘需要移动,则直接输出移动步骤;否则,递归地将前�−1n−1个圆盘从起始柱子移动到辅助柱子,然后移动第�n个圆盘到目标柱子,最后将前�−1n−1个圆盘从辅助柱子移动到目标柱子。 int main(): 主函数用于接收输入参数(圆盘数量和柱子标识),调用hanoi函数解决汉诺塔问题。 输入格式为首先输入圆盘数量,然后输入起始柱子、目标柱子和辅助柱子的标识。 程序输出每一步的移动过程,格式为圆盘编号: 起始柱子 -> 目标柱子。

3.归并排序:

归并排序是将两个或两个以上的有序表组合成一个新的有序表。其基本思想是:先将N个数据看成N个长度为1的表,将相邻两个表合并,得到长度为2的N/2个有序表,进一步将相邻的表合并,得到长度为4的N/4个有序表,以此类推,知道所有数据合并成一个长度为N的有序表位置。没一次归并称为一趟。

归并排序.gif

用Java解决问题:

public static void mergeTwo(int[] data,int first,int mid,int last,int[] tmp){

        int i = first, j = mid + 1;
        int m = mid,   n = last;
        int k = 0;
        while(i<=m&&j<=n){
            
            if(data[i]<data[j]){
                tmp[k++] =data[i++];
            }else{
                
                tmp[k++] = data[j++];
            }
        }
        while(i<=m){
            tmp[k++] =data[i++];
        }
        while(j<=n){
            tmp[k++] = data[j++];
        }
        
        for (i = 0; i < k; i++)  {
            data[first + i] = tmp[i];
        }
    }
  1. 函数 mergeTwo 接受五个参数:

    • data:存储待排序元素的数组;
    • first:子数组1的起始索引;
    • mid:子数组1的结束索引,也是子数组2的起始索引-1;
    • last:子数组2的结束索引;
    • tmp:用于临时存储合并结果的数组。
  2. 在 while 循环中,不断比较子数组1和子数组2的元素,将较小的元素放入临时数组 tmp 中,直到其中一个子数组遍历完成。

  3. 若子数组1或子数组2还有剩余元素未处理,则将剩余元素依次放入临时数组 tmp 中,最后,将临时数组 tmp 中的元素复制回原数组 data 的对应位置,完成合并操作。

以上是关于分治策略的简单介绍还有事例,希望友友们喜欢。