494. 目标和
2021.6.7 每日一题
题目描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
动规,感觉这个月是动规月
dp[i][j]定义为到下标i目标和为j的方法数
但是这个题因为有负数,所以要将j的范围扩充一倍
设所有元素的总和为sum,因为所以提示nums[i]是大于0的,那么最大值就是sum,最小值就是-sum
将j的范围扩充一倍,使得0 ~ sum - 1表示 -sum ~ -1,sum+1 ~ 2sum表示正数
初始化的时候,第一个元素是0特殊处理
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//我能想到的动规,就是dp[i][j]到下标i目标和为j的方法数
int l = nums.length;
int sum = 0;
for(int i = 0; i < l; i++){
sum += nums[i];
}
if(target > sum || target < -sum)
return 0;
//因为有负数0-sum - 1表示-sum ~ -1,sum+1-2sum表示正数
int[][] dp = new int[l][2 * sum + 1];
//如果开始是0特殊处理
if(nums[0] == 0)
dp[0][sum] = 2;
else{
dp[0][sum - nums[0]] = 1;
dp[0][sum + nums[0]] = 1;
}
for(int i = 1; i < l; i++){
for(int j = 0; j <= 2 * sum; j++){
int a = j + nums[i];
int b = j - nums[i];
if(a >= 0 && a <= 2 * sum){
dp[i][j] += dp[i - 1][a];
}
if(b >= 0 && b <= 2 * sum){
dp[i][j] += dp[i - 1][b];
}
}
}
return dp[l - 1][sum + target];
}
}
官解的数学推导:
数组元素总和为sum,在前面添加负号的元素总和为neg,其余添加+的元素总和为sum - neg
因此目标为sum- neg - neg = target
所以neg = (sum - target) / 2; (从这里可知(sum - target)是非负偶数)
然后现在的目标就是选择 x 个数,使他们的和为neg
定义dp[i][j]为表示在数组nums 的前 i 个数中选取元素,使得这些元素之和等于 j 的方案数
初始化,dp[0][0] = 1;
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.length, neg = diff / 2;
int[][] dp = new int[n + 1][neg + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/target-sum/solution/mu-biao-he-by-leetcode-solution-o0cp/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
474. 一和零
2021.6.6 每日一题,补2021.6.5每日一题
题目描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ones-and-zeroes
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
看到第一眼想到的是回溯,没救了,写了一下,过了22个例子就超时了,代码没问题应该
class Solution {
int max = 0;
int m;
int n;
public int findMaxForm(String[] strs, int m, int n) {
//第一眼想到的是回溯遍历所有的情况,但是能回溯吧,应该也能动规,
//
this.m = m;
this.n = n;
int l = strs.length;
int[] count = new int[2];
backtracking(strs, 0, count, 0);
return max;
}
public void backtracking(String[] strs, int res, int[] count, int index){
if(index > strs.length)
return;
if(count[0] > m || count[1] > n)
return;
max = Math.max(max, res);
for(int i = index; i < strs.length; i++){
int count0 = 0;
for(int j = 0; j < strs[i].length(); j++){
count0 += strs[i].charAt(j) == '0' ? 1 : 0;
}
int count1 = strs[i].length() - count0;
count[0] += count0;
count[1] += count1;
backtracking(strs, res + 1, count, i + 1);
count[0] -= count0;
count[1] -= count1;
}
}
}
然后想了半天终于想出来怎么规划了,和背包问题挺像的,最近不管做什么题都老是看到动态规划,而且都还不容易想,看来又得好好看看了
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//动规动规,咋就这么难想呢
//背包问题是每个物品都有一个价值和重量,然后当前重量下能装物品价值的最大值
//dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weigth[i]] + value[i]);
//这个问题是每个字符串都有0和1两个的数量,然后求当前要求下所装物品的最大数量
int l = strs.length;
int[][][] dp = new int[l + 1][m + 1][n + 1];
for(int i = 1; i <= l; i++){
int count0 = 0;
for(int j = 0; j < strs[i - 1].length(); j++){
count0 += strs[i - 1].charAt(j) == '0' ? 1 : 0;
}
int count1 = strs[i - 1].length() - count0;
for(int j = 0; j <= m; j++){
for(int k = 0; k <= n; k++){
//不选当前字符串
dp[i][j][k] = dp[i - 1][j][k];
//选当前字符串
if(j >= count0 && k >= count1){
dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - count0][k - count1] + 1);
}
}
}
}
return dp[l][m][n];
}
}
状态压缩,注意内层循环反向遍历
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//一看好像就是动态规划,但是好像不知道怎么确定目标
//倒是可以用暴力解法,就是把所有的可能都列举出来,然后看最大符合题意的数目是多少个
//看了一下题解,发现可以理解为有两个维度的目标 的背包问题
//dp[i][j]最多有i个0和j个1的的最大子集大小,这相当于一个滚动数组
//递推公式 dp[i][j] = max(dp[i - 当前0的个数][j- 当前1的个数] + 1,dp[i][j])
int[][] dp = new int[m + 1][n + 1];
//初始化:mn都为0时,放不进去东西,所以为0
dp[0][0] = 0;
//遍历
for(String str : strs){
int num0 = 0;
int num1 = 0;
for(int i = 0; i < str.length(); i++){
char c = str.charAt(i);
if(c == '0')
num0++;
else
num1++;
}
//内层循环反向遍历,i<num0 和 j<num1就不用遍历了,因为他俩都不变
for(int i = m; i >= num0; i--){
for(int j = n; j >= num1; j--){
dp[i][j] = Math.max(dp[i][j], dp[i - num0][j - num1] + 1);
}
}
}
return dp[m][n];
}
}
203. 移除链表元素
题目描述
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-linked-list-elements
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
迭代
class Solution {
public ListNode removeElements(ListNode head, int val) {
//递归写一下
if(head == null)
return null;
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while(cur != null){
if(cur.val == val){
cur = cur.next;
pre.next = cur;
}else{
pre = cur;
cur = cur.next;
}
}
return dummy.next;
}
}
递归
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
head.next = removeElements(head.next, val);
return head.val == val ? head.next : head;
}
}
第 244 场力扣周赛
这场周赛感觉自己状态很不好,可能跟昨天乱七八糟玩了一天有关…
5776. 判断矩阵经轮转后是否一致
题目描述
给你两个大小为 n x n 的二进制矩阵 mat 和 target 。现 以 90 度顺时针轮转 矩阵 mat 中的元素 若干次 ,如果能够使 mat 与 target 一致,返回 true ;否则,返回 false 。
示例 1:
输入:mat = [[0,1],[1,0]], target = [[1,0],[0,1]]
输出:true
解释:顺时针轮转 90 度一次可以使 mat 和 target 一致。
示例 2:
输入:mat = [[0,1],[1,1]], target = [[1,0],[0,1]]
输出:false
解释:无法通过轮转矩阵中的元素使 equal 与 target 一致。
示例 3:
输入:mat = [[0,0,0],[0,1,0],[1,1,1]], target = [[1,1,1],[0,1,0],[0,0,0]]
输出:true
解释:顺时针轮转 90 度两次可以使 mat 和 target 一致。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/determine-whether-matrix-can-be-obtained-by-rotation
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
今天这个题直接给我干懵了,我感觉之前也做过旋转矩阵的题,但是我觉得还是挺麻烦的,我感觉一个简单题,应该不会那么复杂,然后我就迷茫了。
最后在做完第二道题,看了第三道题想不出来以后,再回来做第一道题,然后用翻转矩阵的方法过了,虽然很繁琐,但是还是学到了点东西(注意还有不旋转的情况):
顺时针旋转90度,相当于先按主对角线翻转,然后左右翻转
顺时针旋转180度,相当于先上下翻转,然后左右翻转
顺时针旋转270度,相当于先按主对角线翻转,然后上下翻转
class Solution {
int m;
int n;
public boolean findRotation(int[][] mat, int[][] target) {
//模拟吧,最多旋转三次
m = mat.length;
n = mat[0].length;
int[][] temp = rotate1(mat);
if(equal(mat, target))
return true;
if(equal(target, temp))
return true;
temp = rotate2(mat);
if(equal(target, temp))
return true;
temp = rotate3(mat);
if(equal(target, temp))
return true;
return false;
}
public int[][] rotate1(int[][] mat){
int[][] temp = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp[j][i] = mat[i][j];
}
}
int[][] temp2 = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp2[i][n - 1 - j] = temp[i][j];
}
}
return temp2;
}
public int[][] rotate2(int[][] mat){
int[][] temp = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp[n - 1- i][j] = mat[i][j];
}
}
int[][] temp2 = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp2[i][n - 1- j] = temp[i][j];
}
}
return temp2;
}
public int[][] rotate3(int[][] mat){
int[][] temp = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp[j][i] = mat[i][j];
}
}
int[][] temp2 = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
temp2[n - 1 - i][j] = temp[i][j];
}
}
/**
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
System.out.print(temp2[i][j] + " ");
}
System.out.println();
}
*/
return temp2;
}
public boolean equal(int[][] mat, int[][] temp){
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(temp[i][j] != mat[i][j])
return false;
}
}
return true;
}
}
结束以后,第一时间就看大神怎么写的这道题,然后又想了想自己到底是怎么想的
先看旋转90度和旋转之前坐标的关系,tmep[j][n - i - 1] = mat[i][j];
相当于每次旋转90以后判断,用一个临时数组存储就很简单了
然后我就写了下面的代码
class Solution {
public boolean findRotation(int[][] mat, int[][] target) {
int n = mat.length;
int[][] temp = new int[n][n];
if(equal(mat, target))
return true;
for(int k = 0; k < 3; k++){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
temp[j][n - i - 1] = mat[i][j];
}
}
if(equal(temp, target))
return true;
mat = temp;
}
return false;
}
public boolean equal(int[][] mat, int[][] temp){
int n = mat.length;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(temp[i][j] != mat[i][j])
return false;
}
}
return true;
}
}
然后还是没过,我觉得啥都没问题,然后第二轮的时候,发现mat在旋转前和旋转中的值发生了变化,然后又好好想了一下,发现是mat=temp的问题,传递的是地址,temp变化以后,立刻mat就变化了,可以改成以下这种赋值的方法,或者是直接把temp的定义写在循环里面,让每次temp都不同
class Solution {
public boolean findRotation(int[][] mat, int[][] target) {
int n = mat.length;
int[][] temp = new int[n][n];
if(equal(mat, target))
return true;
for(int k = 0; k < 3; k++){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
temp[j][n - i - 1] = mat[i][j];
}
}
if(equal(temp, target))
return true;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
mat[i][j] = temp[i][j];
}
}
}
return false;
}
public boolean equal(int[][] mat, int[][] temp){
int n = mat.length;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(temp[i][j] != mat[i][j])
return false;
}
}
return true;
}
}
这道题咋说呢,也不能算很简单吧,但是也挺简单的。但是问题还是不少,学到的东西也很多
之前做过旋转90度的题,而且还是原地旋转,那个题都是几个月前自己写的了,但是今天这个题竟然有问题,好好反思一下!
5777. 使数组元素相等的减少操作次数
题目描述
给你一个整数数组 nums ,你的目标是令 nums 中的所有元素相等。完成一次减少操作需要遵照下面的几个步骤:
找出 nums 中的 最大 值。记这个值为 largest 并取其下标 i (下标从 0 开始计数)。如果有多个元素都是最大值,则取最小的 i 。
找出 nums 中的 下一个最大 值,这个值 严格小于 largest ,记为 nextLargest 。
将 nums[i] 减少到 nextLargest 。
返回使 nums 中的所有元素相等的操作次数。
示例 1:
输入:nums = [5,1,3]
输出:3
解释:需要 3 次操作使 nums 中的所有元素相等:
1. largest = 5 下标为 0 。nextLargest = 3 。将 nums[0] 减少到 3 。nums = [3,1,3] 。
2. largest = 3 下标为 0 。nextLargest = 1 。将 nums[0] 减少到 1 。nums = [1,1,3] 。
3. largest = 3 下标为 2 。nextLargest = 1 。将 nums[2] 减少到 1 。nums = [1,1,1] 。
示例 2:
输入:nums = [1,1,1]
输出:0
解释:nums 中的所有元素已经是相等的。
示例 3:
输入:nums = [1,1,2,2,3]
输出:4
解释:需要 4 次操作使 nums 中的所有元素相等:
1. largest = 3 下标为 4 。nextLargest = 2 。将 nums[4] 减少到 2 。nums = [1,1,2,2,2] 。
2. largest = 2 下标为 2 。nextLargest = 1 。将 nums[2] 减少到 1 。nums = [1,1,1,2,2] 。
3. largest = 2 下标为 3 。nextLargest = 1 。将 nums[3] 减少到 1 。nums = [1,1,1,1,2] 。
4. largest = 2 下标为 4 。nextLargest = 1 。将 nums[4] 减少到 1 。nums = [1,1,1,1,1] 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reduction-operations-to-make-the-array-elements-equal
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
排序后,从后面往前面统计个数就行了
class Solution {
public int reductionOperations(int[] nums) {
int l = nums.length;
int[] count = new int[50001];
int temp = 0;
int res = 0;
Arrays.sort(nums);
for(int i = l - 1; i >= 1; i--){
if(nums[i] == nums[i - 1])
temp++;
else{
temp++;
res += temp;
}
}
return res;
}
}
5778. 使二进制字符串字符交替的最少反转次数
题目描述
给你一个二进制字符串 s 。你可以按任意顺序执行以下两种操作任意次:
类型 1 :删除 字符串 s 的第一个字符并将它 添加 到字符串结尾。
类型 2 :选择 字符串 s 中任意一个字符并将该字符 反转 ,也就是如果值为 '0' ,则反转得到 '1' ,反之亦然。
请你返回使 s 变成 交替 字符串的前提下, 类型 2 的 最少 操作次数 。
我们称一个字符串是 交替 的,需要满足任意相邻字符都不同。
比方说,字符串 "010" 和 "1010" 都是交替的,但是字符串 "0100" 不是。
示例 1:
输入:s = "111000"
输出:2
解释:执行第一种操作两次,得到 s = "100011" 。
然后对第三个和第六个字符执行第二种操作,得到 s = "101010" 。
示例 2:
输入:s = "010"
输出:0
解释:字符串已经是交替的。
示例 3:
输入:s = "1110"
输出:1
解释:对第二个字符执行第二种操作,得到 s = "1010" 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-number-of-flips-to-make-the-binary-string-alternating
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
第一种方法前后缀和,看注释吧
class Solution {
public int minFlips(String s) {
//说实话,给我想一个小时肯定都想不出来
//先看了前后缀的方法
//思路是怎么形成的,首先看到类型1的操作可以随便用,那么就可以枚举所有前缀,将它们移动到最后
//然后看形成的新的字符串中分别以0和1开头需要翻转的次数
//但是会超时
//所以预先处理前后缀,prei0表示长度为i的前缀,并且以0开头的交替字符串操作次数
//prei1表示长度为i的前缀,并且以1开头的交替字符串操作次数
//同理,后缀也处理两个sufi0,sufi1
//最后,结果就是prei0+sufi1,prei1+sufi0中的最小值
int l = s.length();
int[][] pre = new int[l][2];
int[][] suf = new int[l][2];
pre[0][0] = s.charAt(0) == '0' ? 0 : 1; //以0开头
pre[0][1] = s.charAt(0) == '1' ? 0 : 1; //以1开头
for(int i = 1; i < l; i++){
char c = s.charAt(i);
//如果当前是偶数,那么0开头当前位置应该是0,1开头当前位置应该是1
if(i % 2 == 0){
pre[i][0] = pre[i - 1][0] + (c == '0' ? 0 : 1);
pre[i][1] = pre[i - 1][1] + (c == '0' ? 1 : 0);
}else{
//如果当前是奇数,那么0开头当前位置应该是1,1开头当前位置应该是0
pre[i][0] = pre[i - 1][0] + (c == '0' ? 1 : 0);
pre[i][1] = pre[i - 1][1] + (c == '0' ? 0 : 1);
}
}
suf[l - 1][0] = s.charAt(l - 1) == '0' ? 0 : 1;
suf[l - 1][1] = s.charAt(l - 1) == '1' ? 0 : 1;
for(int i = l - 2; i >= 0; i--){
char c = s.charAt(i);
//如果当前是偶数,那么0开头当前位置应该是1,1开头当前位置应该是0
if((l - i) % 2 == 0){
suf[i][0] = suf[i + 1][0] + (c == '0' ? 1 : 0);
suf[i][1] = suf[i + 1][1] + (c == '0' ? 0 : 1);
}else{
//如果当前是奇数,那么0开头当前位置应该是0,1开头当前位置应该是1
suf[i][0] = suf[i + 1][0] + (c == '0' ? 0 : 1);
suf[i][1] = suf[i + 1][1] + (c == '0' ? 1 : 0);
}
}
//处理完以后,计算最小值
int res = Math.min(Math.min(pre[l - 1][0], pre[l - 1][1]), Math.min(suf[0][0], suf[0][1]));
for(int i = 0; i < l - 1; i++){
res = Math.min(res, Math.min(pre[i][0] + suf[i + 1][1], pre[i][1] + suf[i + 1][0]));
}
return res;
}
}
第二种方法双倍字符串+滑动窗口
class Solution {
public int minFlips(String s) {
//第二种方法,就是采用滑动窗口
//思路来源还是第一种操作,所以可以把整个字符串拼接在字符串后面,然后使用滑动窗口从前向后处理
//另外,假设以0开头需要翻转的次数是t,字符串长度为l,那么以1开头需要翻转的次数为l - t
int l = s.length();
int count = 0;
//假设以0开头
for(int i = 0; i < l; i++){
char c = s.charAt(i);
//如果当前下标为偶数,那么当前字符是0则不用翻转,是1则需要翻转
if(i % 2 == 0)
count += c == '0' ? 0 : 1;
else
count += c == '0' ? 1 : 0;
}
//滑动窗口,每次去掉的元素和新加的元素是相同的
int res = Math.min(count, l - count);
for(int i = 0; i < l; i++){
char c = s.charAt(i);
//减去当前元素的处理结果
count -= i % 2 == 0 ? (c == '0' ? 0 : 1) : (c == '0' ? 1 : 0);
//加上拼接到后面的处理结果
count += ((i + l) % 2 == 0) ? (c == '0' ? 0 : 1) : (c == '0' ? 1 : 0);
res = Math.min(res, Math.min(count, l - count));
}
return res;
}
}
总结一下这个题是怎么想出来的,关键是对类型一操作的处理,因为类型一的操作可以用无限次,所以就可以想到暴力解法,把所有可能的字符串构造出来,然后再计算分别以0开头和1开头的交替字符串的处理次数
然后呢,所有可能的字符串可以看成前后缀的拼接 或者是 两个字符串拼接后的n个滑动窗口,这样就可以使本来O(n2)的复杂度降为O(n)的复杂度
还有可以使问题简单化的一个技巧就是,0开头和1开头的结果是有关系的!
5779. 装包裹的最小浪费空间
题目描述
给你 n 个包裹,你需要把它们装在箱子里,每个箱子装一个包裹。总共有 m 个供应商提供 不同尺寸 的箱子(每个规格都有无数个箱子)。如果一个包裹的尺寸 小于等于 一个箱子的尺寸,那么这个包裹就可以放入这个箱子之中。
包裹的尺寸用一个整数数组 packages 表示,其中 packages[i] 是第 i 个包裹的尺寸。供应商用二维数组 boxes 表示,其中 boxes[j] 是第 j 个供应商提供的所有箱子尺寸的数组。
你想要选择 一个供应商 并只使用该供应商提供的箱子,使得 总浪费空间最小 。对于每个装了包裹的箱子,我们定义 浪费的 空间等于 箱子的尺寸 - 包裹的尺寸 。总浪费空间 为 所有 箱子中浪费空间的总和。
比方说,如果你想要用尺寸数组为 [4,8] 的箱子装下尺寸为 [2,3,5] 的包裹,你可以将尺寸为 2 和 3 的两个包裹装入两个尺寸为 4 的箱子中,同时把尺寸为 5 的包裹装入尺寸为 8 的箱子中。总浪费空间为 (4-2) + (4-3) + (8-5) = 6 。
请你选择 最优 箱子供应商,使得 总浪费空间最小 。如果 无法 将所有包裹放入箱子中,请你返回 -1 。由于答案可能会 很大 ,请返回它对 109 + 7 取余 的结果。
示例 1:
输入:packages = [2,3,5], boxes = [[4,8],[2,8]]
输出:6
解释:选择第一个供应商最优,用两个尺寸为 4 的箱子和一个尺寸为 8 的箱子。
总浪费空间为 (4-2) + (4-3) + (8-5) = 6 。
示例 2:
输入:packages = [2,3,5], boxes = [[1,4],[2,3],[3,4]]
输出:-1
解释:没有箱子能装下尺寸为 5 的包裹。
示例 3:
输入:packages = [3,5,8,10,11,12], boxes = [[12],[11,9],[10,5,14]]
输出:9
解释:选择第三个供应商最优,用两个尺寸为 5 的箱子,两个尺寸为 10 的箱子和两个尺寸为 14 的箱子。
总浪费空间为 (5-3) + (5-5) + (10-8) + (10-10) + (14-11) + (14-12) = 9 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-space-wasted-from-packaging
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
当时很快的写了个超时的代码如下面第一版,就是排序以后尽量用最小的供应商的箱子装包裹,然后不出意外超时了,改进想到了二分,但是没时间了
因为是排好序的,所以我直观想到的是,可以二分找到当前包裹下的最小箱子或者二分找当前箱子下最大的包裹
然后看了题解,发现是二分找当前箱子下最大的包裹
找到以后,计算上一个下标到当前下标的包裹的尺寸和,这个可以用前缀和优化到O(1)的复杂度,然后就可以计算剩余空间和
class Solution {
public int minWastedSpace(int[] packages, int[][] boxes) {
int l = packages.length;
Arrays.sort(packages);
int m = boxes.length;
for(int i = 0; i < m; i++){
Arrays.sort(boxes[i]);
}
int res = Integer.MAX_VALUE;
//对于第i个公司
for(int i = 0; i < m; i++){
int n = boxes[i].length;
int temp = 0;
if(boxes[i][n - 1] < packages[l - 1])
continue;
int index = 0;
for(int j = 0; j < l; j++){
while(boxes[i][index] < packages[j])
index++;
if(packages[j] <= boxes[i][index])
temp = (temp + boxes[i][index] - packages[j]) % 1000000007;
}
res = Math.min(res, temp);
}
return res == Integer.MAX_VALUE ? -1 : res;
}
}
二分+前缀和优化,二分这里是重灾区,怎么求小于mid的最大值和大于mid的最小值,牢记
class Solution {
public int minWastedSpace(int[] packages, int[][] boxes) {
int mod = (int)(1e9+7);
int l = packages.length;
Arrays.sort(packages);
long[] pre = new long[l + 1];
for(int i = 1; i <= l; i++){
pre[i] = pre[i - 1] + packages[i - 1];
}
int m = boxes.length;
long res = Long.MAX_VALUE;
//对于第i个公司
for(int i = 0; i < m; i++){
Arrays.sort(boxes[i]);
int n = boxes[i].length;
//如果最大的盒子也无法装下包裹,直接跳过
if(boxes[i][n - 1] < packages[l - 1])
continue;
int preid = -1; //上一个盒子可以装下的盒子下标
long temp = 0; //当前剩余空间
for(int box : boxes[i]){
//如果大于当前盒子尺寸,跳过这个盒子
if(preid != -1 && packages[preid] > box)
continue;
//在packages中找到当前盒子能装下的包裹下标
int index = binary(box, packages);
temp += (index - preid) * (long)box - (pre[index + 1] - pre[preid + 1]);
preid = index;
if(index + 1 == l)
break;
}
res = Math.min(res, temp);
}
return res == Long.MAX_VALUE ? -1 : (int)(res % 1000000007);
}
//这个方法找的是大于盒子尺寸的最小包裹下标
/**
public int binary(int box, int[] packages){
int l = 0;
int r = packages.length;
while(l < r){
int mid = ((r - l) >> 1) + l;
if(packages[mid] > box){
r = mid;
}else
l = mid + 1;
}
return l;
}
*/
//这个方法找的是小于盒子尺寸的最大包裹下标
public int binary(int box, int[] packages){
int l = -1;
int r = packages.length - 1;
while(l < r){
int mid = ((r - l + 1) >> 1) + l;
if(packages[mid] > box){
r = mid - 1;
}else
l = mid;
}
return l;
}
}