LeetCode 1110, 1057, 894, 528

152 阅读6分钟

LeetCode 1110

链接:leetcode.com/problems/de…

方法:分治

时间复杂度:O(n),n为树的节点个数

想法:这个题用分治来写。首先考虑假设说我写一个递归函数,它用来删除给定的一个树当中的值在某个set当中的节点,那么它的返回类型应该是什么。不能在这个题用void函数做,因为假设我调了这个函数,它把root的左子树该删的删完了,该放的放完了,把右子树也操作完了,那么我们知道,在这道题里,树的结构会被改变,比方说root的左子节点的值在set里,那当它删完之后,root的左边应该连向null。但void函数无法做到这一点。同理返回List并且试图在递归函数里合并两个list也不可行,仍然绕不开上述问题。 所以为了解决上述问题,我们的递归函数返回类型应该是TreeNode类型,表示在把子树删完放完之后,这个子树的根节点是什么。另外,关于这个递归函数内部的具体实现,因为我们要求的是森林,森林里面的每个数,如果不是原本的根节点的话,它的原来的parent一定是在set里面因此被删掉了的。因此把一个节点放到结果当中这一步,必定发生在parent被删掉的时候,因此在对于当前遍历到的根节点的值进行检查的时候,只有在根节点的值在set当中,即当前根节点会被删掉的情况下,它的children才有可能被加到结果里。我们是不能在遍历到当前根节点的时候,判定这个根节点本身能不能被加到结果里的,因为我们在递归的这一层无从知道这个根节点的parent会不会被删掉。针对这一点,我们不添加当前的root本身,而是试图添加root的left和right子树。所以,当对当前root的左右子树做完分治之后,如果当前root的值不在set里,直接返回就行了。否则,我们将返回null,这样的话递归的上一层,它的parent的指针就会连向null,实现了原树里的删除。在这种情况下,如果左子树或右子树不为null,那就把该加的加进结果。这么递归完会漏掉一个情况,就是假设说原本的root没有充分处理。如果返回过来是null,说明原本root应该被删掉,不会放入结果,那就拉倒。如果不是null,应该把这个树也放进去。

代码:

class Solution {
    public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
        List<TreeNode> res = new ArrayList<>();
        Set<Integer> set = new HashSet<>();
        
        for (int num : to_delete) {
            set.add(num);
        }
        
        root = process(root, set, res);
        
        if (root != null) {
            res.add(root);
        }
        
        return res;
    }
    
    private TreeNode process(TreeNode root, Set<Integer> set, List<TreeNode> res) {
        if (root == null) {
            return null;
        }
        
        root.left = process(root.left, set, res);
        root.right = process(root.right, set, res);
        
        if (!set.contains(root.val)) {
            return root;
        }
        
        if (root.left != null) {
            res.add(root.left);
        }
        if (root.right != null) {
            res.add(root.right);
        }
        
        return null;
    }
}

LeetCode 1057

链接:leetcode.com/problems/ca…

方法:排序

时间复杂度:O(L),L表示桶排序数据范围

想法:这个题比较直观,它说什么你就做什么就完了,反正找出所有的人-车对,然后排序,然后从小到大这么遍历过来反正分配完就完了。但直接排序比较慢,这个题还是在考察桶排序。那么这样的话就是搞出一堆桶来,每个桶代表题目中所说的曼哈顿距离。然后顺着桶的下标捋过去。要是桶也没主,车也没主,那就互相配对上。题目中所说的跟一辆车距离相同有好多人时,或者跟一个人距离相同有好多车时,这两种情况怎么保证呢?这是在我们遍历的时候保证的。我们的桶里面也是按照一定的顺序放的。遍历的时候外面一个大循环遍历人,里面一个循环遍历车,把人-车对放入桶。在这种情况下,再一个桶里如果出现这种情况,那么在前面的一定是人的下标比较小的,如果人下标相同则是车的下标比较小的。因此后面在真正配对时,如果有这种情况,那么那个人或车已经配对完了,所以也就巧妙地规避了这种情况。

代码:

class Solution {
    public int[] assignBikes(int[][] workers, int[][] bikes) {
        int m = workers.length, n = bikes.length;
        int[] assignedWorkers = new int[m];
        int[] assignedBikes = new int[n];
        Arrays.fill(assignedWorkers, -1);
        Arrays.fill(assignedBikes, -1);
        
        List[] bucket = new List[2001];
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int d = Math.abs(workers[i][0] - bikes[j][0]) + Math.abs(workers[i][1] - bikes[j][1]);
                if (bucket[d] == null) {
                    bucket[d] = new ArrayList<int[]>();
                }
                bucket[d].add(new int[] {i, j});
            }
        }
        
        for (int d = 1; d < 2001; d++) {
            if (bucket[d] == null) {
                continue;
            }
            for (int k = 0; k < bucket[d].size(); k++) {
                int[] pair = (int[]) bucket[d].get(k);
                if (assignedWorkers[pair[0]] == -1 && assignedBikes[pair[1]] == -1) {
                    assignedWorkers[pair[0]] = pair[1];
                    assignedBikes[pair[1]] = pair[0];
                }
            }
        }
        
        return assignedWorkers;
    }
}

LeetCode 894

链接:leetcode.com/problems/al…

方法:DP

时间复杂度:听说是Catalan数,但我目前不会证明。这题相当于还没完全做完,抽空补上

想法:这题主要是观察得对不对。full binary tree的左右子树都得是full binary tree,因此通过DP往上推,枚举左边的点数。容易发现只有在n为奇数的时候才有结果,否则直接是空列表。DP推到第n阶时,因为根节点要占去一个名额,因此左与右加起来一共n - 1个点,那直接枚举左边点数,然后加到dp[n]里就可以了。因为前面算的没有重复,因此新产生的dp[n]里面也不会有重复。这题还是刚才强调的,主要是观察得对不对。我一开始觉得是DP或者递归带memo,但是我原本的错误想法是在n-2阶的结果基础上,在每个叶子节点试图加两个子节点。这样会导致去重很麻烦。确实是当时没观察对。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<TreeNode> allPossibleFBT(int n) {
        if (n % 2 == 0) {
            return new ArrayList<>();
        }
        
        List[] dp = new List[n + 1];
        dp[1] = new ArrayList<>();
        dp[1].add(new TreeNode(0));
        
        for (int i = 3; i <= n; i += 2) {
            dp[i] = new ArrayList<>();
            for (int l = 1; l < i; l += 2) {
                List<TreeNode> lList = dp[l];
                List<TreeNode> rList = dp[i - l - 1];
                for (TreeNode ln : lList) {
                    for (TreeNode rn : rList) {
                        TreeNode root = new TreeNode(0);
                        root.left = ln;
                        root.right = rn;
                        dp[i].add(root);
                    }
                }
            }
        }
        
        return dp[n];
    }
}

LeetCode 528

链接:leetcode.com/problems/ra…

方法:前缀和+二分查找

时间复杂度:构建是O(n),pick是O(log(sum)),sum是原本输入数组的所有值之和

想法:这种思路其实也比较基本。实现按权重随机的话,一种思路是把1重复一遍,把2重复两边,4重复4遍这样开一个数组然后随机扔值,但这样开出来数组太大,内存就会爆掉。不能对下标进行随机,那就试图对值进行随机。考虑如果我们对值进行随机,然后这个随机数在某个范围内的时候,我们就扔某个下标,怎么实现?可以想到用前缀数组,扔一个随机数然后看看这个随机数在前缀数组的哪两个值之间,然后我们就知道这一段是哪个下标管的,就把下标返回。这个题在实现当中,我们二分出第一个让这个随机数小于的地方。假如原数组[1,2,3],那么前缀和数组为[0,1,3,6]。在[0,6)这个左闭右开区间当中选一个数出来,那么可能出现的数就是0,1,2,3,4,5。这里有6个数,随机数0会对应原数组的1,1-2对应原数组的2,3-5对应原数组的3,所以是这样来保证概率分布的。你反正随机数的值域一定只能有严格的sum个数,但我们这样选会有可能产生一个随机数0,我们需要让0对应到原数组的第一个值,因此我们在这里选择了“二分出第一个让这个随机数小于的地方”。这里试图举一反三,假如我们不这么写,我们写的随机数范围是1-sum,而不是0到sum-1,那么我们的二分需要查找的就应该是,前缀数组中,第一个小于等于随机数的地方。

代码:

class Solution {
    
    private int[] prefix;
    private int n;
    Random rand = new Random();

    public Solution(int[] w) {
        n = w.length;
        prefix = new int[n];
        prefix[0] = w[0];
        
        for (int i = 1; i < n; i++) {
            prefix[i] = prefix[i - 1] + w[i];
        }
    }
    
    public int pickIndex() {
        int total = prefix[n - 1];
        int pick = rand.nextInt(total);
        
        int left = 0, right = n - 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            if (pick >= prefix[mid]) {
                left = mid;
            }
            else {
                right = mid;
            }
        }
        
        if (pick < prefix[left]) {
            return left;
        }
        
        return right;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(w);
 * int param_1 = obj.pickIndex();
 */