当前位置: 首页 > article >正文

hot100(4)

31.437. 路径总和 III - 力扣(LeetCode)

方法一:递归、dfs

由树的结构想到使用递归解决,且路径相关问题容易考虑到使用dfs和bfs.

使用dfs解决,这里关键在于如何设计递归函数。

递归函数的参数:root,targetSum

递归函数的返回值:以root为根节点,路径和为targetSum的数量

递归函数的单层逻辑:

(1)判断返回条件,root == null,return 0

(2)计算当层结点路径数,需要递归调用得到左节点pathSum(root.left,targetSum - val) 右节点 pathSum(root.right,targetSum - val) 的结果,返回二者之和

rootSum:以当前节点为根节点,路径和等于targetSum的路径数

pathSum:root数中所有路径和等于targetSum的路径数

这种暴力dfs解法的关键之处在于理解需要嵌套两层暴力遍历

public int pathSum(TreeNode root, long targetSum) {
        if(root ==  null) return 0;
        int ret = rootSum(root,targetSum);
        ret += pathSum(root.left,targetSum);
        ret += pathSum(root.right, targetSum);
        return ret;
    }

    public int rootSum(TreeNode root, long targetSum){
        if(root == null) return 0;
        int ret = 0 ;
        if(root.val == targetSum){
            ret += 1;
        }
        ret += rootSum(root.left,targetSum - root.val);
        ret += rootSum(root.right, targetSum - root.val);
        return ret;
    }

复杂度分析:对于每一个节点,求以该节点为起点的路径数目时,都要遍历以该节点为根节点的子树的所有节点,最大时间为O(N);对每一个节点都要进行这样的操作,节点数为N,因此时间复杂度为O(N^2),空间复杂度O(N)

方法二:前缀和 优化方法一

方法一中对每个节点都进行了以该节点为根节点的dfs,这些dfs的很多计算是重复的,可以使用更加合理的计算顺序避免这些重复。

定义前缀和:根节点到当前节点的路径上,除了当前节点以外的所有节点的和

Map<Long,Integer> prefix;
    public int pathSum(TreeNode root, long targetSum) {
        prefix = new HashMap<>();
        prefix.put(0L,1);
        return dfs(root,targetSum,0);
    }

    public int dfs(TreeNode root, long targetSum, long curr){
        if(root == null) return 0;
        int res = 0;
        curr += root.val;
        res += prefix.getOrDefault(curr - targetSum, 0);
        prefix.put( curr,prefix.getOrDefault(curr,0) + 1);
        res += dfs(root.left, targetSum,curr);
        res += dfs(root.right, targetSum,curr);
        prefix.put(curr,prefix.getOrDefault(curr,0) - 1);
        return res;
    }

32.416. 分割等和子集 - 力扣(LeetCode)

回溯会超时,考虑动态规划。

从背包问题的角度看本题的本质:能否把sum/2的背包装满。

重量和价值都是数字大小,判断能否把sum/2的背包装满即判断背包价值最大是否是sum/2。

用01背包解决这个问题:

(1)dp[j]:容量为j的背包所能装入的最大价值为dp[j]

(2)dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]

(3)dp[0] = 0;

(4)先物品,后背包,背包容量逆序

(5)模拟

public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if(sum % 2 == 1) return false;
        int target = sum/2;
        int[] dp = new int[target + 1];
        for(int i = 0 ; i < nums.length ; i++){
            for(int j = target ; j >= nums[i] ; j--){
                dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[target] == target;
    }

33.406. 根据身高重建队列 - 力扣(LeetCode)

题目描述:整数对 (h, k) 表示,其中 h 是这个人的身高,k 是排在这个人前面且身高大于或等于 h 的人数。

一般这种数对,且涉及排序的,根据第一个元素降序排序,根据第二个元素升序排序;或根据第一个元素升序排序,根据第二个元素降序排序,往往能够简化解题过程。

具体到这一道题,我们对身高降序排序,让身高大的排在前面;如果身高相同,则对k升序排序,让k小的排在前面。这样是为了先处理身高大的人,在这道题中,先处理身高大的是为了确保在插入过程中,已经排好位置的人能够满足条件。

关键点:

  • 每个人的条件是:k个比他高的人已经排在他前面。
  • 即处理好身高大的人的k值,这之后对身高小的人的插入是不影响身高大的人的k值的。(可以看成是每个人只能看到比自己高的人,身高大的人看不到身高小的人)

举例排序:

public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people,(a,b)->{
            if(a[0] == b[0]){
                return a[1] - b[1];
            }else{
                return b[0] - a[0];
            }
        });
        List<int[]> list = new ArrayList<>();
        for (int[] person : people) {
            list.add(person[1],person);
        }
        return  list.toArray(new int[people.length][]);
    }

34.399. 除法求值 - 力扣(LeetCode)

根据本题的描述,容易考虑到使用图论的方法解决。即是把value考虑成边的权重,var考虑成图的结点。

根据equations和values建好图,从queries中得到from 和 dest,然后在图中进行bfs,bfs的时候要记录from到么每一个节点的ratios(可以考虑成本题中特殊的距离,为路径权重乘积)。

这里bfs的时候没有用到visited数组,因为题目中明确权重>0,因此只要判断ratios[i]是否小于0即可得到是否来过。

public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int nvar = 0;
        Map<String,Integer> index = new HashMap<>();
        for (List<String> equation : equations) {
            if(!index.containsKey(equation.get(0))){
                index.put(equation.get(0),nvar++);
            }
            if(!index.containsKey(equation.get(1))){
                index.put(equation.get(1),nvar++);
            }
        }
        List<List<Pair>> graph = new ArrayList<>();
        for(int i = 0 ; i < nvar ; i++){
            graph.add(new ArrayList<>());
        }
        for(int i = 0 ; i < equations.size() ; i++){
            String sva = equations.get(i).get(0);
            int va = index.get(sva);
            String svb = equations.get(i).get(1);
            int vb = index.get(svb);
            graph.get(va).add(new Pair(vb,values[i]));
            graph.get(vb).add(new Pair(va,1.0/values[i]));
        }
        double[] res = new double[queries.size()];
        for(int i = 0 ; i < queries.size() ; i++){
            Queue<Integer> queue = new ArrayDeque<>();
            String sfrom = queries.get(i).get(0);
            String sdest = queries.get(i).get(1);
            double result = -1.0;
            if(index.containsKey(sfrom) && index.containsKey(sdest)){
                if(sfrom.equals(sdest)){
                    result = 1.0;
                }else{
                    int from = index.get(sfrom);
                    int dest = index.get(sdest);
                    queue.offer(from);
                    double[] ratios = new double[nvar];
                    Arrays.fill(ratios,-1.0);
                    ratios[from] = 1.0;
                    while(!queue.isEmpty() && ratios[dest] < 0){
                        int node = queue.poll();
                        List<Pair> neighbors = graph.get(node);
                        for (Pair neighbor : neighbors) {
                            if(ratios[neighbor.index] < 0){
                                ratios[neighbor.index] = ratios[node] * neighbor.value;
                                queue.offer(neighbor.index);
                            }
                        }
                    }
                    result = ratios[dest];
                }
            }
            res[i] = result;
        }
        return res;

    }

    class Pair{
        int index;
        double value;
        Pair(){}
        Pair(int neighbor, double value){
            this.index = neighbor;
            this.value = value;
        }
    }

方法II 并查集

这里回顾一下并查集和使用场景和使用方法

通常来说,并查集用来解决连通性问题,即判断两个元素是否在同一个集合里的时候,要想到用并查集,通常用于图论里的连通性判断。

并查集里的集合用一维数组来表示,如A,B,C在一个集合里,即father[A] = B,

father[B] = C  father[C] = C.

并查集里的路径压缩:在递归find的过程中,让father[u]接住 find(father[u])的返回结果。

即:

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u;
    else return father[u] = find(father[u]); // 路径压缩
}

完整的并查集Java模板如下:

public class UnionFind {
    private int[] parent;
    
    public UnionFind(int size) {
        parent = new int[size];
        init();
    }
    
    // 并查集初始化
    private void init() {
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
        }
    }
    
    // 并查集里寻根的过程
    public int find(int u) {
        if (u != parent[u]) {
            parent[u] = find(parent[u]); // 路径压缩
        }
        return parent[u];
    }
    
    // 判断 u 和 v 是否找到同一个根
    public boolean isSame(int u, int v) {
        return find(u) == find(v);
    }
    
    // 将 v -> u 这条边加入并查集
    public void join(int u, int v) {
        int rootU = find(u); // 寻找 u 的根
        int rootV = find(v); // 寻找 v 的根
        if (rootU != rootV) { // 如果根不同,合并两个集合
            parent[rootV] = rootU;
        }
    }
    
    public static void main(String[] args) {
        int n = 1005; // 根据实际情况调整
        UnionFind uf = new UnionFind(n);
        
        // 示例: 加入一些连接
        uf.join(1, 2);
        uf.join(2, 3);
        
        // 检查是否在同一个集合
        System.out.println(uf.isSame(1, 3)); // 输出 true
        System.out.println(uf.isSame(1, 4)); // 输出 false
    }
}

在本题中,要求的是变量之间的关系,并且变量之间的倍数关系具有传递性,处理具有传递性的问题,可以使用并查集。我们需要再并查集的 合并 与 查询 操作中维护这些变量之间的倍数关系

class Solution {
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        Map<String,Integer> map = new HashMap<>(equations.size()*2);
        UnionFind uf = new UnionFind(equations.size()*2);
        int nvar = 0;
        for (List<String> equation : equations) {
            String svar1 = equation.get(0);
            String svar2 = equation.get(1);
            if(!map.containsKey(svar1)){
                map.put(svar1,nvar++);
            }
            if(!map.containsKey(svar2)){
                map.put(svar2,nvar++);
            }
        }
        for(int i = 0 ; i < equations.size() ; i++){
            int var1 = map.get(equations.get(i).get(0));
            int var2 = map.get(equations.get(i).get(1));
            double value = values[i];
            uf.union(var1,var2,value);
        }
        double[] res = new double[queries.size()];
        for(int i = 0 ; i < queries.size() ; i++){
            int var1 = map.getOrDefault(queries.get(i).get(0),-1);
            int var2 = map.getOrDefault(queries.get(i).get(1),-1);
            if(var1 == -1 || var2 == -1){
                res[i] = -1.0d;
                continue;
            }
            res[i]  = uf.isConnected(var1,var2);
        }
        return res;
    }
    class UnionFind{
        int[] parent;
        double[] weight;
        UnionFind(int size) {
            parent = new int[size];
            weight = new double[size];
            Arrays.fill(weight,1.0);
            for(int i = 0 ; i < size ; i++){
                parent[i] = i;
            }
        }
        public void union(int x , int y , double value){
            int rootX = find(x);
            int rootY = find(y);
            if(rootX != rootY){
                parent[rootX] = rootY;
                weight[rootX] = weight[y] * value/weight[x];
            }
        }
        public int find(int x){
            if(x != parent[x]){
                int index = parent[x];
                parent[x] = find(parent[x]);
                weight[x] *= weight[index];
            }
            return parent[x];
        }
        public double isConnected(int x , int y){
            int rootX = find(x);
            int rootY = find(y);
            if(rootX != rootY){
                return -1.0d;
            }else{
                return weight[x]/weight[y];
            }
        }
    }
}

35.394. 字符串解码 - 力扣(LeetCode)

字符串问题,括号问题,使用栈或递归解决

class Solution {
    public String decodeString(String s) {
        Deque<Integer> stack_multi = new ArrayDeque<>();
        Deque<String> stack_string = new ArrayDeque<>();
        int multi = 0;
        StringBuilder res = new StringBuilder();
        for(int i = 0 ; i < s.length() ; i++){
            if(s.charAt(i) == '['){
                stack_multi.push(multi);
                multi = 0;
                stack_string.push(res.toString());
                res = new StringBuilder();
            }else if(s.charAt(i) == ']'){
                StringBuilder tmp = new StringBuilder();
                int curr_multi = stack_multi.pop();
                for(int j = 0 ; j < curr_multi ; j++){
                    tmp.append(res);
                }
                res = new StringBuilder(stack_string.pop() + tmp);
            }else if(s.charAt(i) >= '0' && s.charAt(i) <= '9'){
                multi = multi * 10 + Integer.parseInt(String.valueOf(s.charAt(i)));
            }else{
                res.append(s.charAt(i));
            }
        }
        return res.toString();

    }
}

36.347. 前 K 个高频元素 - 力扣(LeetCode)

I 暴力比较

要使用Arrays.sort比较自定义的数据结构的话,需要实现Comparator<>接口

public int[] topKFrequent(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num,map.getOrDefault(num,0)+1);
        }
        Pair[] pairs = new Pair[map.entrySet().size()];
        int i = 0;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            pairs[i++] = new Pair(entry.getValue(),entry.getKey());
        }
        Arrays.sort(pairs);
        int[] res = new int[k];
        for(int j = 0 ; j < k ; j++){
            res[j] = pairs[j].value;
        }
        return res;

    }
    class Pair implements Comparable<Pair>{
        int num;
        int value;
        Pair(){}
        Pair(int num, int value) {
            this.num = num;
            this.value = value;
        }

        @Override
        public int compareTo(Pair o) {
            return Integer.compare(o.num,this.num);
        }
    }

II 小根堆 PriorityQueue

public int[] topKFrequent(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num,map.getOrDefault(num,0) + 1);
        }
        PriorityQueue<int[]> heap = new PriorityQueue<>((pair1,pair2)->pair1[1] - pair2[1]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if(heap.size() < k){
                heap.add(new int[]{ entry.getKey(),entry.getValue()});
            }else{
                if(heap.peek()[1] < entry.getValue()){
                    heap.poll();
                    heap.add(new int[]{entry.getKey(),entry.getValue()});
                }
            }
        }
        int[] res = new int[k];
        for(int i = 0 ; i < k ; i++){
            res[i] = heap.poll()[0];
        }
        return res;
    }

37.338. 比特位计数 - 力扣(LeetCode)

public int[] countBits(int n) {
        int[] res = new int[n+1];
        for(int i = 0 ; i <= n ; i++){
            res[i] = Integer.bitCount(i);
        }
        return res;
    }

38.337. 打家劫舍 III - 力扣(LeetCode)

本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算

与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。

如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”

方法I 后序遍历,会时间超限

public int rob(TreeNode root) {
        if(root == null) return 0;
        if(root.left == null && root.right == null){
            return root.val;
        }
        //不考虑抢劫root
        int val1 = rob(root.left) + rob(root.right);
        //考虑抢劫root
        int val2 = root.val;
        if(root.left != null) val2  = val2 + rob(root.left.left) + rob(root.left.right);
        if(root.right != null) val2 = val2 + rob(root.right.left) + rob(root.right.right);
        return Math.max(val1,val2);

    }

II 记忆化递推

把当前节点的计算结果存储到map中,如果计算过了,则直接返回,避免重复计算

Map<TreeNode,Integer> map = new HashMap<>();
    public int rob(TreeNode root) {
        if(root == null) return 0;
        if(root.left == null && root.right == null){
            return root.val;
        }
        if(map.containsKey(root)) return map.get(root);
        //不考虑抢劫root
        int val1 = rob(root.left) + rob(root.right);
        //考虑抢劫root
        int val2 = root.val;
        if(root.left != null) val2  = val2 + rob(root.left.left) + rob(root.left.right);
        if(root.right != null) val2 = val2 + rob(root.right.left) + rob(root.right.right);
        map.put(root,Math.max(val1,val2));
        return Math.max(val1,val2);

    }

III 动态规划

在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。

动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

之前用的状态转移容器是数组,这里选择的状态转移容器也是int[]数组,只是结合了树这种数据结构,并根据树形结构的特征结合了递归。

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,下面以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

1.确定递归函数的参数和返回值

要维护一个结点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

dp数组及下标含义:dp[0] : 不偷该节点所得到的最大金钱,dp[1]:偷该节点所得到的最大金钱。

2.确定递归终止条件

if(root == null) return new int[]{0,0};

也相当于dp数组初始化

3.确定遍历顺序

使用后序遍历,因为需要递归函数的返回值做下一步计算。

4.确定单层递归的逻辑

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

5.举例推导dp数组

- 时间复杂度O(n),每个节点只遍历了一次

- 空间复杂度O(logn),递推系统栈的空间

public int rob(TreeNode root) {
        int[] res = robTree(root);
        return Math.max(res[0],res[1]);
    }

    public int[] robTree(TreeNode root){
        if(root == null) return new int[]{0,0};
        int[] left = robTree(root.left);
        int[] right = robTree(root.right);
        //不偷根节点
        int val1 = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
        //偷根节点
        int val2 = root.val + left[0] + right[0];
        return new int[]{val1,val2};
    }

39.121. 买卖股票的最佳时机 - 力扣(LeetCode)

本题限制性要求在于只能买卖股票一次

I贪心

由于只能买卖一次,因此贪心算法选择遍历一次,在遍历的过程中记录每个数字左侧的最小值,与当前能赚到的差价作比较,遍历一次得到最大的差价,即最大的利润。

public int maxProfit(int[] prices) {
        int low = Integer.MAX_VALUE;
        int res = 0;
        for(int i = 0 ; i < prices.length ; i++){
            low = Math.min(low,prices[i]);
            res = Math.max(res,prices[i] - low);
        }
        return res;
    }

II动态规划

本题中的状态有持有股票和不持有股票两种

1.dp数组及下标的含义

dp[i][0] : 第i天持有股票所得最多现金

dp[i][1] : 第i天不持有股票所得最多现金

2.递推公式

dp[i][0] = Math.max(dp[i-1][0] , dp[i-1][1] - prices[i]);

dp[i][1] = Math.max(dp[i-1][0] + prices[i] , dp[i-1][1]);

3.初始化

dp[0][0] = - prices[0];

dp[0][1] = 0;

4.遍历顺序

从前向后

5.举例推导dp数组

时间复杂度O(n)

空间复杂度O(n),使用滚动数组可以优化到O(1)

public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i = 1 ; i < prices.length ; i++){
            dp[i][0] = Math.max(dp[i-1][0], - prices[i]);
            dp[i][1] = Math.max(dp[i-1][0] + prices[i] ,dp[i-1][1]);
        }
        return dp[prices.length - 1][1];
    }

40.312. 戳气球 - 力扣(LeetCode)

区间dp问题,详细生动题解:312. 戳气球 题解- 力扣(LeetCode)

1.dp数组及下标含义

dp[i][j]:开区间(i,j),所能得到的最大硬币数

2.状态转移方程

for k (k > i && k < j )

dp[i][j] = max{dp[i][k] + dp[k][j] + val[i] * val[k] * val[j]}

3.初始化

为了方便处理,nums前后加上边界1,题目中称边界视为1,这样可以避开对边界的讨论

4.遍历顺序

遍历长度length 从 2 到哦 list.size()

5.dp模拟

public int maxCoins(int[] nums) {
        List<Integer> list = new ArrayList<>();
        for (int num : nums) {
            list.add(num);
        }
        list.add(0,1);
        list.add(list.size(),1);
        int[][] dp = new int[list.size()][list.size()];
        for(int length = 1 ; length <= nums.length; length++){
            for(int i = 0 ; i < list.size() - length - 1; i++){
                int j = i + length + 1;
                int maxCoins = 0;
                for(int k = i + 1 ; k < j ; k++){
                    if(dp[i][k] + dp[k][j] + list.get(i)*list.get(k)*list.get(j) > maxCoins){
                        maxCoins = dp[i][k] + dp[k][j] + list.get(i)*list.get(k)*list.get(j);
                    }
                }
                dp[i][j] = maxCoins;
            }
        }
        return dp[0][list.size()-1];
    }


http://www.kler.cn/a/527332.html

相关文章:

  • 对比DeepSeek、ChatGPT和Kimi的学术写作关键词提取能力
  • Baklib推动企业知识管理创新与效率提升的全面探讨
  • 计算机网络 性能指标相关
  • Python——基本数据类型——字符串类型
  • 代码随想录刷题day20|(哈希表篇)15.三数之和
  • 机器学习6-全连接神经网络2
  • 基于改进的强跟踪技术的扩展Consider Kalman滤波算法在无人机导航系统中的应用研究
  • 使用 Ollama 和 Kibana 在本地为 RAG 测试 DeepSeek R1
  • LeetCode 0541.反转字符串 II:模拟
  • C# 数组和列表的基本知识及 LINQ 查询
  • Spring Boot 基础开发:实现 RESTful API 开发
  • 【算法设计与分析】实验4:动态规划—0/1背包问题
  • Baklib赋能企业实现高效数字化内容管理提升竞争力
  • 【基于SprintBoot+Mybatis+Mysql】电脑商城项目之用户注册
  • 第05章 16 Implicit Function应用举例
  • 【蓝桥杯】43697.机器人塔
  • origin如何在已经画好的图上修改数据且不改变原图像的画风和格式
  • 知识库管理如何推动企业数字化转型与创新发展的深层次探索
  • 智联出行公司布局中国市场,引领绿色出行新潮流
  • riscv xv6学习笔记