多条件下的最长上升子序列
马戏团人塔
如本题,要求身高、体重分别递增,且都只能<,不能<=
本文将介绍两种写法:
- 二分法 : O(nlogN)
2.常规dp O(n2)
本文主要解释:
- 二分法中的排序要求及原因
- dp的小优化
若对如何求最长上升子序列完全不了解的,请先参考:最长上升子序列(LIS)算法
二分法
用NlogN的二分查找法求解时,必须按照身高递增排序;身高相同时,体重递减排序,否则可能会出现多个身高相同,而体重递增的答案,则不符合<的要求。
Arrays.sort(person, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
而原因就是采用二分查找法时,统计数据时无法区分该数据是身高不同还是相同情况下的更大值,而只有不同的情况下才是有效数据
dp[]存储的是对应长度LIS的最小末尾,而不是LIS
如果都采用递增排序,如本题数据,就会出现
[1][4],[2][5],[2][6],[2][7],[3][8],
其中[2][5],[2][6],[2][7]都会算作有效数据,
只有按上述做法,排成
[1][4],[2][7],[2][6],[2][5],[3][8],
才能保证[2][6],[2][5]不会生效,因为[2][7]已经率先“占领了dp的该位子了,后续只会更新该位子上的值,而不会因为[2][6],[2][5]新增数据
public static int bestSeqAtIndex(int[] height, int[] weight) {
int len = height.length;
int[][] person = new int[len][2];
for (int i = 0; i < len; ++i)
person[i] = new int[]{height[i], weight[i]};
//身高递增排序
//身高相同时,体重递减排序
Arrays.sort(person, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
int[] dp = new int[len];
int res = 0;
for (int[] pair : person) {
//二分搜索
//不了解可参考:https://blog.csdn.net/Unknownfuture/article/details/104503235
int i = Arrays.binarySearch(dp, 0, res, pair[1]);
if (i < 0)
i = -(i + 1);
dp[i] = pair[1];
if (i == res)
++res;
}
return res;
}
public static void main(String[] args) {
int[] height = {1, 2, 2, 2, 3};
int[] weight = {4, 7, 5, 6, 8};
int max=bestSeqAtIndex(height,weight);
System.out.println(max);
}
注意:之所以要把数据按照体重递减处理,完全只是为了迎合NlogN的解法特性,如果换成正常动态规划,完全可以直接按身高、体重都递增,因为判断条件不会受到这样排序的影响,见下
dp(超时,但是是正确的)
此时的dp数组的含义:
dp[i] 表示以 i 结尾的最长上升子序列长度
public int bestSeqAtIndex(int[] height, int[] weight) {
int len = height.length;
int[][] person = new int[len][2];
for (int i = 0; i < len; ++i)
person[i] = new int[]{height[i], weight[i]};
//排序规则为:身高递增、身高相同时,按体重递增
Arrays.sort(person, (a, b) -> a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]);
int[] dp = new int[len];
//因为dp数组含义不一样,
//每个i都至少可以以自己结尾,即自己一个元素作为最长上升子序列,长度为1
Arrays.fill(dp,1);
int ans = 0;
//这里i从0开始,只是因为求解dp数组的最大值max的逻辑在本循环中,若数组只有一组,则ans=0,错误
//若通过额外一个循环dp数组,这样求解ans,这里就从0 或 1 开始都可以
//当然,直接令ans初值为dp[0]=1 ,然后这里也是从0 或 1 开始都可以,个人代码风格的区别
for(int i = 0; i<len;i++){
for(int j=0;j<i;j++){
//身高、体重都比j位的人大,在j位后“添加”本i位元素作为上升子序列
//这里就是上文所说的判断条件不会受影响,因为只有严格符合条件的,我们才会尝试更新dp[i]
if(person[i][0]>person[j][0]&&person[i][1]>person[j][1]){
dp[i] = Math.max(dp[i],1+dp[j]);
}
}
ans = Math.max(ans,dp[i]);
}
return ans;
}
小拓展
dp中可以有一个小优化:(但还是会超时)
- 用NlogN中所述的排序:身高递增,身高相同时,体重递减
- 判断条件只需要判断体重是否符合条件
原因:假设 i > j ,按此排序规则,有 身高 i >= j
- 大于> 时,直接判断体重,这个不解释,就是正常情况
- 等于=时,因为体重是递减排序,所以必定不存在体重 i > j 的情况,所以if不满足,dp不会更新
这样就省略了一个判断条件,因此笔者的建议是都按NlogN的排序方式,因为
- 可以用二分时,就不会出错
- 用dp时,如果记得就可以省去一些判断条件,就算不记得,使用全条件的性能也一样,不会变差
//本代码只有上述两点区别,其他代码完全一样
public int bestSeqAtIndex(int[] height, int[] weight) {
int len = height.length;
int[][] person = new int[len][2];
for (int i = 0; i < len; ++i)
person[i] = new int[]{height[i], weight[i]};
//排序规则,身高递增,身高相同时,体重递减
Arrays.sort(person, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
int[] dp = new int[len];
Arrays.fill(dp,1);
int ans = 0;
for(int i = 0; i<len;i++){
for(int j=0;j<i;j++){
//只需要判断体重,而无需判断身高
if(person[i][1]>person[j][1]){
dp[i] = Math.max(dp[i],1+dp[j]);
}
}
ans = Math.max(ans,dp[i]);
}
return ans;
}
同类型题目:
354. 俄罗斯套娃信封问题
上述的O(n2)dp算法可在此题验证,原理无异
建议完成↑题目,加深认识
拓展:堆箱子,也是一种类型的题目,不过这个是三维数组,就不好用NlogN;dp的小优化也可以用在该题
本文完,有误欢迎指出