难度简单4
卡车有两个油箱。给你两个整数,mainTank
表示主油箱中的燃料(以升为单位),additionalTank
表示副油箱中的燃料(以升为单位)。
该卡车每耗费 1
升燃料都可以行驶 10
km。每当主油箱使用了 5
升燃料时,如果副油箱至少有 1
升燃料,则会将 1
升燃料从副油箱转移到主油箱。
返回卡车可以行驶的最大距离。
注意:从副油箱向主油箱注入燃料不是连续行为。这一事件会在每消耗 5
升燃料时突然且立即发生。
示例 1:
输入:mainTank = 5, additionalTank = 10
输出:60
解释:
在用掉 5 升燃料后,主油箱中燃料还剩下 (5 - 5 + 1) = 1 升,行驶距离为 50km 。
在用掉剩下的 1 升燃料后,没有新的燃料注入到主油箱中,主油箱变为空。
总行驶距离为 60km 。
示例 2:
输入:mainTank = 1, additionalTank = 2
输出:10
解释:
在用掉 1 升燃料后,主油箱变为空。
总行驶距离为 10km 。
提示:
1 <= mainTank, additionalTank <= 100
class Solution {
public int distanceTraveled(int mainTank, int additionalTank) {
int ans = 0;
while(mainTank >= 5){
ans += 50;
mainTank -= 5;
if(additionalTank > 0){
additionalTank -= 1;
mainTank += 1;
}
}
ans += mainTank * 10;
return ans;
}
}
class Solution {
public int distanceTraveled(int mainTank, int additionalTank) {
return (mainTank + Math.min(additionalTank, (mainTank-1) / 4)) * 10;
}
}
难度中等1
给你一个 正 整数数组 nums
。
将 nums
分成两个数组:nums1
和 nums2
,并满足下述条件:
nums
中的每个元素都属于数组 nums1
或数组 nums2
。分区值的计算方法是 |max(nums1) - min(nums2)|
。
其中,max(nums1)
表示数组 nums1
中的最大元素,min(nums2)
表示数组 nums2
中的最小元素。
返回表示分区值的整数。
示例 1:
输入:nums = [1,3,2,4]
输出:1
解释:可以将数组 nums 分成 nums1 = [1,2] 和 nums2 = [3,4] 。
- 数组 nums1 的最大值等于 2 。
- 数组 nums2 的最小值等于 3 。
分区值等于 |2 - 3| = 1 。
可以证明 1 是所有分区方案的最小值。
示例 2:
输入:nums = [100,1,10]
输出:9
解释:可以将数组 nums 分成 nums1 = [10] 和 nums2 = [100,1] 。
- 数组 nums1 的最大值等于 10 。
- 数组 nums2 的最小值等于 1 。
分区值等于 |10 - 1| = 9 。
可以证明 9 是所有分区方案的最小值。
提示:
2 <= nums.length <= 105
1 <= nums[i] <= 109
先将数组排序,然后枚举划分方案。排序后的数组的某个前缀是nums1,其余部分(后缀)是nums2,此时发现本质上就是求相邻元素的最小差值 min(nums[i]-nums[i-1])
。
class Solution {
public int findValueOfPartition(int[] nums) {
Arrays.sort(nums);
int ans = nums[1] - nums[0];
for(int i = 2; i < nums.length; i++){
ans = Math.min(ans, nums[i] - nums[i-1]);
}
return ans;
}
}
难度中等12
给你一个下标从 0 开始的整数数组 nums
,它包含 n
个 互不相同 的正整数。如果 nums
的一个排列满足以下条件,我们称它是一个特别的排列:
0 <= i < n - 1
的下标 i
,要么 nums[i] % nums[i+1] == 0
,要么 nums[i+1] % nums[i] == 0
。请你返回特别排列的总数目,由于答案可能很大,请将它对 109 + 7
取余 后返回。
示例 1:
输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。
示例 2:
输入:nums = [1,4,3]
输出:2
解释:[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。
提示:
2 <= nums.length <= 14
1 <= nums[i] <= 109
题解:https://leetcode.cn/problems/special-permutations/solution/zhuang-ya-dp-by-endlesscheng-4jkr/
关键点:
1、为什么可以这个东西可以用记忆化搜索进行优化?
dfs(*,4)
,参数相同,是一个重复的子问题,可以用记忆化搜索解决 O(n!) -> O(2^n)
2、状态压缩DP = ①排列型的回溯、②记忆化搜索=>递推、③集合=>位运算
记忆化搜索
class Solution {
// 定义dfs(i, j) 表示当前可以选的下标集合为 i, 上一个选的数的下标是j,
// 转移:从i中选一个下标k
// 如果 nums[i] % nums[j] == 0 or nums[j] % nums[i] == 0
// 则 dfs(i, j) += sum(dfs(i\{k}, k) for k in i)
// 递归边界:dfs(空集【0】, j) = 1 // 递归到i是空集,说明找到了一个特别的排列
// 递归入口:dfs(U\{j}, j)
// 答案: sum(dfs(U\{j}, j) for j in range(n))
// 时间复杂度 = O(状态个数) * O(单个状态的计算时间) <- 【动态规划的时间复杂度】
// = O(n * 2^n) * O(n)
private static final int MOD = (int) 1e9 + 7;
int n;
int[][] cache;
int[] nums;
public int specialPerm(int[] nums) {
n = nums.length;
this.nums = nums;
// cache[i][j] : i是集合的所有情况 2^i个,j表示上一次选的数 n个
cache = new int[1 << n][n];
for(int i = 0; i < (1 << n); i++)
Arrays.fill(cache[i], -1);
int ans = 0;
int u = (1 << n) - 1; // 全集
for(int i = 0; i < n; i++){ // 初始状态下每个数都可以选
ans = (ans + dfs(u ^ (1 << i), i)) % MOD;
}
return ans % MOD;
}
public int dfs(int i, int j){
if(i == 0) return 1;
if(cache[i][j] >= 0) return cache[i][j];
int res = 0;
// 遍历集合
for(int k = 0; k < n; k++){
// 判断元素k是否在集合i中(是否可以选)
if(((i >> k) & 1) == 1){
if(nums[j] % nums[k] == 0 || nums[k] % nums[j] == 0){ // 题目要求
res = (res + dfs(i ^ (1 << k), k)) % MOD;
}
}
}
return cache[i][j] = res % MOD;
}
}
转成递推
class Solution {
private static final int MOD = (int) 1e9 + 7;
public int specialPerm(int[] nums) {
int n = nums.length;
// 定义f[i][j] 表示当前可以选的下标集合为 i, 上一个选的数的下标是j,
int[][] f = new int[1 << n][n];
for(int i = 0; i < n; i++)
f[0][i] = 1;
// 递归dfs(i,j) ,递推就得循环计算i和j
// i 从小到大遍历(遍历所有状态集合)
for(int i = 0; i < (1 << n); i++){
// 遍历每个元素
for(int j = 0; j < n; j++){
for(int k = 0; k < n; k++){
if(((i >> k) & 1) == 1 && (nums[j] % nums[k] == 0 || nums[k] % nums[j] == 0))
f[i][j] = (f[i][j] + f[i ^ (1 << k)][k]) % MOD;
}
}
}
int ans = 0;
for(int i = 0; i < n; i++){
ans = (ans + f[((1<<n)-1)^(1<<i)][i]) % MOD;
}
return ans;
}
}
难度困难109
给定一个非负整数数组 A
,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。
返回 A 的正方形排列的数目。两个排列 A1
和 A2
不同的充要条件是存在某个索引 i
,使得 A1[i] != A2[i]。
示例 1:
输入:[1,17,8]
输出:2
解释:
[1,8,17] 和 [17,8,1] 都是有效的排列。
示例 2:
输入:[2,2,2]
输出:1
提示:
1 <= A.length <= 12
0 <= A[i] <= 1e9
class Solution {
int[] nums;
int n;
int[][] cache;
public int numSquarefulPerms(int[] nums) {
this.nums = nums;
n = nums.length;
int u = (1 << n) - 1;
cache = new int[1 << n][n];
for(int i = 0; i < (1 << n); i++)
Arrays.fill(cache[i], -1);
int res = 0;
for(int i = 0; i < n; i++){
res += dfs(u ^ (1 << i), i);
}
// 去重 : dp 算出来的结果有很多重复的,需要去重,这里用的是乘法原理去重,
// 例如1,1,2,2,2,3中全排列去重,两个1交换位置会多算一次(共2次),
// 三个2交换位置会多算5次(共6次),最后结果除以每个重复数次数的阶乘。
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
res /= getFactorial(entry.getValue());
}
return res;
}
// 定义dfs(i, j) 表示当前可以选的下标集合为 i, 上一个选的数的下标是j,
public int dfs(int i, int j){
if(i == 0) return 1;
if(cache[i][j] >= 0) return cache[i][j];
int res = 0;
for(int k = 0; k < n; k++){
if(((i >> k) & 1) == 1 && isSqrt(nums[j] + nums[k])){
res += dfs(i ^ (1 << k), k);
}
}
return cache[i][j] = res;
}
public boolean isSqrt(int x){
int i = (int)Math.sqrt(x);
return i * i == x;
}
public int getFactorial(int x){
int cnt = 1;
for(int i = 1; i <= x; i++){
cnt *= i;
}
return cnt;
}
}
转成递推
class Solution {
public int numSquarefulPerms(int[] nums) {
int n = nums.length;
int[][] f = new int[1 << n][n];
for(int i = 0; i < n; i++)
f[0][i] = 1;
for(int i = 0; i < (1 << n); i++){
for(int j = 0; j < n; j++){
for(int k = 0; k < n; k++){
if(((i >> k) & 1) == 1 && isSqrt(nums[j] + nums[k]))
f[i][j] += f[i ^ (1 << k)][k];
}
}
}
int res = 0;
for(int i = 0; i < n; i++) res += f[((1<<n)-1) ^ (1<<i)][i];
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
res /= getFactorial(entry.getValue());
}
return res;
}
public boolean isSqrt(int x){
int i = (int)Math.sqrt(x);
return i * i == x;
}
public int getFactorial(int x){
int cnt = 1;
for(int i = 1; i <= x; i++){
cnt *= i;
}
return cnt;
}
}
难度困难9
给你两个长度为 n
下标从 0 开始的整数数组 cost
和 time
,分别表示给 n
堵不同的墙刷油漆需要的开销和时间。你有两名油漆匠:
i
堵墙需要花费 time[i]
单位的时间,开销为 cost[i]
单位的钱。1
单位,开销为 0
。但是必须在付费油漆匠 工作 时,免费油漆匠才会工作。请你返回刷完 n
堵墙最少开销为多少。
示例 1:
输入:cost = [1,2,3,2], time = [1,2,3,2]
输出:3
解释:下标为 0 和 1 的墙由付费油漆匠来刷,需要 3 单位时间。同时,免费油漆匠刷下标为 2 和 3 的墙,需要 2 单位时间,开销为 0 。总开销为 1 + 2 = 3 。
示例 2:
输入:cost = [2,3,4,2], time = [1,1,1,1]
输出:4
解释:下标为 0 和 3 的墙由付费油漆匠来刷,需要 2 单位时间。同时,免费油漆匠刷下标为 1 和 2 的墙,需要 2 单位时间,开销为 0 。总开销为 2 + 2 = 4 。
提示:
1 <= cost.length <= 500
cost.length == time.length
1 <= cost[i] <= 106
1 <= time[i] <= 500
https://leetcode.cn/problems/painting-the-walls/solution/xuan-huo-bu-xuan-de-dian-xing-si-lu-by-e-ulcd/
class Solution {
// 付费的油漆匠 选或不选,只有一个付费油漆匠,time[i]需要加起来
// 如果第 i 面墙分配给免费的油漆匠,那么消耗 1 单位的时间
// 定义dfs(i,j):刷完0 ~ i 的墙,且当前累计付费时间为j(已经赊给免费的时间),最少开销为多少
// 付费:dfs(i,j) = dfs(i-1, j+time[i]) + cost[i]
// 免费:dfs(i,j) = dfs(i-1, j-1)
// 转移:dfs(i,j) = min(dfs(i-1, j+time[i])+cost[i], dfs(i-1, j-1))
// 递归边界:dfs(-1, <0) = inf ; dfs(i,j) = 0 if j > i(时间超过了剩下需要刷的墙)
// 递归入口:dfs(n-1, 0)
int[] cost, time;
int[][] cache;
int n;
public int paintWalls(int[] cost, int[] time) {
this.cost = cost;
this.time = time;
n = cost.length;
cache = new int[n][2*n+1]; // 免费时长可以为负数,因此需要加偏移量
for(int i = 0; i < n; i++)
Arrays.fill(cache[i], -1);
return dfs(n-1, n); // 偏移量防止负数
}
// 免费时长为j,刷前i片墙需要的最小花费
public int dfs(int i, int j){
if(i < j-n) return 0; // 剩余所有墙都可以由免费油漆工刷
if(i < 0) return Integer.MAX_VALUE / 2;
if(cache[i][j] >= 0) return cache[i][j];
// 免费油漆工刷 dfs(i-1, j-1)
// 付费油漆工刷 dfs(i-1, j+time[i])+cost[i]
return cache[i][j] = Math.min(dfs(i-1, j+time[i])+cost[i], dfs(i-1, j-1));
}
}