0
点赞
收藏
分享

微信扫一扫

阿里题练——挑最多的物品(DP+二分法)

_刘彦辉 2022-03-11 阅读 26

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;
        
	}
	
}

举报

相关推荐

0 条评论