最长上升子序列——O (nlogn)算法原因解析!为什么这样可以求出来!(附带动态规划dp + 二分查找讲解)

 

什么是最长上升子序列

 

 

网上流传着一个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之间。因为求的是最长上升子序列,而你的序列是235,这样显然是不符合题意的。

所以这样思考,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)进行比较,如果a[i]>a[k],那就是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();

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值