一次性弄懂到底什么叫做分治思想(含有大量經典例題,附帶詳細解析)


期末了,通過寫博客的方式復習一下算法,把自己知道的全部寫出來

分治:分而治之,把一個復雜的問題分解成很多規模較小的子問題,然后解決這些子問題,把解決的子問題合並起來,大問題就解決了

但是我們應該在什么時候用分治呢?這個問題也困擾了我很久,做題的時候就不知道用什么算法

能用分治法的基本特征:

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("請輸入需要查找的正數字:\n");
    int x;
    scanf("%d",&x);
    int r=binarysearch(a,x,0,9);
    if(r==-1)
    {
        printf("沒有查到\n");
    }
    else
    {
        printf("查到了,在數列的第%d個位置上\n",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\n",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("請輸入需要划分的數字和最大家數:\n");
    int n,m;
    scanf("%d %d",&n,&m);
    int r=q(n,m);
    printf("%d\n",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("請輸入排序數字的個數:\n");
    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("\n");
    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("請輸入正方形棋盤的大小(行數):\n");
    int n;
    scanf("%d",&n);
    printf("請輸入在%d*%d棋盤上不可覆蓋點的位置:\n",n,n);
    int i,j,k,l;
    scanf("%d %d",&i,&j);
    printf("不可覆蓋點位置輸入完畢,不可覆蓋點的值為-1\n");
    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("\n");
    }
    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\n",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\n",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\n",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\n",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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM