算法复习_分治算法之二分搜索、棋盘覆盖、快速排序

一、基本概念

  分治法,顾名思义,即分而治之的算法,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……

二、基本思想及策略

  设计思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

  策略:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

三、分治法适用的情况

  分治法所能解决的问题一般具有以下几个特征:

  1) 该问题的规模缩小到一定的程度就可以容易地解决

  2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

  3) 利用该问题分解出的子问题的解可以合并为该问题的解;

  4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

  第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

  第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

  第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。

  第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

四、分治法的基本步骤

  分治法在每一层递归上都有三个步骤:

  • 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
  • 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  • 合并:将各个子问题的解合并为原问题的解。

五、(一)分治法之二分搜索

  算法概述:二分搜索算法,也称折半查找算法,即在一个有序数组中查找某一个特定元素。整个搜索过程从中间开始,如果要查找的元素即中间元素,那么搜索过程结束;反之根据中间元素与要查找元素的关系在数组对应的那一半查找,例如查找元素大于中间元素,则在整个数组较大元素的那一半查找,反复进行这个过程,直到找到元素,或者数组为空,查找不到元素。

  算法思路

  给定一个数组A_0,A_1...A_{n-1}, A_0 \le A_1 \le \cdot \le A_{n - 1},待查找元素为searchnum

  

  1. leftright分别表示左右端点,即要查找的范围;
  2. middle表示中间点,middle = \lfloor (left + right) / 2 \rfloor
  3. left > right,搜索失败;
  4. A{middle} > searchnumright = middle - 1,返回3;
  5. A{middle} < searchnumleft = middle + 1,返回3;
  6. A{middle} = searchnum,搜索结束,返回middle

  动态演示【low表示左端点,high表示右端点】

  

代码实现

  不使用递归求解

int binarySearch(arrType * a, arrType searchnum, int left, int right)
{
    int middle;
    while (left <= right) {
        middle = (left + right) / 2;
        if (a[middle] > searchnum)
            right = middle - 1;
        else if (a[middle] < searchnum)
            left = middle + 1;
        else if (a[middle] == searchnum)
            return middle;
    }
    return -1;
}

  使用递归求解

int binarySearch(arrType * a, arrType searchnum, int left, int right)
{
    int middle;
    if (left <= right) {
        middle = (left + right) / 2;
        if (a[middle] > searchnum)
            return binarySearch(a, searchnum, left, middle - 1);
        else if (a[middle] < searchnum)
            return binarySearch(a, searchnum, middle + 1, right);
        else if (a[middle] == searchnum)
            return middle;
    }
    return -1;
}

 五、(二)分治法之棋盘覆盖

  问题描述:在一个2^k×2^k个方格组成的棋盘中,若有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一个特殊棋盘.显然特殊方格在棋盘上出现的位置有4^k种情形.因而对任何k≥0,有4^k种不同的特殊棋盘.

  下图中的特殊棋盘是当k=364个特殊棋盘中的一个:k = 3,棋盘大小8 x 8

  

  在棋盘覆盖问题中,要用下图中 4 中不同形态的** L 型骨牌覆盖一个给定的特殊棋牌上除特殊方格以外的所有方格,且任何 2 个 L 型骨牌不得重叠覆盖**。易知,在任何一个 2^k × 2^k 的棋盘中,用到的 L 型骨牌个数恰为 (4^k-1)/3 。

  

  用分治策略,可以设计解棋盘问题的一个简捷的算法

  当 k>0 时,将 2^k * 2^k 棋盘分割为 4 个 2^(k-1) * 2^(k-1) 子棋盘,如下图所示:四个子棋盘

  

  特殊方格必位于 4 个较小子棋盘之一中,其余 3 个子棋盘中无特殊方格。

  为了将这 3 个无特殊方格的子棋盘转化为特殊棋盘,我们可以用一个 L 型骨牌覆盖这 3 个较小的棋盘的汇合处,如下图所示,这 3 个子棋盘上被 L 型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题化为 4 个较小规模的棋盘覆盖问题

  

  递归的使用这种分割,直至棋盘简化为 1x1 棋盘。

代码实现

  (1)棋盘:可以用一个二维数组board[size][size]表示一个棋盘,其中,size=2^k。为了在递归处理的过程中使用同一个棋盘,将数组board设为全局变量;
  (2)子棋盘:整个棋盘用二维数组board[size][size]表示,其中的子棋盘由棋盘左上角的下标tr、tc和棋盘大小s表示;
  (3)特殊方格:用board[dr][dc]表示特殊方格,dr和dc是该特殊方格在二维数组board中的下标;
  (4) L型骨牌:一个2k×2k的棋盘中有一个特殊方格,所以,用到L型骨牌的个数为(4^k-1)/3,将所有L型骨牌从1开始连续编号,用一个全局变量t表示。

#include <stdio.h>

#define BOARD_SIZE 4
int board[BOARD_SIZE][BOARD_SIZE];

// c1, r1: 棋盘左上角的行号和列号
// c2, r2: 特殊方格的行号和列号
// size = 2 ^ k
void chessboard(int r1, int c1, int r2, int c2, int size)
{
    if(1 == size) return;
    int half_size;
    static int domino_num = 1;
    int d = domino_num++;
    half_size = size / 2;   

    if(r2 < r1 + half_size && c2 < c1 + half_size) //特殊方格在左上角子棋盘
    {
       chessboard(r1, c1, r2, c2, half_size);
    }
    else   // 不在此棋盘,将此棋盘右下角设为相应的骨牌号
    {
       board[r1 + half_size - 1][c1 + half_size - 1] = d;
       chessboard(r1, c1, r1 + half_size - 1, c1 + half_size - 1, half_size);
    }

    if(r2 < r1 + half_size && c2 >= c1 + half_size) //特殊方格在右上角子棋盘
    {
       chessboard(r1, c1 + half_size, r2, c2, half_size);
    }
    else  // 不在此棋盘,将此棋盘左下角设为相应的骨牌号
    {
       board[r1 + half_size - 1][c1 + half_size] = d;
       chessboard(r1, c1 + half_size, r1 + half_size - 1, c1 + half_size, half_size);
    }

    if(r2 >= r1 + half_size && c2 < c1 + half_size) //特殊方格在左下角子棋盘
    {
       chessboard(r1 + half_size, c1, r2, c2, half_size);
    }
    else  // 不在此棋盘,将此棋盘右上角设为相应的骨牌号
    {
       board[r1 + half_size][c1 + half_size - 1] = d;
       chessboard(r1 + half_size, c1, r1 + half_size, c1 + half_size - 1, half_size);
    }

    if(r2 >= r1 + half_size && c2 >= c1 + half_size) //特殊方格在左上角子棋盘
    {
       chessboard(r1 + half_size, c1 + half_size, r2, c2, half_size);
    }
    else   // 不在此棋盘,将此棋盘左上角设为相应的骨牌号
    {
       board[r1 + half_size][c1 + half_size] = d;
       chessboard(r1 + half_size, c1 + half_size, r1 + half_size, c1 + half_size, half_size);
    }
}

int main()
{
    int i, j;
    board[2][2] = 0;
    chessboard(0, 0, 2, 2, BOARD_SIZE);
    for(i = 0; i < BOARD_SIZE; i++)
    {
        for(j = 0; j < BOARD_SIZE; j++)
        {
           printf("%-4d", board[i][j]);
        }
        printf("\n");
    }
}

#include<iostream>
using namespace std;
int tile=1;                   //L型骨牌的编号(递增)
int board[100][100];  //棋盘
/*****************************************************
* 递归方式实现棋盘覆盖算法
* 输入参数:
* tr--当前棋盘左上角的行号
* tc--当前棋盘左上角的列号
* dr--当前特殊方格所在的行号
* dc--当前特殊方格所在的列号
* size:当前棋盘的:2^k
*****************************************************/
void chessBoard ( int tr, int tc, int dr, int dc, int size )
{
    if ( size==1 )    //棋盘方格大小为1,说明递归到最里层
        return;
    int t=tile++;     //每次递增1
    int s=size/2;    //棋盘中间的行、列号(相等的)
    //检查特殊方块是否在左上角子棋盘中
    if ( dr<tr+s && dc<tc+s )              //在
        chessBoard ( tr, tc, dr, dc, s );
    else         //不在,将该子棋盘右下角的方块视为特殊方块
    {
        board[tr+s-1][tc+s-1]=t;
        chessBoard ( tr, tc, tr+s-1, tc+s-1, s );
    }
    //检查特殊方块是否在右上角子棋盘中
    if ( dr<tr+s && dc>=tc+s )               //在
        chessBoard ( tr, tc+s, dr, dc, s );
    else          //不在,将该子棋盘左下角的方块视为特殊方块
    {
        board[tr+s-1][tc+s]=t;
        chessBoard ( tr, tc+s, tr+s-1, tc+s, s );
    }
    //检查特殊方块是否在左下角子棋盘中
    if ( dr>=tr+s && dc<tc+s )              //在
        chessBoard ( tr+s, tc, dr, dc, s );
    else            //不在,将该子棋盘右上角的方块视为特殊方块
    {
        board[tr+s][tc+s-1]=t;
        chessBoard ( tr+s, tc, tr+s, tc+s-1, s );
    }
    //检查特殊方块是否在右下角子棋盘中
    if ( dr>=tr+s && dc>=tc+s )                //在
        chessBoard ( tr+s, tc+s, dr, dc, s );
    else         //不在,将该子棋盘左上角的方块视为特殊方块
    {
        board[tr+s][tc+s]=t;
        chessBoard ( tr+s, tc+s, tr+s, tc+s, s );
    }
}  

void main()
{
    int size;
    cout<<"输入棋盘的size(大小必须是2的n次幂): ";
    cin>>size;
    int index_x,index_y;
    cout<<"输入特殊方格位置的坐标: ";
    cin>>index_x>>index_y;
    chessBoard ( 0,0,index_x,index_y,size );
    for ( int i=0; i<size; i++ )
    {
        for ( int j=0; j<size; j++ )
            cout<<board[i][j]<<"/t";
        cout<<endl;
    }
}  

C++解法一

#include<iostream>
#include<vector>
#include<stack>  

using namespace std;  

vector<vector<int> > board(4);//棋盘数组,也可以作为参数传递进chessBoard中去,作为全局变量可以减少参数传递
stack<int> stI;   //记录当前所使用的骨牌号码,使用栈顶元素填充棋盘数组
int sL = 0;     //L型骨牌序号  

//所有下标皆为0开始的C C++下标
void chessBoard(int uRow, int lCol, int specPosR, int specPosC, int rowSize)
{
    if(rowSize ==1) return;
    //static int sL = 0;棋牌和骨牌都可以用static代替,如果不喜欢用全局变量的话。
    sL++;
    stI.push(sL); //每递归深入一层,就把一个骨牌序号入栈
    int halfSize = rowSize/2;//拆分  

    //注意:下面四个if else,肯定是只有一个if成立,然后执行if句,而肯定有三个else语句要执行的,因为肯定有一个是特殊位置,而其他三个是空白位置,需要填充骨牌。  

    //1如果特殊位置在左上角区域,则继续递归,直到剩下一个格子,并且该格子已经填充,遇到函数头一句if(rowSize == 1) return;就跳出一层递归。
    //注意是一个区域或子棋盘,有一个或者多个格子,并不是就指一个格子。
    if(specPosR<uRow+halfSize && specPosC<lCol+halfSize)
        chessBoard(uRow, lCol, specPosR, specPosC, halfSize);
    //如果其他情况
    else
    {
        board[uRow+halfSize-1][lCol+halfSize-1] = stI.top();
        //因为特殊位置不在,所以可以选择任意一个空格填充,但是本算法只填充左上角(也许不止一个格,也许有很多个格子)区域的右下角。大家仔细查一下,就知道下标[uRow+halfSize-1][lCol+halfSize-1]是本区域中最右下角的一个格子的下标号。
        chessBoard(uRow, lCol, uRow+halfSize-1, lCol+halfSize-1, halfSize);
        //然后是递归填充这个区域的其他空白格子。因为上一句已经填充了[uRow+halfSize-1][lCol+halfSize-1]这个格子,所以,这个下标作为特殊位置参数传递进chessBoard中。
    }     

    //2右上角区域,解析类上
    if(specPosR<uRow+halfSize && specPosC>=lCol+halfSize)
        chessBoard(uRow, lCol+halfSize, specPosR, specPosC, halfSize);
    else
    {
        board[uRow+halfSize-1][lCol+halfSize] = stI.top();
        chessBoard(uRow, lCol+halfSize, uRow+halfSize-1, lCol+halfSize, halfSize);
    }         

    //3左下角区域,类上
    if(specPosR>=uRow+halfSize && specPosC<lCol+halfSize)
        chessBoard(uRow+halfSize, lCol, specPosR, specPosC, halfSize);
    else
    {
        board[uRow+halfSize][lCol+halfSize-1] = stI.top();
        chessBoard(uRow+halfSize, lCol, uRow+halfSize, lCol+halfSize-1, halfSize);
    }     

    //4右下角区域,类上
    if(specPosR>=uRow+halfSize && specPosC>=lCol+halfSize)
        chessBoard(uRow+halfSize, lCol+halfSize, specPosR, specPosC, halfSize);
    else
    {
        board[uRow+halfSize][lCol+halfSize] = stI.top();
        chessBoard(uRow+halfSize, lCol+halfSize, uRow+halfSize, lCol+halfSize, halfSize);
    }     

    stI.pop();//本次骨牌号填充了三个格,填充完就出栈
}  

void test()
{
    //初始化数组
    for(int i=0; i<4; i++)
    {
        board[i].resize(4);
    }  

    chessBoard(0, 0, 3, 3, 4);  

    //特殊位置填充0
    board[3][3] = 0;  

    //序列输出
    for(int j=0; j<4; j++)
    {
        for(int i=0; i<4; i++)
            cout<<board[j][i]<<"\t";
        cout<<endl;
    }
    cout<<endl;
}  

int main()
{
    test();
    return 0;
}  

C++解法二

 五、(三)分治法之快速排序

  快速排序(Quicksort),是一种排序算法,最坏情况复杂度:Ο(n2),最优时间复杂度:Ο(n log n),平均时间复杂度:Ο(n log n)。快速排序的基本思想也是用了分治法的思想:找出一个元素X,在一趟排序中,使X左边的数都比X小,X右边的数都比X要大。然后再分别对X左边的数组和X右边的数组进行排序,直到数组不能分割为止。

  算法思想

  1.设置一个长度为n的数组A,定义两个变量i = 0,j = n - 1;
  2.从数组中挑选出一个元素作为基准元素,复制给key;
  3.从j开始从后向前搜索,j--,找到比key小的值,将A[j]与A[i]互换;
  4.从i 开始向后搜索,i++,找到比key大的值,将A[i]与A[j]互换;
  5.递归的,重复2,3,4步,直到i == j ;

  举例:

  • 存在一个数组A:6 2 7 3 8 9 ,创建i = 0 ; j = 5,选择一个基准元素 k = 6
  • j 从右向左查找比k小的元素,发现,当 j = 3 时,发现元素3比k小,则另A[i] 与 A[j]交换,得到3 2 7 6 8 9;
  • i 从左向右进行查找,当 i = 2时,发现元素 7 比k大,则另A[i] 与A[j]进行交换,得到 3 2 6 7 8 9;
  • 接着,再减小j,重复上面的循环。
  • 但是我们发现,在本例中,一次循环后j与i就相等了,他们的下标同时指向了2.这时候,我们就进行分组,将3 2分为一组,7 8 9分为一组继续上述的比较,最终得到排序好的数组。


  动态演示

  

代码实现

/**
 快速排序算法
 */

void sort(int a[6], int left, int right)
{
    //如果左边索引大于或者等于右边的索引就代表已经整理完成一个组了
    if(left >= right)
    {
        return ;
    }
    int i = left;
    int j = right;
    int key = a[left];

    /*控制在当组内寻找一遍*/
    while(i < j)
    {
        while(i < j && key <= a[j])
        /*而寻找结束的条件就是,1,找到一个小于或者大于key的数(大于或小于取决于你想升
         序还是降序)2,没有符合条件1的,并且i与j的大小没有反转*/
        {
            //向前寻找
            j--;
        }

        a[i] = a[j];
        /*找到一个这样的数后就把它赋给前面的被拿走的i的值(如果第一次循环且key是
         a[left],那么就是给key)*/

        while(i < j && key >= a[i])
        /*这是i在当组内向前寻找,同上,不过注意与key的大小关系停止循环和上面相反,
         因为排序思想是把数往两边扔,所以左右两边的数大小与key的关系相反*/
        {
            i++;
        }

        a[j] = a[i];
    }

    /*当在当组内找完一遍以后就把中间数key回归*/
    a[i] = key;

    /*最后用同样的方式对分出来的左边的小组进行同上的做法*/
    sort(a, left, i - 1);

    /*用同样的方式对分出来的右边的小组进行同上的做法*/
    sort(a, i + 1, right);

    /*当然最后可能会出现很多分左右,直到每一组的i = j 为止*/
}

int main(int argc, const char * argv[])
{
    int a[6] = {1, 10, 8, 7, 4, 3};
    sort(a, 0, 5);

    for (int i = 0; i < 6; i ++) {
        printf("i = %d\n",a[i]);
    }
}

原文地址:https://www.cnblogs.com/1138720556Gary/p/11073808.html

时间: 2024-08-02 00:51:53

算法复习_分治算法之二分搜索、棋盘覆盖、快速排序的相关文章

【算法复习】分治算法、动态规划、贪心算法

Notes ## 分治思想和递归表达式 [分治思想] 将一个问题分解为与原问题相似但规模更小的若干子问题,递归地解这些子问题,然后将这些子问题的解结合起来构成原问题的解.这种方法在每层递归上均包括三个步骤: divide(分解):将问题划分为若干个子问题 conquer(求解):递归地解这些子问题:若子问题Size足够小,则直接解决之 Combine(组合):将子问题的解组合成原问题的解 [分治递归表达式] 设T(n)是Size为n的执行时间,若Size足够小,如n ≤ C (常数),则直接求解

(转)五大常用算法之一:分治算法

五大常用算法之一:分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关.问题的规模越小,越容易直接求解,解题所需的计算时间也越少.例如,对于n个元素的排

五大常用算法之一:分治算法(转)

五大常用算法之一:分治算法 分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关.问题的规模越小,越容易直接求解,解题所需的计算时间也越少.例如,对于n

常用算法一(分治算法)

一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关.问题的规模越小,越容易直接求解,解题所需的计算时间也越少.例如,对于n个元素的排序问题,当n=1时,不需任何

(转)五大常用算法之一:分治算法

http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741370.html 分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求解的

五大常用算法之一:分治算法

一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关.问题的规模越小,越容易直接求解,解题所需的计算时间也越少.例如,对于n个元素的排序问 题,当n=1时,不需任

算法浅谈——分治算法与归并、快速排序(附代码和动图演示)

在之前的文章当中,我们通过海盗分金币问题详细讲解了递归方法. 我们可以认为在递归的过程当中,我们通过函数自己调用自己,将大问题转化成了小问题,因此简化了编码以及建模.今天这篇文章呢,就正式和大家聊一聊将大问题简化成小问题的分治算法的经典使用场景--排序. 排序算法 排序算法有很多,很多博文都有总结,号称有十大经典的排序算法.我们信手拈来就可以说上来很多,比如插入排序.选择排序.桶排序.希尔排序.快速排序.归并排序等等.老实讲这么多排序算法,但我们实际工作中并不会用到那么多,凡是高级语言都有自带的

数据结构与算法复习(一) 排序算法(I)

这篇文章将会介绍最常见的排序算法(使用 JavaScript 语言实现) PS:这里我会尽量使用语言无关的语法去写,大家不要太在意语言,重要的是算法的实现思路 1.冒泡排序 将数组分为有序区(左边)和无序区(右边) 每次从无序区的最后一个元素开始,一直向前冒泡到无序区的第一个位置,使其变成有序 function swap(A, i, j) { if (i === j) return [A[i], A[j]] = [A[j], A[i]] } function bubbleSort(A) { fo

韩顺平_PHP程序员玩转算法公开课(第一季)01_算法重要性_五子棋算法_汉诺塔_回溯算法_学习笔记_源代码图解_PPT文档整理

文西马龙:http://blog.csdn.net/wenximalong/ 课程说明:算法是程序的灵魂,为什么有些网站能够在高并发,和海量吞吐情况下依然坚如磐石,大家可能会说: 网站使用了服务器集群技术.数据库读写分离和缓存技术(比如memcahced和redis等),那如果我再深入的问一句,这些优化技术又是怎样被那些天才的技术高手设计出来的呢? 我在上大学的时候就在想,究竟是什么让不同的人写出的代码从功能看是一样的,但从运行效率上却有天壤之别, 就拿以前在软件公司工作的实际经历来说吧, 我是