1、原题描述
2、算法分析
根据题目和实例的分析,我们可以发现,假如我们先选了物品1,要想加入下一件物品,加入的该件物品必须保证x,y都大于物品1的,再选下一件物品判断是,也要保证这种x,y的严格大。
换句话说,我们每选择一件物品,都要保证该物品加入后物品之间x,y是严格递增的,其实也就等效于 求解给定物品中的最长递增子序列的长度。
因为这里需要对x,y两个变量的判断,所以我们可以控制变量,首先 按x对节点进行升序排序,然后从排序好的节点中挑选出y严格递增的最长序列长度 也就是最终能够选择的最多物品数量。
需要注意的点是,假如节点中有多个节点的x相等,需要对节点x相等的节点按y降序排序,分析如下:
如果我们此时有4组节点 :
x: 1 2 2 4
y: 4 3 5 7
我们发现 对x排序后,因为x有重复,此时我们直接来选择y中的递增序列长度为3,作为最终结果则错误,因为此时y中对应的x相等了 不满足x要递增的要求,其实际满足x,y递增的子序列长度为2.
但是,若我们在对x排序的过程中,考虑到x相等时,又要根据y的降序来排序,结果如下
x: 1 2 2 4
y:4 5 3 7
那么此时我们根据y的最长递增子序列长度作为最终的结果就没有问题 通过y的降序来去除了我们可能引入x重复的问题。
其他简洁的原理分析:
3、代码实现
根据上文的分析 本质就是其最长递增子序列的实际场景应用,我们可以采用DP和二分贪心来完成。
3.1、DP实现
求解最长递增子序列长度时,当前下标的最长子序列长度 其实与前面的下标长度息息相关,可以递推求解。
动态规划五部曲:
1)dp定义
dp[i]表示i之前包括i的最长上升子序列的长度。
2)递推公式
dp[i]表示下标i位置的最长上升子序列,等于j从0~i-1各个位置的最长子序列+1得到的结果中的最大值;
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
(注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。)
3)dp初始化
每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1.
4)确定遍历顺序
dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是0到i-1,遍历i的循环在外层,遍历j则在内层,代码如下:
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
5) 最后从dp数组中跳出最大的即可
上面DP过程讲解的是如何用dp求解最长递增子序列,结合本题,我们需要先对节点x排序,x相同时,对x相同的进行y降序;最后才是用dp求解y中的最长递增子序列长度 作为最终的结果哟。
import java.io.*;
import java.util.*;
class test
{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
int num = Integer.parseInt(s);//总的有多少组
for(int i=0;i<num;i++){
int count=0;
int n = sc.nextInt();
//t[i][0]表示第i个物品的x属性,t[i][1]表示第i个物品的y属性
int[][] t = new int[n][2];
int[] arr = new int[n];
for (int j = 0; j < n; j++)
{
t[j][0] = sc.nextInt();
}
for (int j = 0; j < n; j++)
{
t[j][1] = sc.nextInt();
}
// 先对x 进行升序 x相同的情况下y更大的排序在前面(不然的话会重复统计相同的x)
Arrays.sort(t,new Comparator<int[]>(){
public int compare(int[] o1,int[] o2){
//x升序 遇到x相等 y降序
if(o1[0] >o2[0]){
return 1;
}else if(o1[0]<o2[0]){
return -1;
}else{
//出现x相等 此时y按顺序降序
if(o1[1]>o2[1]){
return -1;
}else if(o1[1]<o2[1]){
return 1;
}else{
return -1;
}
}
}
});
//此时x已经按升序排列,当x相等时,y对应降序
//要找到严格同大或同小,只需要找出此时y的最长递增子序列长度
for(int j=0;j<n;j++){
arr[j]=t[j][1];
}
int result= longSubArray(arr);
System.out.println(result);
}
}
//二分法 求最长上升子序列
// dp[i] 表示下标0~i长度数组中的最长递增子序列的长度
/*
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
*/
public static int longSubArray(int[] nums){
int[] dp = new int[nums.length];
dp[0]=1;
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
}
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
--------------------------------------
3.2 二分法(最长递增子序列)
二分法的大体思路:
那么结合本题,我们依然采用上面的排序方式先处理节点, 然后查找y的最长上升子序列长度的时候采用二分法。
代码如下:
import java.io.*;
import java.util.*;
class test
{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
int num = Integer.parseInt(s);//总的有多少组
for(int i=0;i<num;i++){
//每一组的节点数n
int n =sc.nextInt();
//构建存储n个节点x,y 属性的nx2矩阵数
//t[i][0]表示第i个物品的x属性,t[i][1]表示第i个物品的y属性
int[][] t =new int[n][2];
//当对t进行x升序,遇到x相等时,相等的节点再根据y排序处理后,单独提出y来查找最长递增子序列长度
int[] arr = new int[n];
//控制台输入
for(int j=0;j<n;j++){
t[j][0]=sc.nextInt();
}
for(int j=0;j<n;j++){
t[j][1]=sc.nextInt();
}
// 先对x 进行升序 x相同的情况下y更大的排序在前面(不然的话计算y递增子序列长度会重复统计相同的x)
Arrays.sort(t,new Comparator<int[]>(){
public int compare(int[] o1,int[] o2){
if(o1[0]>o2[0]){
return 1;
}else if(o1[0]<o2[0]){
return -1;
}else{
//相等 加入第二排序规则
if(o1[1]>o2[1]){
return -1;
}else if(o1[1]<o2[1]){
return 1;
}else{
return -1;
}
}
}
});
//此时x已经按升序排列,当x相等时,y对应降序
//要找到严格同大或同小,只需要找出此时y的最长递增子序列长度
//这里采用二分法来找y的严格递增子序列长度
//先把y其取出来
for(int j=0;j<n;j++){
arr[j]=t[j][1];
}
int result = longSubArray(arr);
System.out.println(result);
}
}
//二分找最长递增子序列长度
//二分+贪心
public static int longSubArray(int[] arr){
int len = arr.length;
//新建一个tail数组来存放遍历原数组中当前下标i下的递增子序列元素
tail[i]: 表示长度为i+1的递增子序列的最小尾值
int [] tail =new int[len];
//初始化
tail[0]=arr[0];//表示遍历原数组第一个元素,此时对应的最长递增子序列其实就是这个值
//tail 尾部下标
int endIdx=0;
for(int i=1;i<len;i++){
// nums[i] > tail[endIndex] 的情况:加入尾,更新 endIndex
if(arr[i]>tail[endIdx]){
tail[++endIdx]=arr[i];
}else{
//nums[i]<=tail[endIdx] 那么此时采用二分查找 从tail中找到第一个大于nums[i]的元素并更新替换,保证tail的递增
int left=0,right=endIdx;
while(left<right){
// mid
int mid=left+(right-left)/2;
// m目的是找到tail中第一个大于num[i],当tail[mid]<num[i].,则左半部分舍弃 去右半部分找
if(tail[mid]<arr[i]){
left=mid+1;
}else{
right=mid;
}
}
//的元素并更新替
tail[left]=arr[i];
}
}
//构造结束 那此时的tail便对应原始数组的最长递增子序列
int ans =endIdx+1;
return ans;
}
}