leetcode刷题记录day028:494和133

494、难度中等:

思路:
数组中有 n 项就有一共有 2^n 种可能(每个元素可能为正可能为负共两种情况,共 n 个元素)
(因为0 <= nums[i] <= 1000)所以我们只能暴力遍历所有可能性
遍历所有情况时,要确保每次遍历出的情况都不相同,求各自的值。当 n 个元素都添加符号之后,即得到一种表达式,只要与target相同就计数+1。
本题属于背包问题:以下链接(力扣上的背包问题)从评论区中摘选:这一块建议直接去官方题解的评论区中查看
01背包:416. 分割等和子集 474. 一和零 494. 目标和 879. 盈利计划 1049. 最后一块石头的重量 II 1230. 抛掷硬币
完全背包:1449. 数位成本和为目标值的最大数字 322. 零钱兑换 518. 零钱兑换 II 279. 完全平方数

方法一:回溯:时间复杂度:O(2^n) 空间复杂度:O(n)

原理:先大致看完下方代码,再看代码解析
如何实现的元素符号是 + 是 -:设计一个存储总和的变量,每遍历一个元素就分出两种情况 +、- 该元素。

class Solution {
    int count = 0;
	
    public int findTargetSumWays(int[] nums, int target) {
        // 此时有0个元素被赋予了正负号,累加结果为0
        backtrack(nums, target, 0, 0);
        return count;
    }
    // 回溯:参数 index 代表当前已被添加符号的元素位置,若等于数组长度说明得到了一种表达式
    // 参数 sum 是到下标 index 为止所有元素的运算结果
    // 只有当 index 等于数组长度且 sum 等于 target 时才能计数 +1
    public void backtrack(int[] nums, int target, int index, int sum) {
        if (index == nums.length) {
            if (sum == target) {
                count++;
            }
        } else {
            // 不满足计数情况分为两种可能:index 等于数组长度或还不等于
            backtrack(nums, target, index + 1, sum + nums[index]);
            backtrack(nums, target, index + 1, sum - nums[index]);
        }
    }
}

代码解析:由于计数变量 count 是全局的,所以我们不需要回溯方法有 return,在回溯方法里进行 count 的值改变即可。
因此,我们在回溯方法中的 if (index == nums.length) 里即便不满足 if (sum == target) 也可以不用再在下方写别的。
具体原因:我们先传入参数 (nums, target, 0, 0) 此时不满足 if,直接再推算出两种可能性:总和加减当前元素。
如果出现 index 为数组长度但 sum 不等于 target 的情况,也无需当心,因为这一层只是上一层 else 里两种可能性的一种,这层情况不行就直接结束。若上一层的两种情况都不行,也无需担心,因为上一层也是上上一层两种可能性的一种。

方法二:动态规划:时间复杂度:O(n(sum−target)) 空间复杂度:O(sum−target)

记数组的元素和为 sum,添加 - 号的元素之和为 neg,则其余添加 + 的元素之和为 sum−neg,得到的表达式的结果为 (sumneg)−neg=sum−2⋅neg=target。即 neg = (sum - target) / 2
在这里插入图片描述

对我们的要求变化了:数组中选取哪些元素为负其累加和(所有被选为 - 号的元素的累加值,但这个累加是元素在被添加 - 号前的累加和,也就是累加和是一堆正数的累加和)等于 neg,总共的选取方式有几种。

当可选元素为 0 个时(i 的取值是从 1 开始,i 表示在数组 nums 的前 i 个数中选取元素),若想让元素和 j(neg) = 0,那么就只有 1 种情况(不给任何元素添加 - 即可,符号为 - 的元素总和为 0);若想让元素和 j >= 1 则有 0 种情况(可选元素为 0,那根本不可能出现元素总和大于 1 的情况) 这个是作为循环前的起始条件,来给后续的每个 dp[i] [j] 赋上正确的值。
在这里插入图片描述

对下图的理解(这时代码里双层 for 循环的原理):遍历到当前下标为 i 的元素时,计算总和为 j 的可能性种类。若总和小于当前元素,那说明该元素无法被设置为 - 号(由于 j 是正数的累加和,若当前元素值已经比 j 还大了,那就说明把它加进去就出错了)。不被选这说明当前值不影响总值,所以方案总数 dp[i] [j] 是 dp[i-1] [j] 时的总数不变。
若总和大于当前元素,分为两种情况:
1、即便该元素可以被列为 - 号我们也仍不让其为 -,则方案数为 dp[i-1] [j];
2、将其列为 - 号,那就说明当前的总和 j 里有一部分是 nums[i]。所以在总和为 j 且选了元素 num[i] 为 - 号的前提下方案数 = 选取下标为 i-1 元素时总和为 j-nums[i] 的方案总数dp[i-1] [j-nums[i]](也就是当前元素nums[i]被选是必定的事,若这是事实那说明遍历到nums[i-1]时总和一定为 j-nums[i],所以这个情况的方案总数就是我们所需的总数) 而我们需要顾及到所有情况,所以 dp[i] [j] = dp[i-1] [j] + dp[i-1] [j-nums[i]]
注意点:由于我们的循环遍历是外层 for 循环 1 <= i <= n 加上内层 for 循环 0 <= j <= neg 两层,所以我们无需担心任何的 dp[i] [j] 没有对应的值。

在这里插入图片描述

// 代码实现,此时空间复杂度为O(n.neg)
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        // 获得所有元素累加和
        for (int num : nums) {
            sum += num;
        }
        // 符号为 + 的元素之和
        int diff = sum - target;
        // 若小于 0 或除 2 存在余数那就说明不存在选择方案
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        // 获取添加 - 号的元素之和 neg
        int n = nums.length, neg = diff / 2;
  	    // 创建二维数组并且为起始条件赋值 
        int[][] dp = new int[n + 1][neg + 1];
        // 由于创建一个二维数组后其所有的元素值都是默认的 0,
        // 所以我们无需给 i = 0 情况下的所有 dp[0][j] 赋值为 0。
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) {
                    dp[i][j] += dp[i - 1][j - num];
                }
            }
        }
        return dp[n][neg];
    }
}

由于 dp 的每一行的计算只和上一行有关(dp[i] [j] 的推算只从 dp[i−1] [j] 中获取),因此可以使用滚动数组的方式,去掉 dp 的第一个维度(变为一维数组),将空间复杂度优化到 O(neg)。
实现时,内层循环需采用倒序遍历的方式,这种方式保证转移来的是 dp[i−1] [] 中的元素值(因为我们推算 dp[i] [j] 的数据来源全是从 dp[i−1] [k],k < j 中获取,所以我们从后向前遍历,就可以保证 k<j 的值都是完好的,若从前向后,在遍历后面元素时前面的元素值已经变化了,后续值都会不准确)

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int neg = diff / 2;
        int[] dp = new int[neg + 1];
        dp[0] = 1;
        for (int num : nums) {
            for (int j = neg; j >= num; j--) {
                dp[j] += dp[j - num];
            }
        }
        return dp[neg];
    }
}

133、难度中等:

方法一:深度优先搜索:时空复杂度O(n)

原理:图的深拷贝:构建一张与原图结构,值均一样的图,但是其中的节点不再是原来图节点的引用
题目只给了我们一个节点的引用,因此为了知道整张图的结构以及对应节点的值,我们需要从给定的节点出发,进行「图的遍历」,并在遍历的过程中完成图的深拷贝
为了避免在深拷贝时陷入死循环,我们需要理解图的结构。对于一张无向图,任何给定的无向边都可以表示为两个有向边
结合本题给的Node类也就是:A与B节点相邻,那么A中具有到达B的途径,而B中也有到达A的途径。为防止从A到达B后又从B返回到A陷入死循环,我们需要用一种数据结构记录已经被克隆过的节点。使用一个哈希表存储所有已被访问和克隆的节点。哈希表中的 key 是原始图中的节点,value 是克隆图中的对应节点。

注意点:不要被题给示例 1 的节点顺序图吸引注意,我们真正要克隆的不是那张图,而是创造出一些节点,每个节点可以引向另外的两个节点,至于这另外的两个节点是什么,我们不需要去管去想,因为题目已经传给我们了,我们只需要将其调出来添加进我们自己的节点里即可。整个过程中只需要避免陷入死循环即可。
(不需要顾及1、2、3…的顺序,只需要深拷贝出题给节点 1,将其称作 one,然后根据 1 能引向的另两个节点,深拷贝二者并放入 one 的neighbors中,这个过程通过传入另两个节点作为参数来调用方法 cloneGraph 自己,因为参数本身也是一个节点,所以调用该方法时也会把参数的neighbors深拷贝出,这样逐渐地就把所有节点都拷贝完了。为了防止陷入死循环,我们在深拷贝前会判断其是否已经被拷贝过了。)

class Solution {
    private HashMap <Node, Node> visited = new HashMap <> ();
    public Node cloneGraph(Node node) {
        if (node == null) {
            return node;
        }

        // 如果该节点已经被访问过了,则直接从哈希表中取出对应的克隆节点返回
        if (visited.containsKey(node)) {
            return visited.get(node);
        }

        // 克隆节点,注意到为了深拷贝我们不会克隆它的邻居的列表
        Node cloneNode = new Node(node.val, new ArrayList());
        // 哈希表存储
        visited.put(node, cloneNode);

        // 遍历该节点的邻居并更新克隆节点的邻居列表
        for (Node neighbor: node.neighbors) {
            cloneNode.neighbors.add(cloneGraph(neighbor));
        }
        return cloneNode;
    }
}
方法二:广度优先遍历:

原理:方法一与方法二的区别仅在于搜索的方式。深度优先搜索以深度优先,广度优先搜索以广度优先。这两种方法都需要借助哈希表记录被克隆过的节点来避免陷入死循环
二者区别图解:深度就是一条线向下到底为止;广度就是一层一层的向下。
在这里插入图片描述

下方代码里的while循环部分:
哈希表visited的键是题目传入的Node等原图节点,而对应的值是我们深拷贝出的节点
哈希表里的值是所有的深拷贝节点
队列queue里的是可能还未被处理的节点
进入循环前queue里有节点1,哈希表中同样,进入循环
取出队列里的节点,遍历其neighbors,依次取出并存入到哈希表里深拷贝节点的neighbors中,同时将neighbors里的节点放入queue中。
当前节点的neighbors全部添加完后,for循环结束进入下一轮while循环,此时队列里的是上一个节点neighbors里的两个节点。
开始添加这两个节点到哈希表中(也就是深拷贝),这就达到广度遍历一层一层处理的目的。
只有当前节点不再哈希表中时queue才会被添加新值,所以当哈希表深拷贝了所有节点后queue就会为空。
for循环里的 if 判断语句作用只是上一句话。

class Solution {
    public Node cloneGraph(Node node) {
        if (node == null) {
            return node;
        }

        HashMap<Node, Node> visited = new HashMap();

        // 将题目给定的节点添加到队列
        LinkedList<Node> queue = new LinkedList<Node> ();
        queue.add(node);
        // 克隆第一个节点并存储到哈希表中
        visited.put(node, new Node(node.val, new ArrayList()));

        // 广度优先搜索
        while (!queue.isEmpty()) {
            // 取出队列的头节点
            Node n = queue.remove();
            // 遍历该节点的邻居
            for (Node neighbor: n.neighbors) {
                if (!visited.containsKey(neighbor)) {
                    // 如果没有被访问过,就克隆并存储在哈希表中
                    visited.put(neighbor, new Node(neighbor.val, new ArrayList()));
                    // 将邻居节点加入队列中(可能还未被存入到哈希表中)
                    queue.add(neighbor);
                }
                // 更新当前节点的邻居列表
                // 从哈希表中取出节点,对其添加neighbor
                visited.get(n).neighbors.add(visited.get(neighbor));
            }
        }

        return visited.get(node);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeYello

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值