期末了,通過寫博客的方式復習一下算法,把自己知道的全部寫出來
分治:分而治之,把一個復雜的問題分解成很多規模較小的子問題,然后解決這些子問題,把解決的子問題合並起來,大問題就解決了
但是我們應該在什么時候用分治呢?這個問題也困擾了我很久,做題的時候就不知道用什么算法
能用分治法的基本特征:
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 的指數和底數不好表示。。。)