题目描述:
导弹拦截https://www.luogu.com.cn/problem/P1020
题目的主要信息提取:
题目的描述很长,但是有用的信息我们可以提炼为:
第一问:求得一个数列的最长的单调不增子序列,这个子序列可以不连续.
第二问:求得一个数列的最长的单调递减子序列,这个子序列可以不连续.
思路分析:
这个题目的第一问中规中矩算是一个比较常规的线性动态规划题目,我们结合样例来理解这个题目
将题目给的数据画成柱状图我们可以更直观的感受思路,题目共有200分,我们设计一个O(n^2)的算法只能获得100分,如果想通过所有的测试用例需要一个O(nlogn)的算法,我们先设计O(n^2)的算法然后逐步优化
首先看第一问
如何获得一个最长的不增子序列呢,每个子序列都可以和它前面的任意一个最后一项大于等于它的子序列进行链接形成一个更长的子序列,例如图中子序列389,207,255,65就是由389,207,155这个序列和65这个序列链接而成的,所以我们可以从头到尾遍历,每次寻找当前的值前面大于等于它且以这个值为最后一位的最长的序列,然后与这个序列相连,形成一个新的更长的序列,每次比较新形成的序列和之前出现过的所有的序列,寻找一个最大值即可。这就是一个基本的思路。
我们设dp1[i]为以下标为i的数字为最后一位的序列的最大长度
状态转移方程就为:
dp[i] = dp[k]+1; 0<=k<i 且 仅当 k下标的数字大于等于i下标的数字时成立
我们初步就可以把代码写成这样:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String[] numStr = scan.nextLine().split(" ");
int n = numStr.length;
int[] nums = new int[n];
for(int i = 0;i<n;i++){
nums[i] = Integer.parseInt(numStr[i]);
}
int[] dp = new int[n];
dp[0] = 1;
int ans = 0;
for(int i = 1;i<n;i++){
dp[i] = 1;
for(int j = i-1;j>=0;j--){
if(nums[j]>=nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
ans = Math.max(ans,dp[i]);
}
System.out.println(ans);
}
}
但是这样的代码肯定是过不了的,时间复杂度太高,我们先不考虑如何优化先看第二问
第二问
第二问需要用到一个定理:狄尔沃斯定理_百度百科狄尔沃斯定理(Dilworth's theorem)亦称偏序集分解定理,是关于偏序集的极大极小的定理,该定理断言:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真,它断言:对于任意有限偏序集,其最长链中元素的数目必等于其最小反链划分中反链的数目,由偏序集P按如下方式产生的图G称为偏序集的可比图:G的节点集由P的元素组成,而e为G中的边,仅当e的两端点在P中是可比较的,有限全序集的可比图为完全图。https://baike.baidu.com/item/%E7%8B%84%E5%B0%94%E6%B2%83%E6%96%AF%E5%AE%9A%E7%90%86/18900593 这个定理在此题中的运用可以总结为,序列中最少的不增序列个数就等于最长的递增序列的长度。这里就不解释这个定理的来历了,有兴趣的就可以看一下,所以我们第二问起始就是将第一问的解法反向求出来,这里就不再赘述了
时间复杂度优化:
想要通过所有的测试用例,题目说的很清楚就是需要一个O(nlogn)的算法,如何将n优化为logn呢?其实我们应该立马去想到二分法,但是二分法如何在这里运用呢,我们保持遍历所有数不变,我们如何更快的在当前值的前面找到最佳的子序列?二分法需要序列有序才可以使用,我们之前的序列显然是无序的,我们就想办法构造一个有序的序列,前面在讲第一问的时候说过“从头到尾遍历,每次寻找当前的值前面大于等于它且以这个值为最后一位的最长的序列,然后与这个序列相连,形成一个新的更长的序列”这句话非常的关键。
我们看图
假如当前找已170为结尾的最大序列,我们可以看到长度为3的序列可以看到两个,分别是以299结尾和155结尾我们关注最大的结尾299,长度为2的序列的结尾的最大值一定大于等于长度为3的序列的结尾的数字,如果小于3的结尾的最大值,那么长度为三的序列的结尾的数字将会大于任何一个前面长度为2的序列的结尾的数字,就不可能组合出长度为3的序列,与条件矛盾,所以长度为i的序列的最后一位的最大值一定大于等于长度为i-1的序列的序列的最后一位的最大值。所以长度1-n的序列的最后一位数字的最大值的序列是一个单调不增序列。具有单调性。我们获得了一个新的单调的序列,但是如何通过这个序列找到下标为i的值可以连接的最大的序列呢?
我们继续思考,如果当前的值大于某个长度的最后一位的最大值,那么他就大于所有的这个长度的序列的末尾值,他就无法与其相连,所以我们可以在这个不增序列中找到最后一个大于等于它的长度,然后加一就可以的得到当前数字结尾的最长序列了。所以我们优化刚才的代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String[] numStr = scan.nextLine().split(" ");
int n = numStr.length;
int[] nums = new int[n];
for(int i = 0;i<n;i++){
nums[i] = Integer.parseInt(numStr[i]);
}
int[] dp = new int[n];
int[] drops = new int[n+1];
drops[1] = nums[0];
dp[0] = 1;
int ans = 0;
int temp = 1;
for(int i = 1;i<n;i++){
dp[i] = 1;
//二分
int left = 1,right = temp;
int mid;
int index = 0;
while(left<=right){
mid = left+((right-left)>>1);
if(nums[i]>drops[mid]){
right = mid-1;
}
else{
left = mid+1;index = mid;
}
}
dp[i] = index+1;
int now = dp[i];
drops[now] = nums[i];
ans = Math.max(ans,dp[i]);
temp = Math.max(temp,now);
}
System.out.println(ans);
}
}
第二问优化方式相同,我们将存储前面不同长度不增序列末尾数字最大值的数组改为存储前面出现过的不同长度的单调递增的序列的末尾数字的最小值,为什么时最小值呢?相同的道理,如果当前数字小于了长度为n的序列的末尾的最小值,那么他就小于前面的所有长度n的序列的末尾。他就不可能和前面的任意一个长度为n的序列连接。
最终的代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String[] numStr = scan.nextLine().split(" ");
int n = numStr.length;
int[] nums = new int[n];
for(int i = 0;i<n;i++){
nums[i] = Integer.parseInt(numStr[i]);
}
int[] dp = new int[n];
int[] drops = new int[n+1];
drops[1] = nums[0];
dp[0] = 1;
int ans = 0;
int temp = 1;
for(int i = 1;i<n;i++){
dp[i] = 1;
int left = 1,right = temp;
int mid;
int index = 0;
while(left<=right){
mid = left+((right-left)>>1);
if(nums[i]>drops[mid]){
right = mid-1;
}
else{
left = mid+1;index = mid;
}
}
dp[i] = index+1;
//二分
int now = dp[i];
drops[now] = nums[i];//可以直接赋值,因为后面如果出现新的长度相同的序列,那么它的末尾一定大于前面的末尾,如果小于等于前面的就会形成更长的序列
ans = Math.max(ans,dp[i]);
temp = Math.max(temp,now);
}
int[] incr = new int[n];
incr[0] = 1;
int max = 0;
int[] increase = new int[n+1];
increase[1] = nums[0];
int temp2 = 1;
for(int i = 1;i<n;i++){
incr[i] = 1;
int mid;int left = 1,right = temp2;
int index = 0;
while(left<=right){
mid = left+((right-left)>>1);
if(nums[i]>increase[mid]){
left = mid+1;
index = mid;
}else
right = mid-1;
}
incr[i] = index+1;
temp2 = Math.max(incr[i],temp2);
increase[incr[i]] = nums[i];
max = Math.max(incr[i],max );
}
System.out.println(ans);
System.out.println(max);
}
}