- 写于2019年6月27日
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
都是对一维数组进行升序排列,如何对二维数组进行排序?
- 如果直接使用
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)
- 代码中使用的方法如下:
Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]));
- 网上说的方法一:使用
Collections.reverseOrder()
。
Arrays.sort(a,Collections.reverseOrder());
- 网上说的方法二:创建比较器。
//实现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,有两种情况:
- 物品i将放入背包: F ( i − 1 , C − w i ) + v i F(i-1,C-w_i)+v_i F(i−1,C−wi)+vi。其中, F ( i − 1 , C − w i ) F(i-1,C-w_i) F(i−1,C−wi)表示将第i个物品放入背包以后,前 i − 1 i-1 i−1个物品最多使用了 C − w i C-w_i C−wi的情况下,所能获得的物品的最大价值。
- 物品i不放入背包: F ( i − 1 , C ) F(i-1,C) F(i−1,C)。其中, F ( i − 1 , C ) F(i-1,C) F(i−1,C)表示将第i个物品不放入背包,前 i − 1 i-1 i−1个物品最多使用了 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(i−1,C−wi)+vi和
F
(
i
−
1
,
C
)
F(i-1,C)
F(i−1,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(i−1,C−wi)+vi,F(i−1,C))
③ 递归求解
- 根据上面给出的状态转移方程,我们可以使用递归求解。而且是自顶向下的递归求解,因为求解 F ( i , C ) F(i,C) F(i,C)时需要求解 F ( i − 1 , C − w i ) F(i-1,C-w_i) F(i−1,C−wi)和 F ( i − 1 , C ) F(i-1,C) F(i−1,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];
}