LeetCode 1110
方法:分治
时间复杂度: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
方法:排序
时间复杂度: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
方法: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
方法:前缀和+二分查找
时间复杂度:构建是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();
*/