分治算法

我对分治的理解

分治策略,顾名思义,即是将问题分成若干小问题,将所有的小问题逐一击破,那么大问题就迎刃而解了。那么为什么需要将一个大问题分解成多个小问题呢?因为随着问题规模的减小,问题将变得越来越简单,当问题简单到一定程度的时候,这个问题就非常容易解决了。
而分治策略最常的做法就是递归,因为递归的定义和分治的思想相似。

分治策略经典题目

快速排序

算法思想:从一个无序的数组中按照自己定义的规则挑出一个轴元素,然后将数组中小于这个轴元素的元素排在轴元素的左边,将大于轴元素的元素排在轴元素右边。此时,轴元素的位置和排好序后的位置是处于同一位置了。然后,递归地分别对轴元素左边和右边的数组进行同样的过程。递归终止条件为选取的数组只有一个元素。

源代码:

//
//  main.cpp
//  Sf_exp1_02
//
//  Created by M-Mac on 2019/12/3.
//  Copyright © 2019年 Chenyanting. All rights reserved.
//

#include <iostream>
using namespace std;

void quickSort(int left,int right,int arr[]);

int main(int argc, const char * argv[]) {
    cout << "Please enter ten intergers!" << endl;
    int arr[10] = {0};
    for(int i = 0 ; i < 10; i++){
        cin >> arr[i];
    }
    quickSort(0, 9, arr);
    for(int i = 0 ; i < 10 ; i++){
        cout << arr[i] << " " ;
    }
    return 0;
}

void quickSort(int left,int right,int arr[]){
    if(left >= right)
        return;
    int temp = arr[left];
    int i = left;
    int j = right;
    while (i != j) {
        //在右边找到一个比temp小的数
        while (arr[j] >= temp && i < j) {
            j--;
        }
        //在左边找到一个比temp大的数
        while(arr[i] <= temp && i < j)
            i++;
        if(i < j){
            arr[left] = arr[i];
            arr[i] = arr[j];
            arr[j] = arr[left];
        }
    }
    arr[left] = arr[i];
    arr[i] = temp;
    quickSort(left, i-1, arr);
    quickSort(i+1, right, arr);
}

归并排序

算法思想:将一个无序的数组通过递归,不断地对半拆分。递归终止条件为,拆分后的数组只剩下一个元素,这个递归过程形成了一个递归树。然后沿着这个递归树,由下至上,将两个小数组合并成一个有序的大数组,不停地往上数组二合一、二合一,直至最后合成一个大小和原数组一样大的数组。

源代码:

//
//  main.cpp
//  Sf_exp1_01
//
//  Created by M-Mac on 2019/12/3.
//  Copyright © 2019年 Chenyanting. All rights reserved.
//

#include <iostream>
using namespace std;

void merge(int left,int right,int arr[]);
void mergeSort(int left,int right,int arr[]);

int main(int argc, const char * argv[]) {
    cout << "Please you enter ten intergers:" << endl;
    int arr[10] = {0};
    for(int i = 0 ; i < 10; i++){
        cin >> arr[i];
    }
    mergeSort(0, 9, arr);
    for(int i = 0 ; i < 10;i++){
        cout << arr[i] << " ";
    }
    return 0;
}

void mergeSort(int left,int right,int arr[]){
    int mid = (left + right) / 2;
    if(left < right){
        mergeSort(left, mid, arr);
        mergeSort(mid+1, right, arr);
        merge(left, right, arr);
    }
}

void merge(int left,int right,int arr[]){
    int mid = (left+right) / 2;
    int size = right - left + 1;
    int temp[size];
    int i = left;
    int j = mid + 1;
    int k = 0;
    while(i <= mid && j <= right){
        if(arr[i] < arr[j]){
            temp[k] = arr[i];
            k++;
            i++;
        }
        else{
            temp[k] = arr[j];
            k++;
            j++;
        }
    }
    for(int m = i;m <= mid;m++){
        temp[k] = arr[m];
        k++;
    }
    for(int m = j;m <= right;m++){
        temp[k] = arr[m];
        k++;
    }
    k = 0;
    for(int m = left;m <= right;m++){
        arr[m] = temp[k];
        k++;
    }
}
    

赛程表问题

题目描述:编写程序实现循环赛日程表。设有n=2k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:(1)每个选手必须与其它n-1个选手各赛一次(2)每个选手一天只能赛一场(3)循环赛进行n-1天。

算法思想:先将赛程表的第一行填充,分别为1,2,3、、、、,n。因为题目要求,每行每列都不能出现同样的数组,那么可以某种方法可以简单地实现每行每列的不重复。方法如下:将第一、二行的元素每隔两个分为一组,共分为n/2组,第一组含有两个元素,分别2与1。2和1的由来是,将第一行第一组与第二行第一组看成一个正方形,然后将这个正方形左上角的元素复制到右下角、右上角的元素复制到左下角。对于第二行的第二组、第三组,直至第n组,都是利用这个方法。这个方法有效的避免了第一行与第二行有相同的数字出现在同一列。利用这个思想,将第一行与第二行看成一个整体,利用第一行推第二行的方法,从而推出第三行与第四行。以此类推,推出2的k次方行,即填充满了这个赛程表。现在利用这个思想再看这个题目,可以发现,求2的k次方行赛程表,即是分别求左上角和右上角的2的k-1次方行赛程表,如此递归下去,便可将一个复杂的问题化成若干简单的子问题了。

源代码:

//
//  main.cpp
//  Sf_exp1_03
//
//  Created by M-Mac on 2019/12/3.
//  Copyright © 2019年 Chenyanting. All rights reserved.
//

#include <iostream>
#include <cmath>
using namespace std;
#define NUM 100
static int k = 0;

void makeTable(int table[NUM][NUM],int startX,int startY,int fillSize){
    //设置递归结束条件
    if(fillSize == 1)
        return;
    //递归将大规模问题拆分
    makeTable(table, startX, startY, fillSize/2);
    makeTable(table, startX+(fillSize)/2, startY, fillSize/2);
    //拆分后的对角复制处理
    for(int i = 0 ; i < fillSize/2;i++){
        for(int j = 0 ; j < fillSize/2; j++){
            table[startY+fillSize/2+i][startX+fillSize/2+j] = table[startY+i][startX+j];
        }
    }
    for(int i = 0 ; i < fillSize/2;i++){
        for(int j = 0 ; j < fillSize/2; j++){
            table[startY+fillSize/2+i][startX+j] = table[startY+i][startX+fillSize/2+j];
        }
    }
}

int main(int argc, const char * argv[]) {
    //初始化赛程表矩阵
    cout << "Please enter a number as k" << endl;
    cin >> k;
    int size = pow(2,k);
    int table[NUM][NUM];
    //将赛程表的第一行进行初始化
    for(int i = 0 ; i < size;i++){
        table[0][i] = i;
    }
    //通过分治算法,递归填充矩阵
    makeTable(table, 0, 0, size);
    for(int i = 0 ; i < size;i++){
        for(int j = 0 ; j < size;j++){
            if((j+1) % size == 0)
                cout << table[i][j] << endl;
            else
                cout << table[i][j] << " ";
        }
    }
    return 0;
}



总结

分治策略就是利用递归解决问题,递归就是将复杂问题分解成若个子问题,解决了若个子问题就解决了整个问题。那么递归的过程中有很多需要注意的细节,如下:

  1. 注意递归的终止条件,递归的终止条件其实是递归最关键的部分,终止条件往往是这个递归算法的思路所在。你为什么想到递归?大多数情况是你从脑海里面将这个问题从最简单的问题不断的扩规模的演算了一遍(参考赛程表问题的算法思路)。那么递归其实就是你的思考过程的逆向过程,从复杂着手,不断化简,解决了简单的问题后,利用简单问题的解再不断的返回到上一级来解决更复杂的问题,从而求出答案。
  2. 递归有两种常见的写法:
    第一种如下面代码,递归函数的内部是先递归自身mergeSort(),在递归自身之后再编写处理方法merge()。这个递归类型属于先将问题不断拆分形成一个递归树,然后从递归树最底层开始执行处理方法,当底层处理完后返回到递归树的上一层,以此类推。
void mergeSort(int left,int right,int arr[]){
    int mid = (left + right) / 2;
    if(left < right){
        mergeSort(left, mid, arr);
        mergeSort(mid+1, right, arr);
        merge(left, right, arr);
    }
}

第二种如下面代码,先执行处理部分,然后递归自身。这种递归类型通过递归过程建立了一颗递归树,但是与之前那个不同的是,这个递归类型要求将递归树最高层的部分处理完后,再往递归树下层走,以此类推。

void quickSort(int left,int right,int arr[]){
    if(left >= right)
        return;
    int temp = arr[left];
    int i = left;
    int j = right;
    while (i != j) {
        //在右边找到一个比temp小的数
        while (arr[j] >= temp && i < j) {
            j--;
        }
        //在左边找到一个比temp大的数
        while(arr[i] <= temp && i < j)
            i++;
        if(i < j){
            arr[left] = arr[i];
            arr[i] = arr[j];
            arr[j] = arr[left];
        }
    }
    arr[left] = arr[i];
    arr[i] = temp;
    quickSort(left, i-1, arr);
    quickSort(i+1, right, arr);
}
  1. 递归函数的编写思路如下:首先确定一个数据结构来储存这个问题的解,并将其作为函数的参数。然后明确递归问题的规模变化的代表变量,例如赛程表问题,问题规模变化的代表变量就是StartX、StartY、fillSize,将其作为函数的参数。然后确定递归的类型,是第一种还是第二种还是其它呢?最后根据函数的参数中的变量来编写递归处理部分,实现对不同规模的问题的通用解法!!
原文地址:https://www.cnblogs.com/782687539-nanfu/p/12707610.html