算法
每日一题

每日一题部分记录

https://leetcode.cn/problemset/ (opens in a new tab)

2月28:2673. 使二叉树所有路径值相等的最小代价

2673. 使二叉树所有路径值相等的最小代价 (opens in a new tab)

思路:贪心

我们首先考虑所有的叶结点。

对于任一叶结点,它的值为 x,它的兄弟节点的值为 y。 可以发现,对于树上的其余节点,它们要么同时是这两个叶节点的祖先,要么同时不是这两个叶节点的祖先。 对这些节点进行一次操作,要么同时增加了根到这两个叶节点的路径值,要么没有任何效果。 因此,要想使得根到这两个叶节点的路径值相等,我们只能增加 x 和 y 本身

待考虑完所有叶节点之后,互为兄弟节点的叶节点的值两两相等(并且根到它们的路径值显然也相等)。 如果我们还需要操作某个叶节点,那么为了使得路径值相等,它的兄弟节点同样也需要操作。 此时就需要两次操作,但不如直接操作它们的双亲节点,可以省去一次操作。

因此,所有的叶节点都无需进行操作。我们就可以将它们全部移除。 为了使得路径值保持不变,我们可以将叶节点的值增加至它们的双亲节点。 这样一来,所有的双亲节点都变成了新的叶节点,我们重复进行上述操作即可。 当只剩一个节点(根节点)时,就可以得到最终的答案。

// 其实没必要使用[layer_start, layer_end]显式地按层遍历,
// 不过没什么大的效率差异,就不改了
class Solution {
    public int minIncrements(int n, int[] cost) {
        int layer_end = n;
        int count = 0;
        while (layer_end > 0) {
            // [layer_start, layer_end]
            int layer_start = (layer_end + 1) / 2;
            int cur = layer_end;
            while (cur > layer_start) {
                int parent = cur / 2;
                int max = Math.max(cost[cur - 1], cost[cur - 2]);
                count += max * 2 - cost[cur - 1] - cost[cur - 2];
                cost[parent - 1] += max;
                cur -= 2;
            }
            layer_end = layer_start - 1;
        }
        return count;
    }
}

3月1:2369. 检查数组是否存在有效划分

2369. 检查数组是否存在有效划分 (opens in a new tab)

思路:动态规划,dp数组含义如下:

dp[i][0] 拿到第一个数的状态 (blocked)

dp[i][1] 拿到2个相等元素的状态 (can over)

dp[i][2] 拿到3个相等元素的状态 (can over)

dp[i][3] 拿到2个连续递增元素的状态 (blocked)

dp[i][4] 拿到3个连续递增元素的状态 (can over)

blocked意思是不能作为划分的最后一个元素, can over意思是可以作为划分的最后一个元素。

最终判断的是上述can over的状态是否至少有一个为true。

class Solution {
    public boolean validPartition(int[] nums) {
        // dp[i][0] 拿到第一个数的状态 (blocked)
        // dp[i][1] 拿到2个相等元素的状态 (can over)
        // dp[i][2] 拿到3个相等元素的状态 (can over)
        // dp[i][3] 拿到2个连续递增元素的状态 (blocked)
        // dp[i][4] 拿到3个连续递增元素的状态 (can over)
        boolean[][] dp = new boolean[nums.length][5];
        dp[0][0] = true;
        for (int i = 1; i < nums.length; i++) {
 
            dp[i][0] = dp[i - 1][1] || dp[i - 1][2] || dp[i - 1][4];
 
            if (nums[i] == nums[i - 1]) {
                dp[i][1] = dp[i - 1][0];
                dp[i][2] = dp[i - 1][1];
            }
            else if (nums[i] == nums[i - 1] + 1) {
                dp[i][3] = dp[i - 1][0];
                dp[i][4] = dp[i - 1][3];
            }
        }
        if (dp[nums.length - 1][1] || dp[nums.length - 1][2] || dp[nums.length - 1][4]) {
            return true;
        }
        else return false;
    }
}

3月2:2368. 受限条件下可到达节点的数目

2368. 受限条件下可到达节点的数目 (opens in a new tab)

自己解法1

思路:遍历边集,如果边的一个顶点和0相连且另一个顶点不是受限顶点,则将此顶点加入与0相连的集合。 重复上述操作,直到与0相连的集合不再变化。

这种解法会超时...

class Solution {
    public int reachableNodes(int n, int[][] edges, int[] restricted) {
        int ans = 1;
        boolean changed  = true;
        boolean[] set = new boolean[n];
        boolean[] restricted_set = new boolean[n];
        set[0] = true;
        for (int e : restricted) {
            restricted_set[e] = true;
        }
        while (changed) {
            changed = false;
            for (int[] v : edges) {
                if (set[v[0]] && !restricted_set[v[1]] && !set[v[1]]) {
                    set[v[1]] = true;
                    changed = true;
                    ans++;
                }
                if (set[v[1]] && !restricted_set[v[0]] && !set[v[0]]) {
                    set[v[0]] = true;
                    changed = true;
                    ans++;
                }
            }
        }
        return ans;
    }
}

自己解法2

建表,dfs遍历。

class Solution {
    private int ans = 1;
    List<Integer>[] matrix;
    boolean[] set;
    public int reachableNodes(int n, int[][] edges, int[] restricted) {
        boolean[] restricted_set = new boolean[n];
        matrix = new List[n];
        set = new boolean[n];
        for (int e : restricted) {
            restricted_set[e] = true;
        }
        for (int i = 0; i < n; i++) {
            matrix[i] = new ArrayList<Integer>();
        }
        for (int[] e : edges) {
            if (!restricted_set[e[0]] && !restricted_set[e[1]]) {
                matrix[e[0]].add(e[1]);
                matrix[e[1]].add(e[0]);
            }
        }
        set[0] = true;
        dfs(n, 0);
        return ans;
    }
 
    private void dfs(int n, int cur) {
        for (int i = 0; i < matrix[cur].size(); i++) {
            if (!set[matrix[cur].get(i)]) {
                ans++;
                set[matrix[cur].get(i)] = true;
                dfs(n, matrix[cur].get(i));
            }
        }
    }
}

3月5:1976. 到达目的地的方案数

1976. 到达目的地的方案数 (opens in a new tab)

思路:dijkstra算法+动态规划

class Solution {
    public int countPaths(int n, int[][] roads) {
        long[][] G = new long[n][n];
        long[] dist = new long[n];
        long[] D = new long[n];
        int[] count = new int[n];
        for (long[] row : G) {
            Arrays.fill(row, Long.MAX_VALUE / 2);
        }
        Arrays.fill(dist, Long.MAX_VALUE / 2);
        Arrays.fill(D, Long.MAX_VALUE / 2);
        Arrays.fill(count, 0);
 
        // 构建邻接矩阵
        for (int i = 0; i < roads.length; i++) {
            G[roads[i][0]][roads[i][1]] = roads[i][2];
            G[roads[i][1]][roads[i][0]] = roads[i][2];
        }
 
        // dij算法求最短路+dp计算路的数量
        dist[0] = 0;
        D[0] = 0;
        count[0] = 1;
        int i = 0;
        for (int k = 0; k < n - 1; k++) {
            int minIndex = 0;
            long min = Long.MAX_VALUE / 2;
            for (int j = 0; j < n; j++) {
                // 如果ij有路
                if (G[i][j] < Long.MAX_VALUE / 2) {
                    // 如果是新的最短路
                    if (D[i] + G[i][j] < dist[j]) {
                        // 更新最短路
                        dist[j] = D[i] + G[i][j];
                        // 更新dp:到j的最短路数量 = 到i最短路的数量
                        count[j] = count[i];
                    }
                    // 如果与记录的最短路相同
                    else if (D[i] + G[i][j] == dist[j]) {
                        // 更新dp:到j的最短路数量 += 到i最短路的数量
                        count[j] = (count[j] + count[i]) % (int) (1e9 + 7);
                    }
                }
                if (D[j] == Long.MAX_VALUE / 2 && dist[j] < min) {
                    min = dist[j];
                    minIndex = j;
                }
            }
            D[minIndex] = min;
            i = minIndex;
        }
        // System.out.println(D[n - 1]);
        return count[n - 1];
    }
}

3月6:2917. 找出数组中的 K-or 值

2917. 找出数组中的 K-or 值 (opens in a new tab)

简单题,注意位运算和越界就行

((num >> i) & 1) != 0 代表第i位是否为1

3月7:2575. 找出字符串的可整除数组

2575. 找出字符串的可整除数组 (opens in a new tab)

分析:本题字符串数字长度过大,无法直接数字模除,当然可以考虑用java的BigInteger,但是这样效率太低。

思路:利用模除性质动规

余数的递推公式是:rest = (rest * 10 + 当前位) % m,

因为m是int范围,所以rest最大也可以用long存储,不会越界。

class Solution {
    public int[] divisibilityArray(String word, int m) {
        int[] resu = new int[word.length()];
        int start = 0;
        long rest = 0;
        String str_m = Integer.toString(m);
        for (int i = 0; i < word.length(); i++) {
            // int now = Integer.parseInt(word.substring(i, i + 1));
            // rest = (rest * 10 + now) % m;
            // 直接操作char效率更高
            char c = word.charAt(i);
            rest = (rest * 10 + (c - '0')) % m;
            if (rest == 0) resu[i] = 1;
        }
        return resu;
    }
}

这边也给出BigInteger的解法:

import java.math.BigInteger;
 
class Solution {
    public int[] divisibilityArray(String word, int m) {
        int[] resu = new int[word.length()];
        int start = 0;
        String str_m = Integer.toString(m);
        for (int i = 0; i < word.length(); i++) {
            BigInteger dived = new BigInteger(word.substring(start, i + 1));
            BigInteger diver = new BigInteger(str_m);
            if (dived.mod(diver).equals(BigInteger.ZERO)) {
                resu[i] = 1;
                start = i + 1;
            }
        }
        return resu;
    }
}

3月8:2834. 找出美丽数组的最小和

2834. 找出美丽数组的最小和 (opens in a new tab)

思路:贪心

本来用hashset解的,但是超内存限制,所以还是按数学方式贪心来做

class Solution {
    public int minimumPossibleSum(int n, int target) {
        // Set<Integer> beauty = new HashSet<>(2 * n);
        // int now = 1;
        // int count = 0;
        // int sum = 0;
        // while (count < n) {
        //     if (!beauty.contains(now) && !beauty.contains(target - now)) {
        //         beauty.add(now);
        //         count++;
        //         sum = (sum + now) % (int) (1e9 + 7);
        //     }
        //     now++;
        // }
        // return sum;
 
        long half = target / 2;
        if (n <= half) 
            return (int) ((long) ((1 + n) * n / 2) % (int) (1e9 + 7));
        else 
            return (int) ((long) (((1 + half) * half / 2) + (long) ((target + target + (n - half) - 1) * (n - half) / 2)) % (int) (1e9 + 7));
 
    }
}

3月10:299. 猜数字游戏

299. 猜数字游戏 (opens in a new tab)

思路:哈希

总共遍历3次:一次建立secret的哈希表,一次遍历计算bulls,一次遍历计算cows。

一定要先计算bulls,再计算cows,避免计算cows计算时把后面可能是bulls的地方先改了

class Solution {
    public String getHint(String secret, String guess) {
        int bulls = 0;
        int cows = 0;
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < secret.length(); i++) {
            int cur = secret.charAt(i) - '0';
            if (map.containsKey(cur)) {
                map.put(cur, map.get(cur) + 1);
            }
            else {
                map.put(cur, 1);
            }
        }
        for (int i = 0; i < guess.length(); i++) {
            int cur = guess.charAt(i) - '0';
            if (i < secret.length() && secret.charAt(i) == guess.charAt(i)) {
                bulls++;
                if (map.get(cur) > 1) map.put(cur, map.get(cur) - 1);
                else map.remove(cur);
            }
        }
        for (int i = 0; i < guess.length(); i++) {
            int cur = guess.charAt(i) - '0';
            if (i < secret.length() && secret.charAt(i) == guess.charAt(i)) {
            }
            else {
                if (map.containsKey(cur)) {
                    cows++;
                    if (map.get(cur) > 1) map.put(cur, map.get(cur) - 1);
                    else map.remove(cur);
                }
            }
        }
        return "" + bulls + "A" + cows + "B";
    }
}

3月15:2312. 卖木头块

2312. 卖木头块 (opens in a new tab)

思路:递归+记忆化搜索

class Solution {
    private long[][] dp;
    private int[][] priceM;
    public long sellingWood(int m, int n, int[][] prices) {
        // dp[m][n]表示大小为m,n的木块的最大钱数
        // 初始化
        dp = new long[m + 1][n + 1];
        for (long[] row : dp) {
            Arrays.fill(row, -1L);
        }
        
        // priceM[i][j] 表示长i宽j的木块的价格
        priceM = new int[m + 1][n + 1];
        for (int[] price : prices) {
            priceM[price[0]][price[1]] = price[2];
        }
        return recu(m, n);
    }
 
    private long recu(int m, int n) {
        if (m == 0 || n == 0) return 0L;
        if (dp[m][n] != -1) {
            return dp[m][n];
        }
        long max = 0L;
        if (priceM[m][n] != 0) max = (long) priceM[m][n];
        // 可横切
        for (int i = 1; i < m; i++) {
            long cur_max = recu(i, n) + recu(m - i, n);
            if (cur_max > max) max = cur_max;
        }
        // 可竖切
        for (int j = 1; j < n; j++) {
            long cur_max = recu(m, j) + recu(m, n - j);
            if (cur_max > max) max = cur_max;
        }
        dp[m][n] = max;
        return max;
    }
}

3月16:310. 最小高度树

310. 最小高度树 (opens in a new tab)

自己解法

思路1:两次深搜,找最长路径,其中点即为答案(可能有1个或2个)

注意n*n的邻接矩阵在面临n较大的稀疏矩阵时会超内存,建议使用邻接表

class Solution {
    private List<Integer> path = new ArrayList();
    private int max_height= 0;
    private int farest_node = 0;
    List<Integer>[] matrix;
    boolean[] visited;
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        matrix = new List[n];
        visited = new boolean[n];
        List<Integer> ans = new ArrayList();
        for (int i = 0; i < n; i++) {
            matrix[i] = new ArrayList<Integer>();
        }
        for (int i = 0; i < edges.length; i++) {
            int u = edges[i][0];
            int v = edges[i][1];
            matrix[u].add(v);
            matrix[v].add(u);
        }
        dfs(n, 0, 1);
        int endpoint1 = farest_node;
        Arrays.fill(visited, false);
        max_height = 0;
        farest_node = 0;
        dfs(n, endpoint1, 0);
        int endpoint2 = farest_node;
        Arrays.fill(visited, false);
        max_height = 0;
        farest_node = 0;
 
        dfsPath(n, endpoint1, endpoint2);
 
        if (path.size() % 2 == 0) {
            ans.add(path.get(path.size() / 2));
            ans.add(path.get(path.size() / 2 - 1));
        }
        else {
            ans.add(path.get(path.size() / 2));
        }
        return ans;
    }
 
    private boolean dfsPath(int n, int start, int end) {
        // System.out.println(start);
        visited[start] = true;
        path.add(start);
        if (start == end) {
            return true;
        }
        for (int i = 0; i < matrix[start].size(); i++) {
            if (!visited[matrix[start].get(i)]) {
                if (!dfsPath(n, matrix[start].get(i), end)) {
                    path.remove(path.size() - 1);
                }
                else return true;
            }
        }
        return false;
    }
 
    private void dfs(int n, int root, int height) {
        visited[root] = true;
        if (height > max_height) {
            max_height = height;
            farest_node = root;
        }
        for (int i = 0; i < matrix[root].size(); i++) {
            if (!visited[matrix[root].get(i)]) {
                dfs(n, matrix[root].get(i), height + 1);
            }
        }
        return;
    }
}

题解解法

思路2:拓扑排序

  • 首先找到所有度为 1 的节点压入队列,此时令节点剩余计数 remainNodes=n

  • 将当前 remainNodes 计数减去度为 1 的节点数目,将最外层的度为 1 的叶子节点取出, 并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 的节点压入队列中;

  • 重复上述步骤,直到剩余的节点数组 remainNodes≤2 时,此时剩余的节点即为当前高度最小树的根节点。

class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer>[] matrix = new List[n];
        int[] degree = new int[n];
        List<Integer> ans = new ArrayList();
        if (n == 1) {
            ans.add(0);
            return ans;
        }
        for (int i = 0; i < n; i++) {
            matrix[i] = new ArrayList<Integer>();
        }
        for (int i = 0; i < edges.length; i++) {
            int u = edges[i][0];
            int v = edges[i][1];
            matrix[u].add(v);
            matrix[v].add(u);
            degree[u]++;
            degree[v]++;
        }
 
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (degree[i] == 1) {
                queue.offer(i);
            }
        }
        int remain = n;
        while (remain > 2) {
            int sz = queue.size();
            remain -= sz;
            for (int i = 0; i < sz; i++) {
                int curr = queue.poll();
                for (int v : matrix[curr]) {
                    degree[v]--;
                    if (degree[v] == 1) {
                        queue.offer(v);
                    }
                }
            }
        }
        while (!queue.isEmpty()) {
            ans.add(queue.poll());
        }
 
        return ans;
    }
}

3月19:1793. 好子数组的最大分数

1793. 好子数组的最大分数 (opens in a new tab)

思路:双指针,复杂度O(n)

从中间出发,向两边扩展:

  • 保证答案变大的尝试:

    • 如果左面的下一个数不小于当前区间最小的数,左指针左移(先判断指针是否越界)

    • 如果右面的下一个数不小于当前区间最小的数,右指针右移(先判断指针是否越界)

  • 不保证答案变大的尝试:经过上面的判断,左指针左边和右指针右边的数都小于当前区间最小的数,所以左右指针都可以左右移动(不越界的情况下)

    • 如果移动左指针,先记录一个答案,但不更新左指针

    • 如果移动右指针,先记录一个答案,但不更新右指针

    • 判断到底该移动哪个:此时才更新左或右指针

      • 如果左右指针都不越界,移动左右指针中大的那个(因为反正移动指针,区间就会变大,移动大的那个更可能会使答案变大)

      • 如果只有一个指针越界,移动另一个

      • 如果都越界,结束

class Solution {
    public int maximumScore(int[] nums, int k) {
        int left  = k;
        int right = k;
        int ans = nums[k];
        int min = nums[k];
        while (left > 0 || right < nums.length - 1) {
            // System.out.println("==================================");
            // System.out.println("left: " + left + " right: " + right + " min: " + min);
            while (left > 0 && nums[left - 1] >= min) {
                left--;
            }
            ans = Math.max(ans, (right - left + 1) * min);
            // System.out.println("right: " + right + ", left: " + left + " min: " + min + " ans: " + ans);
            while (right < nums.length - 1 && nums[right + 1] >= min) {
                right++;
            }
            ans = Math.max(ans, (right - left + 1) * min);
            // System.out.println("right: " + right + ", left: " + left + " min: " + min + " ans: " + ans);
            if (left > 0) {
                // left--;
                ans = Math.max(ans, (right - left + 2) * nums[left - 1]);
                // System.out.println("right: " + right + ", left: " + (left - 1) + " min: " + nums[left - 1] + " ans: " + ans);
            }
            if (right < nums.length - 1) {
                // right++;
                ans = Math.max(ans, (right - left + 2) * nums[right + 1]);
                // System.out.println("right: " + (right + 1) + ", left: " + left + " min: " + nums[right + 1] + " ans: " + ans);
            }
            if (left > 0 && right < nums.length - 1) {
                if (nums[left - 1] < nums[right + 1]) {
                    min = nums[right + 1];
                    right++;
                } else {
                    min = nums[left - 1];
                    left--;
                }
            } else if (left > 0) {
                min = nums[left - 1];
                left--;
            } else if (right < nums.length - 1) {
                min = nums[right + 1];
                right++;
            } else {
                break;
            }
            // System.out.println("left: " + left + " right: " + right + " min: " + min);
            // System.out.println("==================================");
        }
        return ans;
    }
}

3月20:1969. 数组元素的最小非零乘积

1969. 数组元素的最小非零乘积 (opens in a new tab)

思路:数学+快速幂

class Solution {
    private final long MOD = (long) 1e9 + 7;
    public int minNonZeroProduct(int p) {
        if (p == 1) return 1;
        long countOne = 1L;
        // 每一位上1的数量
        countOne = countOne << (p - 1);
        // 每一位上0的数量
        long countZero = countOne - 1;
 
        // 因数和幂次
        long factor = (1L << p) - 2;
        long power = countZero;
 
        // 最后一个要乘的数
        long pow = quickPow(factor, power);
        long last = (1L << p) - 1;
 
        int resu = (int) (((pow % MOD) * (last % MOD)) % MOD);
 
        return resu;
    }
 
    private long quickPow(long factor, long power) {
        if (power == 0) return 1;
        if (power == 1) return factor;
        long part = quickPow(factor, power / 2);
        if (power % 2 == 1) {
            return (((part % MOD) * (part % MOD)) % MOD * (factor % MOD)) % MOD;
        }
        else {
            return ((part % MOD) * (part % MOD)) % MOD;
        }
    }
}

3月27:2580. 统计将重叠区间合并成组的方案数

2580. 统计将重叠区间合并成组的方案数 (opens in a new tab)

思路:贪心+数学(快速幂)

先排序,排序后贪心合并,然后计算方案数。

计算方案数:合并后的每个区间可以出现在组1也可以出现在组2,所以是2的区间数次方。别傻乎乎一个一个算组合数

class Solution {
    public int countWays(int[][] ranges) {
        if (ranges.length == 1) return 2;
        // 计算不重叠区间数量(类似合并区间)
        Arrays.sort(ranges, (a, b) -> {
            if(a[0] == b[0]) return Integer.compare(a[1], b[1]);
            else return Integer.compare(a[0], b[0]);
        });
        List<Integer> counts = new ArrayList<>();
        int right = ranges[0][1];
        int count = 1;
        for (int i = 1; i < ranges.length; i++) {
            if(ranges[i][0] > right) {
                counts.add(count);
                count = 1;
                right = ranges[i][1];
                if (i == ranges.length - 1) counts.add(count);
            }
            else {
                count++;
                right = Math.max(right, ranges[i][1]);
                if (i == ranges.length - 1) counts.add(count);
            }
        }
        return (int) quickPow(2, counts.size());
    }
 
    private final long MOD = (long) 1e9 + 7;
    public long quickPow(long factor, long power) {
        if (power == 0) return 1;
        if (power == 1) return factor;
        long part = quickPow(factor, power / 2);
        if (power % 2 == 1) {
            return (((part % MOD) * (part % MOD)) % MOD * (factor % MOD)) % MOD;
        }
        else {
            return ((part % MOD) * (part % MOD)) % MOD;
        }
    }
}

3月30:2952. 需要添加的硬币的最小数量

2952. 需要添加的硬币的最小数量 (opens in a new tab)

数组可重复、取硬币不可重复

思路:贪心

如果[0,getable]的数值是可以得到的,且getable+1的数在数组中,那么[0,getable*2+1]的数值也是可以得到的。

为了实现贪心,要先对数组排序,从最小硬币开始:

  • 如果当前硬币小于getable+1,那么getable = Math.max(coins[coinIndex] * 2 - 1, getable + coins[coinIndex]);. 含义是:当前硬币是没有用过的,如果当前数值小于等于getable,那么getable可以扩展到当前硬币的两倍减一,也可以扩展到添加当前硬币。

  • 如果当前硬币等于getable+1,那么getable可以扩展到当前硬币的两倍减一。

  • 如果当前硬币大于getable+1,那么需要添加一个硬币,其值为getable+1。

class Solution {
    public int minimumAddedCoins(int[] coins, int target) {
        Arrays.sort(coins);
        int ans = 0;
        int getable = 0;
        int coinIndex = 0;
        while (getable < target) {
            System.out.println(getable);
            while (coinIndex < coins.length && coins[coinIndex] < getable + 1) {
                getable = Math.max(coins[coinIndex] * 2 - 1, getable + coins[coinIndex]);
                coinIndex++;
            }
            // 注意额外判断一次跳出
            if (getable >= target) break;
            if (coinIndex < coins.length && getable + 1 == coins[coinIndex]) {
                getable = coins[coinIndex] * 2 - 1;
                coinIndex++;
            }
            else {
                ans++;
                System.out.println("add: " + (getable + 1));
                getable = getable * 2 + 1;
            }
        }
        return ans;
    }
}

4月2:894. 所有可能的真二叉树

894. 所有可能的真二叉树 (opens in a new tab)

思路:递归+记忆化搜索

非常考验对递归的理解,原来想的是到达叶子后把树从根节点拷贝一份,然后回溯,但是这样会导致重复计算,而且实际上一边到叶子时另一边不一定到叶子,所以不可行。

实际上在递归的时候就可以把左右子树的所有可能都计算出来,然后再组合。

组合是重点!!!

除此之外可以考虑用记忆化搜索,但是题目比较小,也无所谓了。

下面写了两种解法,一种是记忆化搜索,一种是普通递归。稍微统计了一下运行时间和访问递归次数,普通递归效率明显低得多。

public class Solution {
 
    public static void main(String[] args) throws Exception {
        try {
            Solution s = new Solution();
            int n = 25;
            long startTime = System.currentTimeMillis();
            s.allPossibleFBT(n);
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime; // Compute the elapsed time in milliseconds.
            System.out.println("Execution time: " + duration + " milliseconds");
            System.out.println("count: " + s.count);
 
            long startTime2 = System.currentTimeMillis();
            s.allPossibleFBT2(n);
            long endTime2 = System.currentTimeMillis();
            long duration2 = endTime2 - startTime2; // Compute the elapsed time in milliseconds.
            System.out.println("Execution time: " + duration2 + " milliseconds");
            System.out.println("count2: " + s.count2);
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
    private int count2 = 0;
    public List<TreeNode> allPossibleFBT2(int n) {
        count2++;
        List<TreeNode> possibles = new ArrayList<TreeNode>();
        if (n % 2 == 0) return possibles;
        if (n == 1) {
            possibles.add(new TreeNode(0));
            return possibles;
        }
        n -= 1;
        for (int i = 1; i <= n; i += 2) {
            List<TreeNode> leftPossibles = allPossibleFBT2(i);
            List<TreeNode> rightPossibles = allPossibleFBT2(n - i);
            for (TreeNode left : leftPossibles) {
                for (TreeNode right : rightPossibles) {
                    possibles.add(new TreeNode(0, left, right));
                }
            }
        }
        return possibles;
    }
 
    private int count = 0;
    private List<List<TreeNode>> global_possibles = new ArrayList<>();
    public List<TreeNode> allPossibleFBT(int n) {
        for (int i = 0; i <= n; i++) {
            global_possibles.add(null);
        }
        List<TreeNode> resu = invert(n);
        return resu;
    }
 
    private List<TreeNode> invert (int n) {
        if (global_possibles.get(n) != null) return global_possibles.get(n);
        count++;
        List<TreeNode> possibles = new ArrayList<TreeNode>();
        if (n % 2 == 0) return possibles;
        if (n == 1) {
            possibles.add(new TreeNode(0));
            global_possibles.set(n, possibles);
            return possibles;
        }
        n -= 1;
        for (int i = 1; i <= n; i += 2) {
            if (global_possibles.get(i) == null) global_possibles.set(i, invert(i));
            List<TreeNode> leftPossibles = global_possibles.get(i);
            if (global_possibles.get(n - i) == null) global_possibles.set(n - i, invert(n - i));
            List<TreeNode> rightPossibles = global_possibles.get(n - i);
            for (TreeNode left : leftPossibles) {
                for (TreeNode right : rightPossibles) {
                    possibles.add(new TreeNode(0, left, right));
                }
            }
        }
        return possibles;
    }
}
// n = 25时:
// 记忆化
Execution time: 11 milliseconds
count: 13
// 普通
Execution time: 85 milliseconds
count2: 531441