貪心算法和分治算法及經典例子

貪心算法

基本概念

所謂貪心算法是指,在對問題求解時,老是作出在當前看來是最好的選擇。也就是說,不從總體最優上加以考慮,他所作出的僅是在某種意義上的局部最優解。java

貪心算法沒有固定的算法框架,算法設計的關鍵是貪心策略的選擇。必須注意的是,貪心算法不是對全部問題都能獲得總體最優解,選擇的貪心策略必須具有無後效性,即某個狀態之後的過程不會影響之前的狀態,只與當前狀態有關。c++

 因此對所採用的貪心策略必定要仔細分析其是否知足無後效性。
 

貪心算法的基本思路

  1. 創建數學模型來描述問題。

    2. 把求解的問題分紅若干個子問題。算法

    3. 對每一子問題求解,獲得子問題的局部最優解。數組

    4. 把子問題的解局部最優解合成原來解問題的一個解。框架

 

貪心算法的 實現框架

貪心算法適用的前提是:局部最優策略能致使產生全局最優解函數

實際上,貪心算法適用的狀況不多。通常,對一個問題分析是否適用於貪心算法,能夠先選擇該問題下的幾個實際數據進行分析,就可作出判斷。
spa

 

貪心算法的實現框架

    從問題的某一初始解出發;
    while (能朝給定總目標前進一步)
    { 
          利用可行的決策,求出可行解的一個解元素;
    }
    由全部解元素組合成問題的一個可行解;
 

貪心策略的選擇

     由於用貪心算法只能經過解局部最優解的策略來達到全局最優解,所以,必定要注意判斷問題是否適合採用貪心算法策略,找到的解是否必定是問題的最優解。
 

例題分析

    下面是一個能夠試用貪心算法解的題目,貪心解的確不錯,惋惜不是最優解。
 
    [揹包問題]有一個揹包,揹包容量是M=150。有7個物品,物品能夠分割成任意大小。
    要求儘量讓裝入揹包中的物品總價值最大,但不能超過總容量。
 
    物品  A     B    C    D     E     F    G
    重量 35   30   60   50   40   10   25
    價值 10   40   30   50   35   40   30
    分析:
    目標函數: ∑pi最大(價值總和最大)
    約束條件是裝入的物品總重量不超過揹包容量:∑wi<=M( M=150)
    (1)根據貪心的策略,每次挑選價值最大的物品裝入揹包,獲得的結果是否最優?
    (2)每次挑選所佔重量最小的物品裝入是否能獲得最優解?
    (3)每次選取單位重量價值最大的物品,成爲解本題的策略。
 
    值得注意的是,貪心算法並非徹底不可使用,貪心策略一旦通過證實成立後,它就是一種高效的算法。
 好比,求最小生成樹的Prim算法和Kruskal算法都是漂亮的貪心算法。
    貪心算法仍是很常見的算法之一,這是因爲它簡單易行,構造貪心策略不是很困難。
    惋惜的是,它須要證實後才能真正運用到題目的算法中。
    通常來講, 貪心算法的證實圍繞着:整個問題的最優解必定由在貪心策略中存在的子問題的最優解得來的。
 
    對於例題中的3種貪心策略,都是沒法成立(沒法被證實)的,解釋以下:
    (1)貪心策略:選取價值最大者。反例:
    W=30
    物品:A     B   C
    重量:28  12  12
    價值:30  20  20
    根據策略,首先選取物品A,接下來就沒法再選取了,但是,選取B、C則更好。
    (2)貪心策略:選取重量最小。它的反例與第一種策略的反例差很少。
    (3)貪心策略:選取單位重量價值最大的物品。反例:
    W=30
    物品: A   B    C
    重量:28  20  10
    價值:28  20  10
    根據策略,三種物品單位重量價值同樣,程序沒法依據現有策略做出判斷,若是選擇A,則答案錯誤。
 

其實該狀況是符合貪心策略的,由於該總狀況無論先選哪兩個都會把揹包塞滿,由於該題物品能夠分割成任意大小,因此,就算空下一下,也能夠將最後一個物品分割,放進去,它們的單位重量的價值是同樣的,因此,最後揹包最後重量相同,重量相同那麼價值也相同。)設計

因此採用第三種策略,代碼以下:code

package cn.itcast.recursion; import java.util.Arrays; public class GreedyPackage { private int MAX_WEIGHT = 150; private int[] weights = new int[]{35, 30, 60, 50, 40, 10, 25}; private int[] values = new int[]{10, 40, 30, 50, 35, 40, 30}; private void packageGreedy(int capacity, int weights[], int[] values) { int n = weights.length;//物品的數量
        double[] r = new double[n];//性價比數組
        int[] index = new int[n];//性價比排序物品的下標
        for (int i = 0; i < n; i++) { r[i] = (double) values[i] / weights[i]; index[i] = i;//默認排序
 } double temp = 0;//對性價比進行排序
        for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { //降序,對性價比和對應下標進行排序
                if (r[i] < r[j]) { temp = r[i]; r[i] = r[j]; r[j] = temp; int x = index[i]; index[i] = index[j]; index[j] = x; } } } //排序好的重量和價值分別存到數組
        int[] w1 = new int[n]; int[] v1 = new int[n]; //排序好的重量和價值分別存到數組
        for (int i = 0; i < n; i++) { w1[i] = weights[index[i]]; v1[i] = values[index[i]]; } //用來裝物品的數組
        int[] x = new int[n]; //放入物品的最大價值
        int maxValue = 0; //放入物品的總重量
        int totalweights = 0; for (int i = 0; i < n; i++) { //物品重量比包的總容量小,表示還能夠裝得下
            if (w1[i] < capacity) { x[i] = 1;//表示該物品被裝了
                maxValue += v1[i]; System.out.println(w1[i] + "kg的物品被放進包包,價值:" + v1[i]); totalweights += w1[i]; capacity = capacity - w1[i]; } } System.out.println("總共放入的物品數量:" + Arrays.toString(x)); System.out.println("總共放入的物品總重量" + totalweights); System.out.println("放入物品的最大價值:" + maxValue); } public static void main(String[] args) { GreedyPackage greedyPackage = new GreedyPackage(); greedyPackage.packageGreedy(greedyPackage.MAX_WEIGHT, greedyPackage.weights, greedyPackage.values); } }

 

 

分治算法

定義

將原問題劃分紅n個規模較小,而且結構與原問題類似的子問題,遞歸地解決這些子問題,而後再合併其結果,就獲得原問題的解。blog

 

分治策略

  「分而治之」,大問題可以拆成類似的小問題,記住這些小問題須要具備類似性。然後將小問題的每一個解合成爲大問題的解。因此說大問題如何拆,小問題如何合併纔是這個算法最主要的一個思想。實際上不少算法如貪心算法,動態規劃等等都是要求把大問題拆成小問題。而分治算法的重要一點就是要適用於可以從新把小問題的解合併爲大問題的解。

 

使用分治算法的前提條件

  • 原問題與分解成的小問題具備相同的模式;
  • 原問題分解成的子問題能夠獨立求解,子問題之間沒有相關性,這一點是分治算法跟動態規劃的明顯區別
  • 具備分解終止條件,也就是說,當問題足夠小時,能夠直接求解;
  • 能夠將子問題合併成原問題,而這個合併操做的複雜度不能過高,不然就起不到減少算法整體複雜度的效果了

 

每一次遞歸都會涉及三個操做

  • 分解:將原問題分解成一系列子問題;
  • 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解;
  • 合併:將子問題的結果合併成原問題;

分治法適用條件

 

  一、該問題的規模縮小到必定程度就能夠很容易解決;

  二、該問題能夠分解爲若干個規模較小的相同問題,這裏注意是最優子結構性質;

  三、利用該問題分解出的子問題的解能夠合併爲該問題的解;

  四、該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共子問題;

  對於不少算法而言,第一條每每是必要的,由於數據量一旦大起來,問題每每複雜度上升的特別快。這裏就須要將這個大問題分解爲小問題。小問題處理起來更加方便。第2、三條的纔是分治思想的核心,由於不少時候咱們會採用遞歸的方式進行解決,因此在大問題分解爲小問題的時候須要保證小問題之間的相同性。單單分解爲小問題以後還不能算完成,必需要可以將小問題的解合併爲這個問題的最終解才能算真正用到了分治的思想。最後一條也是最關鍵的,各個子問題之間必需要保證獨立性,即不互相影響。若是相互之間有影響,這時候咱們採用的是動態規劃就更加好一點。

 

例題

  其實算法的思想不用講太多,可以化爲幾句話是最好的,下面就舉幾個例子來看看分治算法:

  例題一:二分查找,給定一個按照升序排好的數組array,要在這個數組中找出一個特定的元素x;

  當咱們遇到一個問題,徹底能夠在內心問本身下面四個問題:

  一、當前問題能不能切分?

  答:能切分,由於數組按照升序來排列。因此當x大於某個元素array[mid]時,x必定在array[mid]的右邊。以此再來切分。每次切一半

  二、分解出來的子問題相同嗎?

  答:相同,每一個子問題的數據集都是父問題的1/2倍。而且每次只比較子問題的中間的數據

  三、子問題的解能合併爲父問題的解嗎?

  答:不須要合併,子問題的解即爲父問題的解。

  四、子問題之間相互獨立嗎?

  答:獨立,子問題只是判斷,不須要和父問題有很強的關聯性(這裏能夠參考一下動態規劃算法,就能理解子問題之間怎麼判斷是獨立的)

 

 例題二:歸併排序,給定一個無序數組array[7]={49,38,65,97,76,13,27},使其變的有序

  一樣在本身內心問問4個問題

  一、當前問題能切分嗎?

  答:能,最簡單的就是兩個數之間的比較,這個數組能夠當作多個兩個數來比較

  二、分解出來的子問題是否相同?

  答:相同,都是兩個數比較大小。

  三、子問題的解可以合成父問題的解嗎?

   答:每兩個有序數組再按照必定順序合起來就是最終的題解。這裏就是有個合併的過程

  四、子問題之間相互獨立嗎?

  答:獨立,分到最小的時候子問題之間互不影響。

  下面是歸併排序代碼:

 

總結

  分治算法只是一種思想,不是一個具體的套路,只能說在遇見具體問題時咱們可以從這個思路去思考,切分問題?合併問題?子問題之間影響關聯大不大?這些都是具體問題具體考慮。還有不少不少題目是用了分治算法。也能夠多刷刷題

問題:   

設有n=2^k個運動員,要進行網球循環賽。如今要設計一個知足如下要求的比賽日程表

    (1)每一個選手必須與其餘n-1個選手各賽一場

    (2)每一個選手一天只能賽一次

    (3)循環賽一共進行n-1天

將比賽日程表設計成n行n列,表中除了第一列,其餘n-1列纔是咱們要的,數組下標行列都從0開始,第i行j列表明第(i+1)位選手在第j天的對手:

 

 以8個選手爲例子,下面是填表的步驟:

 

①咱們先初始化第一行各個數爲1~8(2~8爲:第1天 — 第7天);

②由於是遞歸,那麼要填8x8的左下角和右下角,分別須要知道它的右上角和左上角

③而8x8的盒子它的左上角是一個4x4的盒子,要填4x4的左下角和右下角,也分別須要知道它的右上角和左上角

④如今遞歸到4x4的盒子的左上角,是一個2x2的盒子,它不須要遞歸了,直接沿對角線填左下角和右下角的數字,也就是上面的圖②

⑤能夠看到,通過上面的②③步,咱們左上角4x4的盒子,它的·右上角和左上角已經知道了,那就能夠沿對角線填它的左下角和右下角了,因此出現了圖④

⑥其餘的依次類推

 

通俗易懂地講,就是若是你想填一個大的,你得先得出它左上角和右上角兩個盒子 , 再沿對角線分別抄到右下角和左下角而爲了得出它左上角和右上角,就須要遞歸了。

 

package cn.itcast.recursion; public class SportsSchedule { public void scheduleTable(int[][] table, int n) { if (n == 1) { table[0][0] = 1; } else { /* 填充左上區域矩陣 n值的變化:8 4 2 1 m值的變化:4 2 1 1 */
            int m = n / 2; scheduleTable(table, m); //填充右上區域矩陣
            for (int i = 0; i < m; i++) { for (int j = m; j < n; j++) { table[i][j] = table[i][j - m] + m; } } //填充左下區域矩陣
            for (int i = m; i < n; i++) { for (int j = 0; j < m; j++) { table[i][j] = table[i - m][j] + m; } } //填充右下區域矩陣
            for (int i = m; i < n; i++) { for (int j = m; j < n; j++) { table[i][j] = table[i - m][j - m]; } } } } public static void main(String[] args) { int[][] table = new int[8][8]; int n = 8; SportsSchedule schedule = new SportsSchedule(); schedule.scheduleTable(table, n); int c = 0; //打印二維數組
        for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { System.out.print(table[i][j] + " "); c++;//每打印一個數,c++
                if (c % n == 0) {//說明打印一行了
                    System.out.println();//換行
 } } } } }
相關文章
相關標籤/搜索