[-演算法篇-] 最大子序列和

張風捷特烈發表於2019-04-17

零、前言

最大子序列和問題

這個問題是《資料結構和演算法分析》一書中的一個問題,書中給了四種演算法
我感覺它是入手演算法很不錯的一個問題,本文演算法源於書中,但文中包含了我的分析和理解

[-演算法篇-] 最大子序列和

2.題目的分析

也許很多人看到題目就懵圈了,這裡解釋一下:

拿一個例子來說: -2,11,-4,13,-5,-2
它的子序列是什麼意思: 連續的若干個元素, 比如:11,-4和-4,13,-5,-2等
該問題也就是說:這些子序列元素的和最大是多少,以下列出了所有子序列的情況及子序列和
複製程式碼

[-演算法篇-] 最大子序列和

一、第一種演算法

1.具體演算法

根據上面分析的圖,很容易可以想到第一種演算法:

private static int maxSonNum(int[] a) {
    int maxSum = 0;
    for (int i = 0; i < a.length; i++) {
        for (int j = i; j < a.length; j++) {
            int sum = 0;
            for (int k = i; k <= j; k++) {
                sum += a[k];
            }
            if (sum > maxSum) {
                maxSum = sum;
            }
        }
    }
    return maxSum;
}
複製程式碼

2.演算法分析

可以說下面是我發明的for迴圈的簡單圖示:簡稱for圖

[-演算法篇-] 最大子序列和

[1].一條線代表一個for迴圈
[2].線的左邊是for迴圈中索引的變數名
[3].線兩端數字表示迴圈的範圍,空心代表不包含,實心表示包含
[4].箭頭表示某個時刻的遍歷情況
[5].內部迴圈在下邊,可以看成底部箭頭向右走,走到頭上面的箭頭右動一格(類似時鐘,分針,秒針)
複製程式碼

想像一下,看腦中是否可以出現for迴圈中運動的場景:

k 箭頭向右走,計算`i~j`子序列的和,並維護maxSum的值。當走到 j 時,發出"刺啦"一聲
j 箭頭向右移一格,發出一聲"嗒"。然後k箭頭再跑,直到j跑到a.length之後  
i 箭頭右移一格, 發出一聲"叮",j 箭頭在k箭頭的推動下一點點跑,直到i跑到了a.length

交響樂大概就是這樣的吧:
...
刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
...
複製程式碼

從上面來看,這個演算法雖然可以解決問題,使用了三層,每層都是N的複雜度
時間複雜度為O(N^3),可想而知,非常耗時


二、第二種演算法

1.具體演算法
private static int maxSonNum(int[] a) {
    int maxSum = 0;
    for (int i = 0; i < a.length; i++) {
        int sum = 0;
        for (int j = i; j < a.length; j++) {
            sum += a[j];
            if (sum > maxSum) {
                maxSum = sum;
            }
        }
    }
    return maxSum;
}
複製程式碼

2.演算法分析

[-演算法篇-] 最大子序列和

剛才:在j的迴圈中新開了一個k迴圈計算i~j的元素和
如 i=0,j=4 時:子序列 -2,11,-4,13,-5 用一個k迴圈就算他們的和

這裡:在j的迴圈中維護sum變數也能達到一樣的效果:
如 i=0,j=4 時:sum= sum + -5 即 -2+11-4+13-5,然後維護maxSum
這樣就減少一層for迴圈:時間複雜度為O(N^2)
複製程式碼

三、第三種演算法

1.具體演算法

分治的思想,也是本文最想講的內容

private static int maxSumRec(int[] a, int start, int end) {
    if (start == end) {
        return a[start];//一個元素的最大子序列是其本身
    }
    
    int center = (start + end) / 2;
    int maxLeftSum = maxSumRec(a, start, center);//1.左半最大子序列和
    int maxRightSum = maxSumRec(a, center + 1, end);//2.右半最大子序列和

    /*
    3.如果最大子序列和貫穿左右時:
        |--- 1.子序列是連續的
        |--- 2.中點和中點的後面元素在最大子序列中
    */
    //尋找左半中含左半一個元素的最大子序列和
    int maxLeftSBorderSum = 0, leftBorderSum = 0;
    for (int i = center; i >= start; i--) {
        leftBorderSum += a[i];
        if (leftBorderSum > maxLeftSBorderSum) {
            maxLeftSBorderSum = leftBorderSum;
        }
    }
    //判斷右半中含右半第一個元素的最大子序列和
    int maxRightSBorderSum = 0, rightBorderSum = 0;
    for (int i = center + 1; i <= end; i++) {
        rightBorderSum += a[i];
        if (rightBorderSum > maxRightSBorderSum) {
            maxRightSBorderSum = rightBorderSum;
        }
    }
    return max3(maxLeftSum, maxRightSum, maxLeftSBorderSum + maxRightSBorderSum);
}

private static int max3(int a, int b, int c) {
    int max;
    if (a > b && a > c) {
        max = a;
    } else if (c > a && c > b) {
        max = c;
    } else {
        max = b;
    }
    return max;
}
複製程式碼

2.演算法分析

將一個大問題拆解成若干個小問題,使用遞迴來解決 雖然演算法複雜了很多,但執行的時間複雜度降到了O(NlongN),還是很有價值的

[-演算法篇-] 最大子序列和

最大子序列和可能存在於:
1.左半的序列:maxLeftSum
2.由半的序列:maxRightSum
3.該序列貫穿左右
    |--- 判斷左半(子序列含:左半最後一個元素):maxLeftSBorderSum
    |--- 判斷右半(子序列含:右半第一個元素):maxRightSBorderSum
尋找子序列即(含左半最後一個元素),又含(右半第一個元素),說明兩半序列可連續

int maxLeftSBorderSum = 0, leftBorderSum = 0;
for (int i = center; i >= start; i--) {//遍歷包含center點的最側子序列取最大和
    leftBorderSum += a[i];
    if (leftBorderSum > maxLeftSBorderSum) {
        maxLeftSBorderSum = leftBorderSum;
    }
}

int maxRightSBorderSum = 0, rightBorderSum = 0;
for (int i = center + 1; i <= end; i++) {//遍歷包含center+1點的右側子序列取最大和
    rightBorderSum += a[i];
    if (rightBorderSum > maxRightSBorderSum) {
        maxRightSBorderSum = rightBorderSum;
    }
}
maxLeftSBorderSum和maxRightSBorderSum由於包含center點和center+1點
所以是貫穿左右的子序列,並且其和是[貫穿左右的子序列]中最大的
複製程式碼

具體來分析一下問題的分解

Q1: 求 -2 11 -4 13 -5 -2 的最大子序列和

Q1可以分解為下面三個問題的最大值:
    |---Q1.1: -2 11 -4 的最大子序列和
    |---Q1.2: 13 -5 -2 的最大子序列和
    |---Q1.3: 序列和最大值貫穿左右時的最大值:
        |--- 判斷左半:序列含左半最後一個元素的子序列最大值
        |--- 判斷右半:序列含右半第一個元素子序列最大值
        
Q1.1可以分解為下面三個問題的最大值:
    |---Q1.1.1: -2 11  的最大子序列和
    |---Q1.1.2: -4     的最大子序列和 -4
    |---Q1.1.3: 序列和最大值貫穿左右時的最大值:
        |--- 判斷左半:序列含左半最後一個元素的子序列最大值 11
        |--- 判斷右半:序列含右半第一個元素子序列最大值 -4

Q1.1.1可以分解為下面三個問題的最大值:
    |---Q1.1.1.1: -2     的最大子序列和 -2
    |---Q1.1.1.2: 11     的最大子序列和 11
    |---Q1.1.1.3: 序列和最大值貫穿左右時的最大值:
        |--- 判斷左半:序列含左半最後一個元素的子序列最大值 -2
        |--- 判斷右半:序列含右半第一個元素子序列最大值 11

所以 Q1.1.1 = 11        Q1.1.2 = -4         Q1.1.3 = 7
所以 Q1.1 = 11          同理分解 Q1.2 = 13  Q1.3 = 7 + 13 =20 
所以 Q1 = 20 得解
複製程式碼

四、 第四種演算法

1.具體演算法
private static int maxSonNum4(int[] a) {
    int maxSum = 0, sum = 0;
    for (int i = 0; i < a.length; i++) {
        sum += a[i];
        if (sum > maxSum) {
            maxSum = sum;
        } else if (sum < 0) {
            sum = 0;
        }
    }
    return maxSum;
}
複製程式碼

2.分析

反正我是很難想像第一個寫出這個演算法的人腦子是怎麼想的,也很難去說明這個演算法的正確性
這在O(N)的時間完成了最大子序列和問題,這種"簡潔和聰明以及高效"也許就是演算法的迷人之處。

 -2 11 -4 13 -5 -2 
                sum         maxSum      sum
 i = 0          -2            0          0
 i = 1          11            11         11
 i = 2          7             11         7
 i = 3          20            20         20
 i = 4          15            20         15
 i = 5          13            20         13
複製程式碼

一種演算法可以從O(N^3)優化到O(N^2),再用分治優化到O(NlogN)
最後被一個O(N)的演算法亮瞎的的鈦合金言,所以這個問題真的挺有意思。

相關文章