原創公衆號:bigsai
文章收錄在 bigsai-algorithm
分治算法(divide and conquer)是五大經常使用算法(分治算法、動態規劃算法、貪心算法、回溯法、分治界限法)之一,不少人在平時學習中可能只是知道分治算法,可是可能並無系統的學習分治算法,本篇就帶你較爲全面的去認識和了解分治算法。java
在學習分治算法以前,問你一個問題,相信你們小時候都有存錢罐的經歷,父母親人若是給錢都會往本身的寶藏中存錢,咱們每隔一段時間都會清點清點錢。可是一堆錢讓你處理起來你可能以爲很複雜,由於數據相對於大腦有點龐大了,而且很容易算錯,你可能會將它先分成幾個小份算,而後再疊加起來計算總和就得到這堆錢的總數了node
固然若是你以爲各個部分錢數量仍是太大,你依然能夠進行劃分而後合併,咱們之因此這麼可能是由於:git
固然這些錢都是想出來的……github
分治算法是用了分治思想的一種算法,什麼是分治?算法
分治,字面上的解釋是「分而治之」,就是把一個複雜的問題分紅兩個或更多的相同或類似的子問題,再把子問題分紅更小的子問題……直到最後子問題能夠簡單的直接求解,原問題的解即子問題的解的合併。在計算機科學中,分治法就是運用分治思想的一種很重要的算法。分治法是不少高效算法的基礎,如排序算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)等等。數組
將父問題分解爲子問題同等方式求解,這和遞歸的概念很吻合,因此在分治算法一般以遞歸的方式實現(固然也有非遞歸的實現方式)。分治算法的描述從字面上也很容易理解,分、治其實還有個合併的過程:微信
通常分治算法在正文中分解爲兩個即以上的遞歸調用,而且子類問題通常是不想交的(互不影響)。當求解一個問題規模很大很難直接求解,可是規模較小的時候問題很容易求解而且這個問題而且問題知足分治算法的適用條件,那麼就可使用分治算法。ide
那麼採用分治算法解決的問題須要 知足那些條件(特徵) 呢?工具
1 . 原問題規模一般比較大,不易直接解決,但問題縮小到必定程度就能較容易的解決。學習
2 . 問題能夠分解爲若干規模較小、求解方式相同(似)的子問題。且子問題之間求解是獨立的互不影響。
3 . 合併問題分解的子問題能夠獲得問題的解。
你可能會疑惑分治算法和遞歸有什麼關係?其實分治重要的是一種思想,注重的是問題分、治、合併的過程。而遞歸是一種方式(工具),這種方式經過方法本身調用本身造成一個來回的過程,而分治可能就是利用了屢次這樣的來回過程。
對於分治算法的經典問題,重要的是其思想,由於咱們大部分藉助遞歸去實現,因此在代碼實現上大部分都是很簡單,而本篇也重在講述思想。
分治算法的經典問題,我的將它分紅兩大類:子問題徹底獨立和子問題不徹底獨立。
1 . 子問題徹底獨立就是原問題的答案可徹底由子問題的結果推出。
2 . 子問題不徹底獨立,有些區間類的問題或者跨區間問題使用分治可能結果跨區間,在考慮問題的時候須要仔細借鑑下。
二分搜索是分治的一個實例,只不過二分搜索有着本身的特殊性
正常二分將一個完整的區間分紅兩個區間,兩個區間本應單獨找值而後確認結果,可是經過有序的區間能夠直接肯定結果在那個區間,因此分的兩個區間只須要計算其中一個區間,而後繼續進行一直到結束。實現方式有遞歸和非遞歸,可是非遞歸用的更多一些:
public int searchInsert(int[] nums, int target) { if(nums[0]>=target)return 0;//剪枝 if(nums[nums.length-1]==target)return nums.length-1;//剪枝 if(nums[nums.length-1]<target)return nums.length; int left=0,right=nums.length-1; while (left<right) { int mid=(left+right)/2; if(nums[mid]==target) return mid; else if (nums[mid]>target) { right=mid; } else { left=mid+1; } } return left; }
快排也是分治的一個實例,快排每一趟會選定一個數,將比這個數小的放左面,比這個數大的放右面,而後遞歸分治求解兩個子區間,固然快排由於在分的時候就作了不少工做,當所有分到最底層的時候這個序列的值就是排序完的值。這是一種分而治之的體現。
public void quicksort(int [] a,int left,int right) { int low=left; int high=right; //下面兩句的順序必定不能混,不然會產生數組越界!!!very important!!! if(low>high)//做爲判斷是否截止條件 return; int k=a[low];//額外空間k,取最左側的一個做爲衡量,最後要求左側都比它小,右側都比它大。 while(low<high)//這一輪要求把左側小於a[low],右側大於a[low]。 { while(low<high&&a[high]>=k)//右側找到第一個小於k的中止 { high--; } //這樣就找到第一個比它小的了 a[low]=a[high];//放到low位置 while(low<high&&a[low]<=k)//在low往右找到第一個大於k的,放到右側a[high]位置 { low++; } a[high]=a[low]; } a[low]=k;//賦值而後左右遞歸分治求之 quicksort(a, left, low-1); quicksort(a, low+1, right); }
快排在分的時候作了不少工做,而歸併就是相反,歸併在分的時候按照數量均勻分,而合併時候已是兩兩有序的進行合併的,由於兩個有序序列O(n)級別的複雜度便可獲得須要的結果。而逆序數在歸併排序基礎上變形一樣也是分治思想求解。
private static void mergesort(int[] array, int left, int right) { int mid=(left+right)/2; if(left<right) { mergesort(array, left, mid); mergesort(array, mid+1, right); merge(array, left,mid, right); } } private static void merge(int[] array, int l, int mid, int r) { int lindex=l;int rindex=mid+1; int team[]=new int[r-l+1]; int teamindex=0; while (lindex<=mid&&rindex<=r) {//先左右比較合併 if(array[lindex]<=array[rindex]) { team[teamindex++]=array[lindex++]; } else { team[teamindex++]=array[rindex++]; } } while(lindex<=mid)//當一個越界後剩餘按序列添加便可 { team[teamindex++]=array[lindex++]; } while(rindex<=r) { team[teamindex++]=array[rindex++]; } for(int i=0;i<teamindex;i++) { array[l+i]=team[i]; } }
最大子序列和的問題咱們可使用動態規劃的解法,可是也可使用分治算法來解決問題,可是最大子序列和在合併的時候並非簡單的合併,由於子序列和涉及到一個長度的問題,因此正確結果不必定全在最左側或者最右側,而可能出現結果的區域爲:
用一張圖能夠表示爲:
因此在具體考慮的時候須要將沒法遞歸獲得結果的中間那個最大值串的結果也算出來參與左側、右側值得比較。
力扣53. 最大子序和在實現的代碼爲:
public int maxSubArray(int[] nums) { int max=maxsub(nums,0,nums.length-1); return max; } int maxsub(int nums[],int left,int right) { if(left==right) return nums[left]; int mid=(left+right)/2; int leftmax=maxsub(nums,left,mid);//左側最大 int rightmax=maxsub(nums,mid+1,right);//右側最大 int midleft=nums[mid];//中間往左 int midright=nums[mid+1];//中間往右 int team=0; for(int i=mid;i>=left;i--) { team+=nums[i]; if(team>midleft) midleft=team; } team=0; for(int i=mid+1;i<=right;i++) { team+=nums[i]; if(team>midright) midright=team; } int max=midleft+midright;//中間的最大值 if(max<leftmax) max=leftmax; if(max<rightmax) max=rightmax; return max; }
最近點對是一個分治很是成功的運用之一。在二維座標軸上有若干個點座標,讓你求出最近的兩個點的距離,若是讓你直接求那麼枚舉暴力是個很是很是大的計算量,咱們一般採用分治的方法來優化這種問題。
若是直接分紅兩部分分治計算你確定會發現若是最短的若是一個在左一個在右會出現問題。咱們能夠優化一下。
在具體的優化方案上,按照x或者y的維度進行考慮,將數據分紅兩個區域,先分別計算(按照同方法)左右區域內最短的點對。而後根據這個兩個中較短的距離向左和向右覆蓋,計算被覆蓋的左右點之間的距離,找到最小那個距離與當前最短距離比較便可。
這樣你就能夠發現就這個一次的操做(不考慮子狀況),左側紅點就避免和右側大部分成點進行距離計算(O(n2)的時間複雜度)。事實上,在進行左右區間內部計算的時候,它其實也這樣遞歸的進行不少次分治計算。如圖所示:
這樣下去就能夠節省不少次的計算量。
可是這種分治會存在一種問題就是二維座標可能點都彙集某個方法某條軸那麼可能效果並不明顯(點都在x=2附近對x分割做用就不大),須要注意一下。
杭電1007推薦給你們,ac的代碼爲:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StreamTokenizer; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; public class Main { static int n; public static void main(String[] args) throws IOException { StreamTokenizer in=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in))); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); //List<node>list=new ArrayList(); while(in.nextToken()!=StreamTokenizer.TT_EOF) { n=(int)in.nval;if(n==0) {break;} node no[]=new node[n]; for(int i=0;i<n;i++) { in.nextToken();double x=in.nval; in.nextToken();double y=in.nval; // list.add(new node(x,y)); no[i]=new node(x,y); } Arrays.sort(no, com); double min= search(no,0,n-1); out.println(String.format("%.2f", Math.sqrt(min)/2));out.flush(); } } private static double search(node[] no, int left,int right) { int mid=(right+left)/2; double minleng=0; if(left==right) {return Double.MAX_VALUE;} else if(left+1==right) {minleng= (no[left].x-no[right].x)*(no[left].x-no[right].x)+(no[left].y-no[right].y)*(no[left].y-no[right].y);} else minleng= min(search(no,left,mid),search(no,mid,right)); int ll=mid;int rr=mid+1; while(no[mid].y-no[ll].y<=Math.sqrt(minleng)/2&&ll-1>=left) {ll--;} while(no[rr].y-no[mid].y<=Math.sqrt(minleng)/2&&rr+1<=right) {rr++;} for(int i=ll;i<rr;i++) { for(int j=i+1;j<rr+1;j++) { double team=0; if(Math.abs((no[i].x-no[j].x)*(no[i].x-no[j].x))>minleng) {continue;} else { team=(no[i].x-no[j].x)*(no[i].x-no[j].x)+(no[i].y-no[j].y)*(no[i].y-no[j].y); if(team<minleng)minleng=team; } } } return minleng; } private static double min(double a, double b) { // TODO 自動生成的方法存根 return a<b?a:b; } static Comparator<node>com=new Comparator<node>() { @Override public int compare(node a1, node a2) { // TODO 自動生成的方法存根 return a1.y-a2.y>0?1:-1; }}; static class node { double x; double y; public node(double x,double y) { this.x=x; this.y=y; } } }
到這裏,分治算法就講這麼多了,由於分治算法重要在於理解其思想,還有一些典型的分治算法解決的問題,例如大整數乘法、Strassen矩陣乘法、棋盤覆蓋、線性時間選擇、循環賽日程表、漢諾塔等問題你能夠本身研究其分治的思想和原理。
原創不易,bigsai請你幫兩件事幫忙一下: