【周赛总结】LIS问题,比较Integer数值,状态压缩DP。


d第223,224场周赛和43场双周赛的题目解析。中等题目不难,主要是H题目都还是很有特色的。

1713. 得到子序列的最少操作次数 H 最长递增子序列问题的nlogn解法

在这里插入图片描述

本质上在考察最长连续上升子序列问题,我们需要在坐标对应层次上寻找到一个最长上升子序列。

核心是二分的思路,我们维护一个数组,数组中的数字索引是单调递增的(如果要求严格单调,也会是不重复的)。我们每次获取新的备选数字时会考虑能否更新数组,我们会寻找大于等于target的最小值,更新为target(对于严格单调的,可以不考虑等于)。

class Solution {
    public int minOperations(int[] target, int[] arr) {
        // 可以转化为最长递增子序列问题
        HashMap<Integer, Integer> map = new HashMap<>();
        int n = target.length;
        int m = arr.length;
        for(int i = 0;i<n;i++){
            map.put(target[i], i);
        }
        int[] res = new int[m];
        for(int i = 0;i<m;i++){
            if(map.containsKey(arr[i])){
                res[i] = map.get(arr[i]);
            }else{
                res[i] = -1;
            }
        }
        // 用List慢的令人发指 2000 ms
        // List<Integer> queue = new LinkedList<>();
        int[] queue = new int[m];
        int j = 0;
        for (int i = 0;i<m;i++){
            if(res[i] == -1) continue;
            if(j == 0 || res[i]>queue[j-1]){
                queue[j] = res[i];
                j++;
            }else{
                int index = binarySearch(0, j-1, res[i], queue);
                queue[index] = res[i];
            }
        }
        return n-j;
    }
     最小递增子序列的dp, 大于(等于)target的最小值
    private int binarySearch(int left, int right, int target, int[] queue){
        while (left<=right){
            int mid = (left+right)/2;
            int cur = queue[mid];
            if(target>cur){
                left = mid+1;
            }else if (target<cur){
                right = mid-1;
            }else{
                //right = mid-1;
                return mid; // 等于的情况也不能更新,大于(等于)target的最小值。因此可以直接返回
            }
        }
        return left;
    }
}

1719. 重构一棵树的方案数 H

在这里插入图片描述

这个当且仅当条件一定要读清楚。因此我们完全可以按照pairs数组的东西先尝试构造一棵树,然后再检验是不是合理。并且我们可以发现,越是根的树,祖孙关系数量应该越多。换言之,祖孙关系最多的就是根节点。我们可以根据这个性质构造树。

如何检验呢?我们每次构造之后维护fa[]数组,是每个节点的亲父节点。如果两个节点有祖孙关系,但是亲父节点不同。则存在矛盾。因为这说明某个节点的父节点更新时候,竟然没有更新到这“逆子节点”,可见两者不存在关系。

因此我们需要统计,每个节点的祖孙关心,用于寻找根节点。然后维护祖孙邻接表。每次更新。

优雅的函数式编程

如何实现给一个list数组添加东西呢?常规方法分三步,找到对应的数组,添加元素进数组,把数组存回字典。

函数编程一个函数就可以computeIfAbsent()这个函数可以实现如果没有先构建一个数组,然后返回数组让我们更新。

比较Intger的数值

在java中,如果是基本数据类型,==判断的是值。如果是对象类型,==判断的是对象的地址。
对于-128~127的Integer数字,还是可以正常判断的。但是大了就不可以了。因此我们推荐使用Objects.equals(a,b)。来判断两个Integer类型的大小。

 graph.computeIfAbsent(p[0], t->new ArrayList<Integer>()).add(p[1]);

class Solution {
    public int checkWays(int[][] pairs) {
        int ans = 1;
        HashMap<Integer,List<Integer>> graph = new HashMap<Integer, List<Integer>>();
        HashMap<Integer, Integer> map = new HashMap<>();
        // 构建邻接矩阵
        for (int[] p:pairs){
            map.put(p[0], map.getOrDefault(p[0],0)+1);
            map.put(p[1], map.getOrDefault(p[1],0)+1);
            // 优雅的函数式编程
            graph.computeIfAbsent(p[0], t->new ArrayList<Integer>()).add(p[1]);
            graph.computeIfAbsent(p[1], t->new ArrayList<Integer>()).add(p[0]);

        }
        int n = map.size();
        Integer[] nums = new Integer[n];
        int y = 0;
        for (Integer x:map.keySet()){
            nums[y++] = x;
        }
        Arrays.sort(nums, new Comparator<Integer>(){
            //@override
            public int compare(Integer a, Integer b){
                return map.get(b)-map.get(a);
            }
        });
        if(map.get(nums[0]) != n-1)return 0;
        for(int[] p:pairs){
            //如果是基本数据类型,==判断的是值
            //如果是对象类型,==判断的是对象的地址
            if (Objects.equals(map.get(p[1]),map.get(p[0]))){ // 某一对具有祖孙关系的节点的 祖孙复杂关系也一样,说明可以交换
                ans = 2;
                break;
            }
        }
        HashSet<Integer> vis = new HashSet<>();
        HashMap<Integer, Integer> fa = new HashMap<>();
        for(int i:nums){
            fa.put(i,nums[0]); // 初始下,全部节点都链接在根节点下
        }
        vis.add(nums[0]);
        
        for(int i = 1;i<n;i++){;
            for (int j: graph.get(nums[i])){ // 找到nums[i]这个节点的邻接
                if(!vis.contains(j)){ // vis不包含说明是当前nums[i]节点的孙节点
                    if(fa.get(j)!=fa.get(nums[i])){
                        return 0;
                    }
                    fa.put(j, nums[i]);
                }
            }
            vis.add(nums[i]);
        }
        return ans;
    }
}

1723. 完成所有工作的最短时间 H DFS/状态压缩dp

在这里插入图片描述

三种主要的思路,一个是暴力的dfs加上一定的剪枝和贪心。具体就是首先我们对整个队列进行排序按照从大到小排序。然后每次枚举添加的任务到哪个人的队列里面。

第二个思路是状态压缩dp,dp[i][j]表示前 j 名员工,完成 i 所代表的任务需要的最短时间。每次在更新的时候dp[i][j] = Math.min(dp[i][j], Math.max(dp[i-p][j-1], tot[p]));得到更新的结果。其中tot表示一名员工完成s任务需要的时间。 时间复杂度 主要是枚举子集为 3 n 3^n 3n,总复杂度是 k 3 n k3^n k3n

第三种思路是状态压缩DP+二分。我们采用dp[i]表示 i 个员工完成全部任务的时间。我们发现具有单调性,也就是说5个人能在T时间内完成的话,6个人一定也可以。因此我们用二分的方法寻找k个人完成任务的下界时间。时间复杂度 log ⁡ ( s u m ( j o b s ) ) 3 N \log(sum(jobs)) 3^N log(sum(jobs))3N.。如果k大于sum(jobs)会更好。

class Solution {
    int ans = 100000000;
    int[] jobs;
    int k;
    public int minimumTimeRequired(int[] jobs, int k) {
        // dfs搜索的方法,
        this.jobs = jobs;
        this.k = k;
        Arrays.sort(jobs);
        int n = jobs.length;
        dfs(0, n-1, new ArrayList<Integer>());
        return ans;
    }
    private void dfs(int min, int index, List<Integer> queue){
        if (min>=ans) return;
        if(index == -1){
            ans = Math.min(min, ans);
            return;
        }
        int n = queue.size();
        for(int i = 0; i<n;i++){
            queue.set(i, queue.get(i)+jobs[index]);
            dfs(Math.max(min, queue.get(i)), index-1,new ArrayList<>(queue));
            queue.set(i, queue.get(i)-jobs[index]);
        }
        if (n<k){
            queue.add(jobs[index]);
            dfs(Math.max(min, jobs[index]), index-1,new ArrayList<>(queue));
        }
    }
}

============================状态压缩=======================
class Solution {
    public int minimumTimeRequired(int[] jobs, int k) {
        int n = jobs.length;
        int N = 1<<n;
        int[] tot = new int[N];

        for(int i = 1;i<N;i++){
            for(int j = 0;j<n;j++){
                if ((i&(1<<j)) != 0){
                    tot[i] += jobs[j];
                }
            }
        }
        int[][] dp = new int[N][k+1];
        for(int i = 1;i<N;i++) dp[i][1] = tot[i];

        for(int i = 1;i<N;i++){
            for(int j = 2;j<=k;j++){
                dp[i][j] = Integer.MAX_VALUE;
                for (int p = i;p>0;p = (p-1)&i){
                    dp[i][j] = Math.min(dp[i][j], Math.max(dp[i-p][j-1], tot[p]));
                }
            }
        }
        return dp[N-1][k];
    }
}


=============================状态压缩+二分===========================================
class Solution {
    public int minimumTimeRequired(int[] jobs, int k) {
        int n = jobs.length;
        int N = 1<<n;
        int[] tot = new int[N];

        for(int i = 1;i<N;i++){
            for(int j = 0;j<n;j++){
                if ((i&(1<<j)) != 0){
                    tot[i] += jobs[j];
                }
            }
        }
        int l = 0;
        int r = 0;
        for (int i = 0;i<n;i++){
            l = Math.max(l,jobs[i]);
            r += jobs[i];
        }
        while(l<=r){
            int mid = (l+r)/2;
            int[] dp = new int[N];
            for (int i = 1;i<N;i++){
                dp[i] = Integer.MAX_VALUE/2;
                for(int p = i;p>0;p=(p-1)&i){
                    if(tot[p]<=mid){
                        dp[i] = Math.min(dp[i], dp[i-p]+1);
                    }
                }
            }
            if (dp[N-1]<k){
                r = mid-1;
            }else if (dp[N-1]>k){
                l = mid+1;
            }else{
                r = mid-1;
            }
        }
        return l;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值