周赛350(模拟、脑经急转弯、状压DP、动态规划)

文章目录

  • 周赛350
    • [2739. 总行驶距离](https://leetcode.cn/problems/total-distance-traveled/)
      • 模拟
      • 数学
    • [2740. 找出分区值](https://leetcode.cn/problems/find-the-value-of-the-partition/)
      • 转换题意(脑经急转弯)
    • [2741. 特别的排列](https://leetcode.cn/problems/special-permutations/)
      • 状态压缩DP
      • [996. 正方形数组的数目](https://leetcode.cn/problems/number-of-squareful-arrays/)(相似)
    • [2742. 给墙壁刷油漆](https://leetcode.cn/problems/painting-the-walls/)
      • 选或不选DP

周赛350

2739. 总行驶距离

难度简单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;    
    }
}

2740. 找出分区值

难度中等1

给你一个 整数数组 nums

nums 分成两个数组:nums1nums2 ,并满足下述条件:

  • 数组 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;
    }
}

2741. 特别的排列

难度中等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/

状态压缩DP

关键点:

1、为什么可以这个东西可以用记忆化搜索进行优化?

  • 【先选 2 再选 1 然后递归到 4】和【先选 1 再选 2 然后递归到4】都会递归到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;
    }
}

996. 正方形数组的数目(相似)

难度困难109

给定一个非负整数数组 A,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。

返回 A 的正方形排列的数目。两个排列 A1A2 不同的充要条件是存在某个索引 i,使得 A1[i] != A2[i]。

示例 1:

输入:[1,17,8]
输出:2
解释:
[1,8,17] 和 [17,8,1] 都是有效的排列。

示例 2:

输入:[2,2,2]
输出:1

提示:

  1. 1 <= A.length <= 12
  2. 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;
    }
}

2742. 给墙壁刷油漆

难度困难9

给你两个长度为 n 下标从 0 开始的整数数组 costtime ,分别表示给 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

选或不选DP

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

你可能感兴趣的:(算法刷题记录,动态规划,算法,leetcode)