五大常用算法:一文搞懂分治算法

2020-12-08 紙鶴視界

原創公眾號:bigsai文章收錄在 bigsai-algorithm

前言

分治算法(divide and conquer)是五大常用算法(分治算法、動態規划算法、貪心算法、回溯法、分治界限法)之一,很多人在平時學習中可能只是知道分治算法,但是可能並沒有系統的學習分治算法,本篇就帶你較為全面的去認識和了解分治算法。

在學習分治算法之前,問你一個問題,相信大家小時候都有存錢罐的經歷,父母親人如果給錢都會往自己的寶藏中存錢,我們每隔一段時間都會清點清點錢。但是一堆錢讓你處理起來你可能覺得很複雜,因為數據相對於大腦有點龐大了,並且很容易算錯,你可能會將它先分成幾個小份算,然後再疊加起來計算總和就獲得這堆錢的總數了

當然如果你覺得各個部分錢數量還是太大,你依然可以進行劃分然後合併,我們之所以這麼多是因為:

計算每個小堆錢的方式和計算最大堆錢的方式是相同的(區別在於體量上)然後大堆錢總和其實就是小堆錢結果之和。這樣其實就有一種分治的思想。當然這些錢都是想出來的……

分治算法介紹

分治算法是用了分治思想的一種算法,什麼是分治

分治,字面上的解釋是「分而治之」,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。在計算機科學中,分治法就是運用分治思想的一種很重要的算法。分治法是很多高效算法的基礎,如排序算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)等等。

將父問題分解為子問題同等方式求解,這和遞歸的概念很吻合,所以在分治算法通常以遞歸的方式實現(當然也有非遞歸的實現方式)。分治算法的描述從字面上也很容易理解,分、治其實還有個合併的過程:

分(Divide):遞歸解決較小的問題(到終止層或者可以解決的時候停下)治(Conquer):遞歸求解,如果問題夠小直接求解。合併(Combine):將子問題的解構建父類問題一般分治算法在正文中分解為兩個即以上的遞歸調用,並且子類問題一般是不想交的(互不影響)。當求解一個問題規模很大很難直接求解,但是規模較小的時候問題很容易求解並且這個問題並且問題滿足分治算法的適用條件,那麼就可以使用分治算法。

那麼採用分治算法解決的問題需要 滿足那些條件(特徵)呢?

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請你幫兩件事幫忙一下:

點讚在看, 您的肯定是我在平臺創作的源源動力。微信搜索「bigsai」,關注我的公眾號,不僅免費送你電子書,我還會第一時間在公眾號分享知識技術。加我還可拉你進力扣打卡群一起打卡LeetCode。記得關注、咱們下次再見!

相關焦點

  • 算法系列總結:分而治之——分治算法
    分治算法的基本思想是將一個規模為N的問題分解為K個規模較小的子問題,這些子問題相互獨立且與原問題性質相同。求出子問題的解,就可得到原問題的解。分治法解題的一般步驟:(1)分解,將要解決的問題劃分成若干規模較小的同類問題;(2)求解,當子問題劃分得足夠小時,用較簡單的方法解決;(3)合併,按原問題的要求,將子問題的解逐層合併構成原問題的解。一言以蔽之:分治法的設計思想是,將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
  • 分治算法詳解:表達式的不同優先級
    ,回溯(DFS)算法,BFS 算法,貪心算法,雙指針算法,滑動窗口算法,現在就差個分治算法沒寫了,今天來寫一下,集齊七顆龍珠,就能召喚神龍了~其實,我覺得回溯、分治和動態規划算法可以劃為一類,因為它們都會涉及遞歸。
  • 五大常用算法:動態規划算法
    二、基本思想與策略基本思想與分治法類似,也是將待求解的問題分解為若干個子問題(階段),按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各種可能的局部解,通過決策保留那些有可能達到最優的局部解,丟棄其他局部解。依次解決各子問題,最後一個子問題就是初始問題的解。
  • 算法原理:大數據處理的分治思想!
    實際上,萬變不離其宗,它的本質就是分治算法思想,分治算法。如何理解分治算法?為什麼說 MapRedue 的本質就是分治算法呢?分治是一種被廣泛應用的有效方法,它的基本思想是把最初的問題分解成若干子問題,然後,在逐個解決各個子問題的基礎上得到原始問題的解。
  • 一文搞懂貪心算法
    在一些情況下,即使貪心算法不能得到整體最優解,其最終結果卻是最優解的很好近似。題一、活動安排問題問題表述:設有n個活動的集合E = {1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每個活i都有一個要求使用該資源的起始時間si和一個結束時間fi,且si < fi 。
  • 機器學習中常用優化算法介紹
    > 作者 | Walker編輯 | 磐石出品 | 磐創AI技術團隊【磐創AI導讀】:本文主要介紹了常用的一些機器學習中常用的優化算法
  • 一文讀懂動態規划算法
    (點擊上方藍字,快速關注我們)轉自:紅臉書生http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741374.html好文投稿基本思想與策略基本思想與分治法類似,也是將待求解的問題分解為若干個子問題(階段),按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各種可能的局部解,通過決策保留那些有可能達到最優的局部解,丟棄其他局部解。依次解決各子問題,最後一個子問題就是初始問題的解。
  • 算法基礎:五大排序算法Python實戰教程
    :五大排序算法Python實戰教程排序算法的複雜度排序是每個軟體工程師和開發人員都需要掌握的技能。不同的排序算法很好地展示了算法設計上如何強烈的影響程序的複雜度、運行速度和效率。讓我們看一下前6種排序算法,看看如何在Python中實現它們!
  • 一文圖解 Java 源碼的插入排序算法
    前言上一講《程序兵法:Java String 源碼的排序算法(一)》講了什麼是選擇問題,什麼是比較能力。選擇問題,是假設一組 N 個數,要確定其中第 K 個最大值者。算法是為求解一個問題。那什麼是算法?算法是某種集合,是簡單指令的集合,是被指定的簡單指令集合。確定該算法重要的指標:第一是否能解決問題;第二算法運行時間,即解決問題出結果需要多少時間;還有所需的空間資源,比如內存等。很多時候,寫一個工作程序並不夠。
  • Python算法基礎班 | 轉專業必備,從零開始學Python!
    計算機內存模型線性數據結構 II Linear Data Structure II鍊表(Linked list)鍊表的構建鍊表的常用操作算法的時間複雜度分析棧(Stack)棧的實現及常用操作棧在作業系統中的應用隊列(Queue)
  • Python 算法基礎班 | 轉專業必備,從零開始學Python!
    計算機內存模型線性數據結構 II Linear Data Structure II鍊表(Linked list)鍊表的構建鍊表的常用操作算法的時間複雜度分析棧(Stack)棧的實現及常用操作棧在作業系統中的應用隊列(Queue)
  • Python 算法基礎班 | 從零開始學Python!
    列表(List)和元組(Tuple)列表的概念及常用操作元組的概念及常用操作字符串(String)字符串的概念及常用操作字符和 Unicode什麼是引用(Reference)?計算機內存模型線性數據結構 II Linear Data Structure II鍊表(Linked list)鍊表的構建鍊表的常用操作算法的時間複雜度分析棧(Stack)棧的實現及常用操作棧在作業系統中的應用隊列(Queue)
  • Go語言實現常用排序算法
    目前被認為綜合最好的高級排序算法是快速排序,快速排序的平均用時最短,大多數的編程庫內置的排序算法都是它。堆排序也是一種很快的排序算法,通過維持一棵二叉樹,樹的根節點總是最大或最小從而可實現排序。歸併排序和快速排序一樣使用分治法,遞歸地先使每個子序列有序,再將兩個有序的序列進行合併成一個有序的序列。
  • 從此篇文章入手,輕輕鬆鬆學算法
    歡迎點擊上方藍字「iOSer」關注我們再點擊右上角「...」菜單,選擇「設為星標
  • 九章算法基礎班(Python) | 免費試聽 從零開始學Python!
    列表(List)和元組(Tuple)列表的概念及常用操作元組的概念及常用操作字符串(String)字符串的概念及常用操作字符和 Unicode什麼是引用(Reference)?計算機內存模型線性數據結構 II Linear Data Structure II鍊表(Linked list)鍊表的構建鍊表的常用操作算法的時間複雜度分析棧(Stack)棧的實現及常用操作棧在作業系統中的應用隊列(Queue)隊列的實現及常用操作
  • 一文搞懂負載均衡中的一致性哈希算法
    不同領域場景不同,需要顧及的因素也有所差異,本文主要討論在負載均衡中一致性哈希算法的設計。在介紹一致性哈希算法之前,我將會介紹一些哈希算法,討論它們的區別和使用場景。也會給出一致性哈希算法的 Java 通用實現,可以直接引用,文末會給出 github 地址。
  • 這次,讓算法走下神壇!
    很多程式設計師對各種排序、搜索、遍歷等常用算法了如指掌,但遇到實際問題時還是束手無策。耳熟能詳的三本算法書《算法》、《算法導論》、《算法圖解》,卻一本都沒讀完:「看了半年的《算法》這本書,才看了幾十頁」「4 年了,還是沒有啃完《算法導論》」有的朋友覺得像人工智慧、數據搜索與挖掘這樣高薪的工作才用得上算法,覺得算法深不可測,只可遠觀。這次,我將會用一些通俗的案例解釋算法,讓算法走下神壇。
  • 程式設計師:算法導論,分治法、歸併排序,偽代碼和Java實現
    分治法我們首先先介紹分治法。分治法的思想:將原問題分解為幾個規模較小但類似於原問題的子問題,遞歸地求解這些子問題,然後在合併這些子問題的解來解決原問題的解。還是拿撲克牌舉例子,假設桌上有兩堆牌面朝上的牌(牌面朝上:有值),每堆都已排序,最小的牌在頂上。
  • 程式設計師為什麼要學算法?
    很多人覺得像人工智慧、數據搜索與挖掘這樣高薪的工作才用得上算法,覺得算法深不可測。但是這些其實都不是具體的算法,而是一系列算法的集合。數據挖掘領域涉及的算法和其他領域算法只是問題域不同。數據挖掘和機器學習常用的方法,比如決策樹、貝葉斯學習、神經網絡、遺傳算法等,在其他領域也有應用。在人工智慧領域或各種專家系統中,決策樹算法也是常用算法。各種算法在不同領域扮演不同角色,本質上沒有區別,一通百通。
  • 九章基礎算法班(Python) | 零基礎入門,一個月實現網頁爬蟲!
    列表(List)和元組(Tuple)列表的概念及常用操作元組的概念及常用操作字符串(String)字符串的概念及常用操作字符和 Unicode什麼是引用(Reference)?計算機內存模型線性數據結構 II Linear Data Structure II鍊表(Linked list)鍊表的構建鍊表的常用操作算法的時間複雜度分析棧(Stack)棧的實現及常用操作棧在作業系統中的應用隊列(Queue)隊列的實現及常用操作