LeetCode Top100之406, 494,416题( Arrays.sort如何实现降序排列,0-1背包问题)

406. 根据身高重建队列
① 题目描述

中文题目:https://leetcode-cn.com/problems/queue-reconstruction-by-height/

② 根据身高重建队列-排序后插入
  • 发现一个规律,每次选择队列中身高最高的人插入新的队列,插入的位置就是他之前所站的人数。如果升高一样,优先选择前面人数较少的插入,这样可以保证对动态数组的插入不会越界。
  • 比如,动态数组最多2个元素,现在要在第四个位置插入会越界错误。
  • 充分利用List数组的add特性,可以指定插入的位置,并且如果当前位置有元素,则当前位置及以后的元素都会往后移动。
List.add(int index, E element)
  • 以输入样例为例子:
    ① 先排序:[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
    [7,0]
    [7,0], [7,1]
    [7,0], [6,1], [7,1] // 元素自动后移
    [5,0], [7,0], [6,1], [7,1] // 元素自动后移
    [5,0], [7,0], [5,2], [6,1], [7,1]// 元素自动后移
    [5,0], [7,0], [5,2], [6,1], [4,4], [7,1] // 元素自动后移
  • 代码如下,运行时间36ms
public int[][] reconstructQueue(int[][] people) {
    // 二维数组的排序,第一个元素不相等,则按照降序排列;第一个元素相等,第二个元素按照升序排列;
    Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]));
    List<int[]> list = new ArrayList<>();// 构造一维素组的动态数组
    for (int i = 0; i < people.length; i++) {
        list.add(people[i][1], people[i]);
    }
    // 将动态数组转化为指定类型的数组
    return list.toArray(new int[people.length][]);
} 
  • 之前使用Arrays.sort都是对一维数组进行升序排列,如何对二维数组进行排序?
  1. 如果直接使用Arrays.sort(a);,报错如下:
Exception in thread "main" java.lang.ClassCastException: [I cannot be cast to java.lang.Comparable
	at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
	at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
	at java.util.Arrays.sort(Arrays.java:1246)
	at Solution.sort(Solution.java:113)
	at Solution.main(Solution.java:10)
  1. 代码中使用的方法如下:
Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]));
  1. 网上说的方法一:使用Collections.reverseOrder()
Arrays.sort(a,Collections.reverseOrder());
  1. 网上说的方法二:创建比较器。
//实现Comparator接口
class MyComparator implements Comparator<Integer>{
   @Override
   public int compare(Integer o1, Integer o2) {
     /*如果o1小于o2,我们就返回正值,如果o1大于o2我们就返回负值,
      这样颠倒一下,就可以实现降序排序了,反之即可自定义升序排序了*/
    return o2-o1;    
}
//定义一个自定义类MyComparator的对象
Comparator cmp = new MyComparator();
Arrays.sort(a,cmp);
  • 如何实现将动态数组转为指定类型的数组?为list.toArray()指定模板类型。
list.toArray(new int[people.length][]);
494. 目标和
① 题目描述

中文题目:https://leetcode-cn.com/problems/target-sum/

② 递归
  • 从第1个数字开始,有两种情况:加上这个数字或者减去这个数字,这时会有一个当前所有数字的总和sum
  • 当我们已经遍历完所有数字时,如果sum等于目标和,计数变量count加一;否则,计数变量count不变。
  • 当我们已经遍历完所有数字时,还需要停止递归。
  • 代码如下,运行时间392ms
int count=0;
public int findTargetSumWays(int[] nums, int S) {
    dfs(nums,S,0,0);
    return count;
}
public void dfs(int[] nums, int S, int index, int sum) {
    if (index == nums.length) {
        if (S==sum){
            count++;
        }
        return;
    }
    // 加上nums[index]
    dfs(nums, S, index + 1, sum + nums[index]);
    // 减去nums[index]
    dfs(nums, S, index + 1, sum - nums[index]);
}
0-1背包问题
① 问题描述
  • 给定n个物品,他们的重量为 w 1 , w 2 , w 3 , . . . , w n w_1,w_2,w_3,...,w_n w1,w2,w3,...,wn,他们的价值为 v 1 , v 2 , v 3 , . . . , v n v_1,v_2,v_3,...,v_n v1,v2,v3,...,vn。假设你是一个将要携带这些金银细软离开的人,你只有一个容量为 C C C的背包。问题来了:你如何选择这些物品,使得背包中的物品价值最大。因为,毕竟你要靠着这些物品换钱呢!
  • 初学者有时会认为,0-1背包可以这样求解:计算每个物品的 v i / w i v_i/w_i vi/wi,然后依据 v i / w i v_i/w_i vi/wi的值,对所有的物品从大到小进行排序。其实,贪心方法是错误的。如下表,有三件物品,背包的最大负重量是50,求可以取得的最大价值。
    在这里插入图片描述
② 问题分析
  • 所谓的0-1背包问题是指:这些物品都是独一无二的,你拿了物品i,就不能在拿物品i了。所以对于给定背包容量的情况下,为了实现总价值最大,物品i要么被装进背包,要么不装进背包。
  • F ( n , C ) F(n,C) F(n,C)表示表示将前 n n n个物品放进容量为 C C C的背包里,得到的最大的价值。则对于物品i,有两种情况:
  1. 物品i将放入背包: F ( i − 1 , C − w i ) + v i F(i-1,C-w_i)+v_i F(i1,Cwi)+vi。其中, F ( i − 1 , C − w i ) F(i-1,C-w_i) F(i1,Cwi)表示将第i个物品放入背包以后,前 i − 1 i-1 i1个物品最多使用了 C − w i C-w_i Cwi的情况下,所能获得的物品的最大价值。
  2. 物品i不放入背包: F ( i − 1 , C ) F(i-1,C) F(i1,C)。其中, F ( i − 1 , C ) F(i-1,C) F(i1,C)表示将第i个物品不放入背包,前 i − 1 i-1 i1个物品最多使用了 C C C的情况下,所能获得的物品的最大价值。
  • 于是,我们所要求解的 F ( i , C ) F(i,C) F(i,C)肯定是 F ( i − 1 , C − w i ) + v i F(i-1,C-w_i)+v_i F(i1,Cwi)+vi F ( i − 1 , C ) F(i-1,C) F(i1,C)的中较大值:
    F ( i , C ) = m a x ( F ( i − 1 , C − w i ) + v i , F ( i − 1 , C ) ) F(i,C)=max(F(i-1,C-w_i)+v_i,F(i-1,C)) F(i,C)=max(F(i1,Cwi)+vi,F(i1,C))
③ 递归求解
  • 根据上面给出的状态转移方程,我们可以使用递归求解。而且是自顶向下的递归求解,因为求解 F ( i , C ) F(i,C) F(i,C)时需要求解 F ( i − 1 , C − w i ) F(i-1,C-w_i) F(i1,Cwi) F ( i − 1 , C ) F(i-1,C) F(i1,C)
  • 递归求解的代码如下:
public static void main(String[] args) {
    int w[] = {2, 2, 6, 5, 4};
    int v[] = {6, 3, 5, 4, 6};
    System.out.println("最大价值:" + knapsack(v, w, v.length - 1, 10));
}
public static int knapsack(int[] v, int[] w, int index, int capacity) {
    // 没有物品可放或者背包没有容量了,此次的所能获得价值为0
    if (index < 0 || capacity <= 0) {
        return 0;
    }
    // 不放第i个物品进背包
    int result = knapsack(v, w, index - 1, capacity);
    if (w[index] <= capacity) {// 不能放第i个进背包,直接返回prev
        result = Math.max(result, knapsack(v, w, index - 1, capacity - w[index]) + v[index]);
    }
    return result;
}
  • 最后的结果:15
④ 记忆化搜索
  • 我们用递归方法可以很简单的实现以上代码,但是有个严重的问题就是,直接采用自顶向下的递归算法会导致要不止一次的解决公共子问题,因此效率是相当低下的。
  • 解决办法: 我们可以将已经求得的子问题的结果保存下来,这样对子问题只会求解一次,这便是记忆化搜索
// 容量为0时,所能获得价值为0
int[][] mem = new int[v.length][capacity + 1];
public int knapsack(int[] v, int[] w, int index, int capacity) {
    // 没有物品可放或者背包没有容量了,此次的所能获得价值为0
    if (index < 0 || capacity <= 0) {
        return 0;
    }
    // 如果已经求解过,直接返回值
    if (mem[index][capacity]!=0){
        return mem[index][capacity];
    }
    // 不放第i个物品进背包
    int result = knapsack(v, w, index - 1, capacity);
    if (w[index] <= capacity) {// 不能放第i个进背包,直接返回prev
        result = Math.max(result, knapsack(v, w, index - 1, capacity - w[index]) + v[index]);
    }
    // 记录下当前问题的解
    mem[index][capacity]=result;
    return result;
}
⑤ 动态规划
  • 都分析出了状态转移方程,不使用动态规划,感觉很吃亏!
  • 物品编号从0开始,因此dp = new int[v.length][capacity + 1]
  • 当容量为0时,一个物品也不能放,于是dp[i][0]=0
  • 放第0件物品时,他是最先尝试放入的,单独初始化。
public static int knapsack(int[] v, int[] w,int capacity) {
    // 物品编号0,1,2,...,n,容量为0,1,2,3,...,c
    int[][] dp = new int[v.length][capacity + 1];
    // 容量为0,对应第0列,所获的价值为0,不用初始化
    // 只放第0件物品
    for (int i = 1; i <= capacity; i++) {
        if (w[0] <= i) {// 能放下,更新总价值
            dp[0][i] = v[0];
        }
    }
    // 放后面的剩余物品
    for (int i = 1; i < v.length; i++) {
        for (int j = 1; j <= capacity; j++) {
            // 容量为j时,放入第i件物品,之前的i-1件物品已经尝试放入过
            // 不放入第i件物品
            dp[i][j] = dp[i - 1][j];
            if (w[i] <= j) {// 可以放入第i件物品
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - w[i]] + v[i]);
            }
        }
    }
    return dp[v.length-1][capacity];
}
  • 动态方程的结果:
0	0	6	6	6	6	6	6	6	6	6	
0	0	6	6	9	9	9	9	9	9	9	
0	0	6	6	9	9	9	9	11	11	14	
0	0	6	6	9	9	9	10	11	13	14	
0	0	6	6	9	9	12	12	15	15	15	

参考链接:彻底理解0-1背包问题

416. 分割等和子集
① 题目描述

中文题目:https://leetcode-cn.com/problems/partition-equal-subset-sum/

② 受494题的启发,递归(Time Limit Exceeded
  • 如果能分成两个等和的子集,那么整个数组的和一定是偶数。这样可以减少对所有情况都进行判断。
  • 两个等和子集,其实就是如何为数组中的数字添加正负号,使其的和为0。
  • 于是使用494题的方法,结果Time Limit Exceeded
public boolean canPartition(int[] nums) {
    int sum = 0;
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
    }
    if (sum % 2 != 0) {
        return false;
    }
    return helper(nums,0,0,0);
}
public boolean helper(int[] nums, int target, int index, int sum) {
    if (index == nums.length) {
        if (target == sum) {
            return true;
        }
        return false;
    }
    return (helper(nums, target, index + 1, sum + nums[index])
            || helper(nums, target, index + 1, sum - nums[index]));
}
③ 动态规划—— 0-1背包问题
  • 整个数组能划分为等和的两部分,其实就是给定背包容量的情况下,如何选择数组中的元素构成子数组,使得他们的总和为背包容量。
  • 注意: 这里的背包容量就是物品的价值。
  • dp[i][j]表示数据中前i个数字是否可以存在和为j的子数组,如果存在则dp[i][j]=true,否则dp[i][j]=false
  • 其中,当子数组和为0时,不需要选择任何元素进行构造就可以满足条件,于是dp[i][0]=true
  • 对于只有第0个元素时,由于子数组和不为0,所以必须选择元素构成子数组。所以就只能选择它本身,这时要想构成满足和为j的子数组,必须nums[0] == j
  • 对于其他元素,如果不选择它作为子数组中的成员,则dp[i][j] = dp[i-1][j],即这时是否满足条件,取决于前i-1个元素是否能够成满足条件的子数组;如果选择它作为子数组中的成员,则dp[i][j] = dp[i-1][j-nums[i-1]],即这时是否满足条件,取决于前i-1个元素是否能够构成剩余和j-nums[i]的子数组。
  • 因此得到转移方程可以表示为dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
  • 代码如下,运行时间17ms:
public boolean canPartition(int[] nums) {
    int sum = 0;
    int len = nums.length;
    for (int i = 0; i < len; i++) {
        sum += nums[i];
    }
    if (sum % 2 != 0) {
        return false;
    }
    int target = sum / 2;

    //dp[i][j]表示数据中前i个数字是否可以满足存在和为j的子数组
    boolean[][] dp = new boolean[len][target + 1];
    // 总和为0,不需要选择任何元素,就能满足要求
    for (int i = 0; i < len; i++) {
        dp[i][0] = true;
    }
    // 总和不为0,肯定要选择子数组,只有第0个数字时
    // 子数组只能为他本身
    for (int i = 1; i <= sum / 2; i++) {
        if (nums[0] == i) {//
            dp[0][i] = true;
        }
    }
    for (int i = 1; i < len; i++) {
        for (int j = 1; j <= target; j++) {
            // 不选第一个数构成子数组
            dp[i][j] = dp[i - 1][j];
            // 选第i个数构成子数组,之前的i-1个元素只需要构成和为j-nums[i]的子数组
            if (nums[i] <= j) {
                dp[i][j]=dp[i][j]||dp[i-1][j-nums[i]];
            }
        }
    }
    return dp[len-1][target];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值