[Leetcode Weekly Contest]256

链接:LeetCode

[Leetcode]1984. 学生分数的最小差值

给你一个 下标从 0 开始 的整数数组 nums ,其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k 。
从数组中选出任意 k 名学生的分数,使这 k 个分数间 最高分 和 最低分 的 差值 达到 最小化 。
返回可能的最小差值 。

暴力循环即可。

import java.util.Arrays;
class Solution {
    public int minimumDifference(int[] nums, int k) {
        Arrays.sort(nums);
        int res = Integer.MAX_VALUE;
        for(int i=0;i<nums.length-k+1;++i)
        {
            int start = i;
            int end = i+k-1;
            res = Math.min(res,nums[end]-nums[start]);
        }
        return res;
    }
}

[Leetcode]1985. 找出数组中的第 K 大整数

给你一个字符串数组 nums 和一个整数 k 。nums 中的每个字符串都表示一个不含前导零的整数。
返回 nums 中表示第 k 大整数的字符串。
注意:重复的数字在统计时会视为不同元素考虑。例如,如果 nums 是 ["1","2","2"],那么 "2" 是最大的整数,"2" 是第二大的整数,"1" 是第三大的整数。

自定义排序可快速找到所有排好序的字符串,对于特定第k大,则使用小根堆为最佳解法。

import java.util.PriorityQueue;
class Solution {
    public String kthLargestNumber(String[] nums, int k) {
        PriorityQueue<String> q = new PriorityQueue<String>((o1, o2) -> {
            // 从小到大排序
            if (o1.length() == o2.length()) {
                return o1.compareTo(o2);
            } else {
                return o1.length() - o2.length();
            }
        });
        for(String num : nums) {
            q.offer(num);
            if (q.size() > k) {
                q.poll();
            }
        }
        System.out.println(q);
        return q.peek();
    }
}

[Leetcode]1986. 完成任务的最少工作时间段

你被安排了 n 个任务。任务需要花费的时间用长度为 n 的整数数组 tasks 表示,第 i 个任务需要花费 tasks[i] 小时完成。一个 工作时间段 中,你可以 至多 连续工作 sessionTime 个小时,然后休息一会儿。
你需要按照如下条件完成给定任务:
如果你在某一个时间段开始一个任务,你需要在 同一个 时间段完成它。
完成一个任务后,你可以 立马 开始一个新的任务。
你可以按 任意顺序 完成任务。
给你 tasks 和 sessionTime ,请你按照上述要求,返回完成所有任务所需要的 最少 数目的 工作时间段 。
测试数据保证 sessionTime 大于等于 tasks[i] 中的 最大值 。

这是一道经典的状压DP题。
状态压缩,其实就是将每个任务 task[i] 看作某个「十进制数字」的二进制上的第 i 位,1 表示该任务被选择,反之不被选择。其经典思想是通过二进制表示一个数组的状态。

我们用 \(f[\textit{mask}]\) 表示当选择任务的状态为 \(\textit{mask}\) 时,最少需要的工作时间段。其中 \(\textit{mask}\) 是一个长度为 n 的二进制表示,\(\textit{mask}\) 从低到高的第 i 位为 1 表示第 i 个任务已经完成,0 表示第 i 个任务未完成。
在进行状态转移时,我们可以枚举最后一个工作时间段完成了哪些任务。

这是这道题的一个难点。通过枚举最后一个工作时间段的任务来分裂所有状态。

显然,这些任务对应的状态 \(\textit{subset}\) 一定是 \(\textit{mask}\) 的一个子集。

另外一个难点就是如何列举mask的子集。

其中 \(\textit{mask} \backslash \textit{subset}\) 表示将 \(\textit{subset}\) 中的所有 1 从 \(\textit{mask}\) 中移除后的二进制表示,可以使用按位异或运算求出。

\(\textit{mask}\)是1111, 其中一个\(\textit{subset}\)为1001,也就是我们最后一个工作时间段完成了第一个和最后一个任务,那么我们在最后一个任务之前需要完成的状态就是\(\textit{mask} \backslash \textit{subset}\),也就是0110.

需要注意的是,\(\textit{subset}\) 中包含的任务的工作时间总和不能大于 \(\textit{sessionTime}\)。因此在动态规划之前,我们可以进行预处理,即在 \([1, 2^n)\)的范围内枚举每一个二进制表示 \(\textit{mask}\),计算其如果包含的任务的工作时间总和。如果其小于等于 \(\textit{sessionTime}\),那么将 \(\textit{valid}[\textit{mask}]\) 标记为 \(\text{True}\),否则标记为 \(\text{False}\)。这样在动态规划时,我们就不需要再计算 \(\textit{subset}\) 是否满足要求了。

动态规划的边界条件为\(f[0] = 0\),最终的答案即为 \(f[2^n - 1]\)]。

class Solution {
    public int minSessions(int[] tasks, int sessionTime) {
        int n = tasks.length, m = 1 << n;
        boolean[] valid = new boolean[m];
        Arrays.fill(valid,false);
        for(int i=1;i<m;++i)
        {
            int spendTime = 0;
            for(int bit=0;bit<n;++bit)
            {
                if((i&(1<<bit))!=0)
                {
                    spendTime += tasks[bit];
                }
            }
            if(spendTime <= sessionTime)
            {
                valid[i] = true;
            }
        }
        int[] dp = new int[m];
        Arrays.fill(dp,Integer.MAX_VALUE);
        dp[0] = 0;
        for(int i=1;i<m;++i)
        {
            int subset = i;
            while(subset>0)
            {
                if(valid[subset])
                {
                    dp[i] = Math.min(dp[i],dp[subset^i]+1);
                }
                subset = (subset-1)&i;
            }
        }
        return dp[m-1];
    }
}

[Leetcode]1987. 不同的好子序列数目

给你一个二进制字符串 binary 。 binary 的一个 子序列 如果是 非空 的且没有 前导 0 (除非数字是 "0" 本身),那么它就是一个 好 的子序列。
请你找到 binary 不同好子序列 的数目。
比方说,如果 binary = "001" ,那么所有 好 子序列为 ["0", "0", "1"] ,所以 不同 的好子序列为 "0" 和 "1" 。 注意,子序列 "00" ,"01" 和 "001" 不是好的,因为它们有前导 0 。
请你返回 binary 中 不同好子序列 的数目。由于答案可能很大,请将它对 1e9 + 7 取余 后返回。
一个 子序列 指的是从原数组中删除若干个(可以一个也不删除)元素后,不改变剩余元素顺序得到的序列。

动态规划。难点还是在于寻找子问题之间的推导关系
我们用 \(f[i][0]\)\(f[i][1]\) 分别表示使用字符串 \(\textit{binary}\) 的第 0, 1, \(\cdots\), i 个字符,可以构造出的以 \(0/1\) 结尾的不同的好子序列的数目。

由于「好子序列」不能包含前导 0,但本身可以为 0,那么我们可以规定「好子序列」必须以 1 开始并求出答案,如果 \(\textit{binary}\) 中包含 0 就再对答案增加 1。这样做可以避免处理复杂的前导 0 规则。

如果第 i 个字符是 1,那么以 0 结尾的不同的好子序列的数目不会改变,即:

\[f[i][0] = f[i - 1][0] \]

难点在于如何通过已经求得结果的状态求出 \(f[i][1]\) 的值。
以1结尾的好序列个数,则可以分成两种情况

  • 包括第i个字符,为\(f[i-1][0] + f[i-1][1] + 1\),设有A类
  • 不包括第i个字符,为\(f[i−1][1]\),设有B类
    其实可以想象A类一定包括了B类的字符序列,对于任意一个B类中的子序列,它的最后一个元素一定为 1,如果我们将该元素变为第 i 个字符 1,那么就将该 B 类子序列「转换」为了 A 类子序列。因此,每一个 B 类子序列都唯一地对应着一个 A 类子序列,说明 B 是 A 的子集,那么两类总共的个数也就是\(f[i-1][0] + f[i-1][1] + 1\)
    同理也可得第 i 个字符是 0的情况。
class Solution {
    public final static int MOD = (int)1e9+7;
    public int numberOfUniqueGoodSubsequences(String binary) {
        int even = 0, odd = 0;
        int n = binary.length();
        for(int i=0;i<n;++i)
        {
            if (binary.charAt(i) == '0') even = (even+odd)%MOD;
            else odd = (even+odd+1)%MOD;
        }
        int res = even + odd;
        if(binary.contains("0")) res++;
        return res%MOD;
    }
}

Leetcode
Leetcode

posted @ 2021-09-01 19:43  Jamest  阅读(38)  评论(0编辑  收藏  举报