最長上升子序列——O (nlogn)演算法原因解析!為什麼這樣可以求出來!(附帶動態規劃dp + 二分查詢講解)

zxfBdd發表於2020-09-25

 

什麼是最長上升子序列

 

 

網上流傳著一個O (nlogn)演算法,大體是這樣的。

模擬一個棧,如果當前的數比棧頂元素大,就要入棧,如果比棧頂元素小,就二分查詢到剛好比當前數大的數,然後進行替換。本例的流程是這樣的。

2 入棧 當前棧:2

5 比2大 入棧 當前棧:2 5

3 比5小 替換5 當前棧:2 3

4 比3大 入棧 當前棧: 2 3 4

1比2小 替換2 當前棧:1 3 4

7比4大 入棧 當前棧:1 3 4 7

6比7小 替換7 當前棧:1 3 4 6

 

為什麼可以這麼做?為什麼這樣就可以求出最大上升子序列的長度?為什麼求不出最大上升子序列的最大上升序列?相信所有人看完了都會有這樣一個疑問。

其實這個方法很正確,只是這樣的表示形式,把人們都誤導了。其實正確的思維流程應該是這樣的!

 

初始的2

5

 

這時是3,比5小,建立一個分支

4只能跟在3後面

1比2小 建立一個分支

7所以的分支後面都可以加

6比7小,建立分支

這樣才是正確的思維流程!你已經成功了一半了!

 

還有一個大問題,就是為什麼要這樣思考呢?(模擬一個棧,如果當前的數比棧頂元素大,就要入棧,如果比棧頂元素小,就二分查詢到剛好比當前數大的數,然後進行替換。)

 

需要一個具體例子來分析。當目前是 2 5這個情況的時候,我們遇到了3。首先排除一種做法,3插入到2 5 之間。因為求的是最長上升子序列,而你的序列是253,這樣顯然是不符合題意的。

所以這樣思考,23有沒有可能才是真正的最優序列?所以我們25也有可能,23也有可能(實際上25已經是不可能了)

當前是2-3 2-5,遇到了4,按照我們之前的思維方式,那麼4和5是不是也要比較一下?2-4難道就沒有可能嗎?

但是我們有一個2-3這個序列了,2-3-4這個情況顯然是最優的,所以2-4直接不要了。(印證了 當前的數比棧頂元素大就要入棧 這句話)

最後分析一下1,當前 2-3-4 2-5兩個序列。1比2小,當然存在這樣一種可能1開頭是最好的所以我們如上面的圖片一樣新增了1這個分支(當然本例的資料不太友好,如果我們 當前存在5-6-7,5-8兩個序列呢?你碰到1的時候,完全有可能後面是234啊,所以你1完全有理由成為一個新增的分支)

例子舉到這裡我們就講明白了。

 

所以演算法就呼之欲出了。

如果你僅僅是求最長長度的需求,直接用網上的方法就可以。

如果你還需要求出這個具體序列來,毫無疑問你採用空間換時間的方法是最好的。新建一個二維陣列,每當遇到要新建分支的情況,就增加一列,簡單方便。具體程式碼我就不寫了,畢竟我當前做的題僅僅是求長度而已,嘿嘿,相信理解原理的你可以輕鬆寫出演算法來。

 

 

下面講一下這一題樸素動態規劃的思路。

這一題我們分析出來的dp思路是,dp[i]代表的是包含當前點位的最長上升子序列,意思就是當前求出的最長上升子序列的最後一位,一定要是i位,這一點對動態規劃有了解的人肯定明白,因為這樣才能實現狀態的轉移。

 

狀態是怎麼轉移的?

開兩個迴圈,不斷用當前的a[i]和之前的數a[k](0<k<i)進行比較,如果比他大,那就是dp[i]=dp[k] + 1,如果相等就是dp[i] = dp[k],如果比他小就只能棄掉了

 

上程式碼

 

//樸素dp o n^2
int dynamic(int[] a) {

    int n = a.length;

    int[] dp = new int[n];

    dp[0] = 1;

    int max = 1;

    for (int i = 1; i < n; i ++) {

        dp[i] = 1;

        for (int j = 0; j < i; j ++) {

            if (a[j] < a[i]) {

                dp[i] = Math.max(dp[j] + 1, dp[i]);

            } else if (a[j] == a[i]) {

                dp[i] = Math.max(dp[j], dp[i]);

            }

        }

        max = Math.max(max, dp[i]);

    }

    return max;

}

 

 

二分優化dp,關於二分查詢我覺得太簡單了,就不講了,即使不查閱資料應該也要知道怎麼去實現

 

//樸素dp優化 二分查詢 模擬棧 -空間換時間
//二分不難不用學就能寫出來,但是要搞明白為什麼要這麼做
int dynamic2(int[] a) {

    int n = a.length;

    List<Integer> list = new ArrayList<>();

    list.add(a[0]);

    for (int i = 1; i < n; i ++) {

        int topIndex = list.size() - 1;

        if (a[i] > list.get(topIndex)) {

            list.add(a[i]);

        } else {

            int startIndex = 0;
            int endIndex = topIndex;

            while (true) {//當end小於start退出

                int index = (endIndex + startIndex) / 2;

                //對本題而言,不存在相等的情況
                //當前元素比中間數小
                if (list.get(index) > a[i]) {

                    endIndex = index - 1;

                    if (endIndex == startIndex) {

                        if (a[i] < list.get(startIndex)) {

                            list.set(startIndex, a[i]);

                        } else {

                            list.set(index, a[i]);

                        }

                        break;

                    }

                    //如果當前元素比中間數大
                } else if (list.get(index) < a[i]) {

                    startIndex = index + 1;

                    if (startIndex == endIndex) {

                        list.set(startIndex, a[i]);

                        break;

                    }

                }

            }

        }

    }

    return list.size();

}

相關文章