zoukankan      html  css  js  c++  java
  • 一次性弄懂到底什么叫做分治思想(含有大量经典例题,附带详细解析)

    期末了,通过写博客的方式复习一下算法,把自己知道的全部写出来

    分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了

    但是我们应该在什么时候用分治呢?这个问题也困扰了我很久,做题的时候就不知道用什么算法

    能用分治法的基本特征:

    1.问题缩小到一定规模容易解决

    2.分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质

    3.分解而成的小问题在解决之后要可以合并

    4.子问题是相互独立的,即子问题之间没有公共的子问题

    第一条大多数问题都可以满足

    第二条的大多数问题也可以满足,反应的是递归的思想

    第三条:这个是能分治的关键,解决子问题之后如果不能合并从而解决大问题的话,那凉凉,如果满足一,二,不满足三,即具有最优子结构的话,可以考虑贪心或者dp

    第四条:如果不满足第四条的话,也可以用分治,但是在分治的过程中,有大量的重复子问题被多次的计算,拖慢了算法效率,这样的问题可以考虑dp(大量重复子问题)

    了解了什么问题可以采用分治,那么分治到达怎么用?步骤是什么呢

    三个步骤:

    1.分解成很多子问题

    2.解决这些子问题

    3.将解决的子问题合并从而解决整个大问题

    化成一颗问题树的话,最底下的就是很多小问题,最上面的就是要解决的大问题,自底向上的方式求解问题

    说的再多不如看经典的样例,更好的体会分治的思想

    样例1:二分查找

    条件:数组有序,假设是升序数组

    虽然二分很容易,但是我还是要具体从算法思想分治的方向分析一下

    现在我们要在一个有序的升序数组里面查找一个数x有没有

    暴力的做法就是拿跟数组里面每个数比较一下,有的话就返回下标,这个是大问题

    仔细想一下,就知道这个大问题是由很多小问题组成的,小问题:在数组的一部分里面找x

    那么我们可以把数组分成很多部分,在很多部分里面找x,如果在这些部分里面没有找到x,那么把这些子问题合并起来,就是大数组里面没有x,否则就是有x

    这个真的很好的反应了分治的思想,先分解成很多小问题,解决这些小问题,把解决的小问题合并起来,大问题就解决了,二分具体的做法我就不多说了,都知道,贴个代码

    #include<string.h>
    #include<stdio.h>
    int k;
    int binarysearch(int a[],int x,int low,int high)//a表示需要二分的有序数组(升序),x表示需要查找的数字,low,high表示高低位
    {
        if(low>high)
        {
            return -1;//没有找到
        }
        int mid=(low+high)/2;
        if(x==a[mid])//找到x
        {
            k=mid;
            return x;
        }
        else if(x>a[mid]) //x在后半部分
        {
            binarysearch(a,x,mid+1,high);//在后半部分继续二分查找
        }
        else//x在前半部分
        {
            binarysearch(a,x,low,mid-1);
        }
    }
    int main()
    {
        int a[10]={1,2,3,4,5,6,7,8,9,10};
        printf("请输入需要查找的正数字:
    ");
        int x;
        scanf("%d",&x);
        int r=binarysearch(a,x,0,9);
        if(r==-1)
        {
            printf("没有查到
    ");
        }
        else
        {
            printf("查到了,在数列的第%d个位置上
    ",k+1);
        }
        return 0;
    }

    经典样例二:全排列问题

    有1,2,3,4个数,问你有多少种排列方法,输出来

    仔细想想,采用分治的话,我们就要把大问题分解成很多的子问题,大问题是所有的排列方法

    那么我们分解得到的小问题就是以1开头的排列,以2开头的排列,以3开头的排列,以4开头的排列

    现在这些问题有能继续分解,比如以1开头的排列中,只确定了1的位置,没有确定2,3,4的位置,把2

    3,4三个又看成大问题继续分解,2做第二个,3做第二个,或者4做第二个

    一直分解下去,直到分解成的子问题只有一个数字的时候,不再分解

    因为1个数字肯定只有一种排列方式啊,现在我们分解成了很多的小问题,解决一个小问题就合并,合并成

    一个大点的问题,合并之后这个大点的问题也解决了,再将这些大点的问题合并成一个更大的问题,那么这

    个更大点的问题也解决了,直到最大的问题解决为止

    这个就是用分治的思想解决全排列问题,我主要想分析的是分治的思想者全排列问题上是怎么用的,不想分析具体全排列的做法,因为我觉得思想比方法更重要,在解题的时候深有体会,因为又的时候没有题是你做过的原题,全排列问题的具体做法参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8684313.html,也贴一下代码

    #include<string.h>
    #include<stdio.h>
    int k=0;
    char a[100];
    long long count=0;//全排列个数的计数
    void s(char a[],int i,int k)//将第i个字符和第k个字符交换
    {
        char t=a[i];
        a[i]=a[k];
        a[k]=t;
    }
    void f(char a[],int k,int n)
    {
        if(k==n-1)//深度控制,此时框里面只有一个字符了,所以只有一种情况,所以输出
        {
           puts(a);
           count++;
        }
        int i;
        for(i=k;i<n;i++)
        {
            s(a,i,k);
            f(a,k+1,n);
            s(a,i,k);//复原,就将交换后的序列除去第一个元素放入到下一次递归中去了,递归完成了再进行下一次循环。这是某一次循环程序所做的工作,这里有一个问题,那就是在进入到下一次循环时,序列是被改变了。可是,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下,所以每次交换后,要还原,确保初始状态一致。
        }
    }
    int main()
    {
        gets(a);
        int l=strlen(a);//字符串长度
        f(a,k,l);
        printf("全排列个数:%lld
    ",count);
        return 0;
    }

    经典样例三:整数划分问题

    给你一个数,问你所有的划分方式,比如4,4=1+3,4=1+1+2,4=2+2,4=1+1+1+1

    我们来分析一下,我们想用分治的话,就要找子问题,假设n是要划分的数,m说最大的加数,n=4,m=3

    分解成两类的子问题,一个是:一个是有m的情况,一个是没有m的情况,然后将有m的情况继续划分,分

    解成有m-1和没有m-1的情况,一直划分下去,直到m=1,比如n=4,m=3,划分成的子问题:有3,无

    3,有2,无2,有1,无1(没有意义,除非0+4=4),将这些子问题合并起来大问题就解决了,比如有

    3:1+3,没有3分成有2,和无2,有2:1+1+2,2+2,无2分成有1:1+1+1+1,一共四种解决方案

    我们来理一下思路:划分成子问题,解决这些子问题,合并

    但是注意:这个问题里面的子问题有很多是重复的,大量重复子问题,比如n=5,m=4,1+4=5,1+1+

    3=5,2+3=5,求3有几种划分方法的时候求了2次,如果n很大的话,那么就会有大量的重复子问题,这个时候可以采用dp(自己有点不理解重复子问题重复在哪里,觉得哪里有点不对劲)

    分析了一下题中分治的思想,具体做法参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8672198.html,也贴个代码

    /*
    整数划分问题
    :将一个整数划分为若干个数相加
    例子:
    整数4 最大加数 4
    4=4
    1+3=4
    1+1+2=4
    2+2=4
    1+1+1+1=4
    一共五种划分方案
    注意:1+3=4,3+1=4被认为是同一种划分方案
    */
    
    #include<stdio.h>
    int q(int n,int m)//n表示需要划分的数字,m表示最大的家数不超过m
    {
        if(m==1||n==1)//只要存在一个为1,那么划分的方法数肯定只有一种,那就是n个1相加
        {
            return 1;
        }else if(n==m&&n>1)//二者相等且大于1的时候,问题等价于:q(n,n-1)+1;意味着将最大加数减一之后n的划分数,然后加一,最后面那个一代表的是:0+n,这个划分的方案
        {
            return q(n,n-1)+1;
        }else if(n<m)//如果m>n,那么令m=n就ok,因为最大加数在逻辑上不可能超过n
        {
            return q(n,n);
        }else if(n>m)
        {
            return q(n,m-1)+q(n-m,m);//分为两种:划分方案没有m的情况+划分方案有m的情况
        }
        return 0;
    }
    int main()
    {
        printf("请输入需要划分的数字和最大家数:
    ");
        int n,m;
        scanf("%d %d",&n,&m);
        int r=q(n,m);
        printf("%d
    ",r);
        return 0;
    }

    经典样例4:归并排序

    把一个无序的数组,变成一个有序的数组,这个是大问题,根据分治的思想,要分解成很多的小问题,比如

    无序数组8个数,要使得数组有序,即使得这8个数有序,分解成两个子问题:使得前面4个数有序,使得后

    面的四个数有序,然后继续分解,在前面的4个数字中,又把它看成一个大问题,继续分解成两个小问题:

    使得前面两个数有序,使得后面两个数有序,直到小问题数组中只有一个数为止,因为一个数的数组肯定是

    有序的,小问题解决之后,还需要合并成一个大一点的问题,这样这个大一点的问题就也解决了,然后将两

    个大一点的问题继续合并成一个更大一点的问题,这样这个更大一点的问题也解决了,直到最后,最大的问

    题也解决了,这个就是分治思想在归并排序中的应用

    也贴个代码,附带详细的解析

    /*
    归并排序
    思想:
    1.分而治之,将一个无序的数列一直一分为二,直到分到序列中只有一个数的时候,这个序列肯定是有序的,因为只有一个数,然后将两个只含有一个数字的序列合并为含有两个数字的有序序列,这样一直进行下去,最后就变成了一个大的有序数列
    2.递归的结束条件是分到最小的序列只有一个数字的时候
    时间复杂度分析:
    最坏情况:T(n)=O(n*lg n)
    平均情况:T(n)=O(n*lg n)
    稳定性:稳定(两个数相等的情况,不用移动位置
    辅助空间:O(n)
    特点总结:
    高效
    耗内存(需要一个同目标数组SR相同大小的数组来运行算法)
    */
    #include<stdio.h>
    #define max 1024
    int SR[max],TR[max];
    int merge(int SR[],int TR[],int s,int m,int t)//SR代表两个有序序列构成的序列,s表示起始位置,m表示两个序列的分解位置,但是SR[m]仍是属于前面一个序列,t表示结束位置
    {//TR是一个空数组,用来存放排序好之后的数字
        int i=s,j=m+1,k=s;
        while(i<=m&&j<=t)
        {
            if(SR[i]<SR[j])
            {
                TR[k++]=SR[i++];
            }else
            {
                TR[k++]=SR[j++];
            }
        }
        while(i<=m)//当前面一个序列有剩余的时候,直接把剩余数字放在TR的后面
        {
            TR[k++]=SR[i++];
        }
        while(j<=t)//当后面一个序列有剩余的时候,直接把剩余数字放在TR的后面
        {
            TR[k++]=SR[j++];
        }
        return 0;
    }//该函数要求SR是由两个有序序列构成
    void copy(int SR[],int TR[],int s,int t)//把TR赋给SR
    {
        int i;
        for(i=s;i<=t;i++)
        {
            SR[i]=TR[i];
        }
    }
    int mergesort(int SR[],int s,int t)
    {
        if(s<t)//表示从s到t有多个数字
        {
            int m=(s+t)/2;//将序列一分为二
            mergesort(SR,s,m);//前一半序列继续进行归并排序
            mergesort(SR,m+1,t);//后一半序列同时进行归并排序,
            //以上递归调用的结束条件是s!<t,也就是进行分到只有一个数字进行归并排序的时候,一个序列只有一个数字,那么这个序列肯定是有序的
            //以上都是属于“分”的阶段,目的是获得两个有序的数列
            merge(SR,TR,s,m,t);//对这两个有序的数列,进行排序,变成一个同样大小但是有序的数列
            copy(SR,TR,s,t);//将在TR中排序好的数列给SR,方便SR递归调用归并排序,因为每次两个归并排序的结果都是保存在TR中的,现在要进行下一步就必须在TR数列的基础上面=进行,所以我们把TR给SR
        }else//表示从s到t只有一个数字(s==t),或者没有数字(s>t)
        {
            ;//空,也可省略,加一个else只是为了更好的理解程序
        }
        return 0;
    }
    int main()
    {
        int n;
        printf("请输入排序数字的个数:
    ");
        scanf("%d",&n);
        int i;
        for(i=0;i<n;i++)
        {
            scanf("%d",&SR[i]);
        }
        mergesort(SR,0,n-1);//升序排列
        for(i=0;i<n;i++)
        {
            printf("%d ",SR[i]);
        }
        printf("
    ");
        return 0;
    }

    经典样例五:棋盘覆盖问题

    不知道棋盘覆盖问题的请自行百度

    在棋盘的某个位置给了你一个不可覆盖点,现在大问题是问我们怎么用L形状块覆盖整个棋盘,现在我们要把大问题分解成很多的子问题:把整块大棋盘分成同样大小的四个棋盘,直到分解成的棋盘大小为1,就是只有一个格子的时候,不再分解,所以最小的子问题就是四个格子的棋盘,如果这个四个格子的棋盘有不可覆盖点的话,那么就进行棋盘覆盖,如果没有的话就进行覆盖点的构造然后在覆盖(先不讲怎么判断,怎么构造,只讲思想,具体做法我有专门的博客),所以这样我们就解决了这个四个格子的棋盘,把所有的这样的小问题解决的,也就是把解决好的小棋盘合并起来不就构成了我们需要的大棋盘吗?

    理清一下思路:

    分解棋盘(分解成四个小棋盘,一直分解下去,直到棋盘大小为1)

    解决问题(是直接覆盖还是先构造再覆盖)

    合并已经解决的问题(将已经解决的所有小问题合并起来就构成了我们需要覆盖的大棋盘,且此时大棋盘也

    已经覆盖好了)

    棋盘问题具体做法请参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8666209.html

    也贴一下代码吧

    #include<stdio.h>
    #define max 1024
    int cb[max][max];//最大棋盘
    int id=0;//覆盖标志位
    int chessboard(int tr,int tc,int dr,int dc,int size)//tr,tc代表棋盘左上角的位置,dr ,dc代表棋盘不可覆盖点的位置,size是棋盘大小
    {
        if(size==1)//如果递归到某个时候,棋盘大小为1,则结束递归
        {
            return 0;
        }
        int s=size/2;//使得新得到的棋盘为原来棋盘大小的四分之一
        int t=id++;
        if(dr<tr+s&&dc<tc+s)//如果不可覆盖点在左上角,就对这个棋盘左上角的四分之一重新进行棋盘覆盖
        {
            chessboard(tr,tc,dr,dc,s);
        }else//因为不可覆盖点不在左上角,所以我们要在左上角构造一个不可覆盖点
        {
            cb[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//因为不可覆盖点不在右上角,所以我们要在右上角构造一个不可覆盖点
        {
            cb[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//因为不可覆盖点不在左下角,所以我们要在左下角构造一个不可覆盖点
        {
            cb[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//因为不可覆盖点不在右下角,所以我们要在右下角构造一个不可覆盖点
        {
            cb[tr+s][tc+s]=t;
            chessboard(tr+s,tc+s,tr+s,tc+s,s);//在我们构造完不可覆盖点之后,棋盘的右下角的四分之一又有了不可覆盖点,所以就对右下角棋盘的四分之一进行棋盘覆盖
        }
    
        //后面的四个步骤都跟第一个类似
    }
    int main()
    {
        printf("请输入正方形棋盘的大小(行数):
    ");
        int n;
        scanf("%d",&n);
        printf("请输入在%d*%d棋盘上不可覆盖点的位置:
    ",n,n);
        int i,j,k,l;
        scanf("%d %d",&i,&j);
        printf("不可覆盖点位置输入完毕,不可覆盖点的值为-1
    ");
        cb[i][j]=-1;
        chessboard(0,0,i,j,n);
        for(k=0;k<n;k++)
        {
            printf("%2d",cb[k][0]);
            for(l=1;l<n;l++)
            {
                printf(" %2d",cb[k][l]);
            }
            printf("
    ");
        }
        return 0;
    }

    经典样例六:快速排序

    快速排序中分治的思想体现在哪里呢?

    首先我们要了解快速排序的思想,选择一个基准元素,比基准元素大的放基准元素后面,比基准元素小的放

    基准元素前面,这个叫做分区,每次分区都使得一个元素有序,进行很多次分区以后,数组就是有序数组

    了,为什么是这样呢?因为每次分区,我们都使得了基准元素有序,以比基准元素小的为例,这些元素都比

    基准元素小,放在基准元素前面,但这些比基准元素小的元素自己是无序的,确定的位置只有基准元素位

    置,有序之后这些元素与基准元素的相对位置是不会变的,变的只有这些元素自己内部的位置,因为进行一

    次分区就可以使得一位元素有序,所以进行很夺次分区以后,数组就是有序的了,

    那么分治的思想到底体现在哪里呢/

    第一步:把大问题分解成很多子问题(每次使得一位元素有序,分区操作可以做到)

    第二步:解决子问题(进行分区操作,每次使得一位元素有序)

    第三步:所有子问题解决了那么最大的问题也解决了

    再简单分析一下:第一次分区是对整个数组进行分区,确定了第一个基准元素的位置,然后对比基准元素大

    的和比基准元素小的进行分区,确定第二个和第三个基准元素的位置,如果序列够好的话(每次分区时,比

    基准元素大的元素和比基准元素小的元素每次都一样多)n*logn时间可解决

    关于快排的具体做法请参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8805233.html

    也贴个代码吧(随机化快排,基准元素选择是随机的)

    #include<bits/stdc++.h>
    using namespace std;
    #define n 5
    int a[n];
    void swap_t(int a[],int i,int j)
    {
        int t=a[i];
        a[i]=a[j];
        a[j]=t;
    }
    int par(int a[],int p,int q)
    {
        int i=p;//p是轴
        int x=a[p];
        for(int j=p+1;j<=q;j++)
        {
            if(a[j]<=x)
            {
                i++;
                swap_t(a,i,j);
            }
        }
        swap_t(a,p,i);
        return i;//轴位置
    }
    int Random(int p,int q)
    {
        return rand()%(q-p+1)+p;
    }
    int Randomizedpar(int a[],int p,int q)
    {
        int i=Random(p,q);
        swap_t(a,p,i);//第一个和第i个交换,相当于有了一个随机基准元素
        return par(a,p,q);
    }
    void RandomizedQuickSort(int a[],int p,int q)
    {
        if(p<q)
        {
            int r=Randomizedpar(a,p,q);
            printf("%d到%d之间的随机数:%d
    ",p,q,r);
            RandomizedQuickSort(a,p,r-1);
            RandomizedQuickSort(a,r+1,q);
        }
    }
    int main()
    {
        int i;
        for(i=0;i<n;i++)
        {
            scanf("%d",&a[i]);
    
        }
        RandomizedQuickSort(a,0,n-1);
        for(i=0;i<n;i++)
        {
            printf("%d
    ",a[i]);
        }
        return 0;
    }

    经典样例七:求第k小/大元素

    这是快排分区思想的应用,也要进行分区操作,和快排不同的是,快排分区之后还有继续处理基准元素

    两边的数据,而求k小/大不用,只用处理一边即可

    假如现在这里5个元素,分为1,2,3,4,5号位置

    第一种情况:假设求第3小元素,假设第一次分区的基准元素完成分区后在第2号位置,那么我们知道3>2

    所以只要对基准元素后面的元素继续分区就可以(注意k的值要变了,k代表的是在升序有序数组的1相对位

    置,现在对第一次分区的基准元素后面的元素进行分区操作,区间大小是变小了的,所以k值是要跟着变的)

    讲了这么多,所以分治的思想到底体现在哪里呢?

    跟快排一样,有分区操作,所以分治的思想在这里的体现和在快排的体现都是一样的,不同的是这里只要对

    基准元素前面元素或者后面元素进行继续分区(如果需要继续分区的话),而快排是基准元素两边都要继续

    分区的

    贴个代码(采用的是随机分区)

    #include<bits/stdc++.h>
    using namespace std;
    void swap_t(int a[],int i,int j)
    {
        int t=a[i];
        a[i]=a[j];
        a[j]=t;
    }
    int par(int a[],int p,int q)//p是轴,轴前面是比a[p]小的,后面是比a[p]大的
    {
        int i=p,x=a[p];
        for(int j=p+1;j<=q;j++)
        {
            if(a[j]>=x)
            {
                i++;
                swap_t(a,i,j);
            }
        }
        swap_t(a,p,i);
        return i;//返回轴位置
    }
    int Random(int p,int q)//返回p,q之间的随机数
    {
        return rand()%(q-p+1)+p;
    }
    int Randomizedpar(int a[],int p,int q)
    {
        int i=Random(p,q);
        swap_t(a,p,i);//第一个和第i个交换,相当于有了一个随机基准元素
        return par(a,p,q);
    }
    int RandomizedSelect(int a[],int p,int r,int k)
    {
        if(p==r)
            return a[p];
        int i=Randomizedpar(a,p,r);
        int j=i-p+1;
        printf("i=%d j=%d
    ",i,j);
        if(k<=j)
            return RandomizedSelect(a,p,i,k);
        else
            return RandomizedSelect(a,i+1,r,k-j);
    }
    int main()
    {
        int n;
        scanf("%d",&n);
        int a[n];
        for(int i=0;i<n;i++)
        {
            scanf("%d",&a[i]);
        }
        int x=RandomizedSelect(a,0,n-1,2);
        printf("%d
    ",x);
    }

    样例大概就是这些

    还有一个很重要的知识点差点忘了复习,分治的主定理

    分治的一般形式:

    T(N)=aT(N/b)+f(n)

    1.a==1 T(n)=O(logn)

    2.a!=1  T(n)=O(n的logb a)次方

    3. a==b T(n)=O(n*log b a)

    4 a<b T(n)=O(n)

    5. a>b  T(n)=O(n的log b a次方)

    用于估算分治算法的时间复杂的(数学log 的指数和底数不好表示。。。)

    735119-20170111112835275-168981902

    735119-20170111112841431-2047172832

  • 相关阅读:
    关于Maya Viewport 2.0 API 开发的介绍视频
    春节大假
    Some tips about the life cycle of Maya thread pool
    Can I compile and run Dx11Shader for Maya 2015 on my side?
    How to get current deformed vertex positions in MoBu?
    想加入全球首届的 欧特克云加速计划吗?
    三本毕业(非科班),四次阿里巴巴面试,终拿 offer(大厂面经)
    mac、window版编辑器 webstorm 2016... 永久破解方法。
    node 搭载本地代理,处理web本地开发跨域问题
    js 一维数组,转成嵌套数组
  • 原文地址:https://www.cnblogs.com/yinbiao/p/9215525.html
Copyright © 2011-2022 走看看