分类: 未分类

  • Can I Win,LeetCode,464

    当然可以!LeetCode 上的 “Can I Win”(题目编号 464)是一个中等难度的动态规划问题。下面我将详细解释这个问题的背景、解题思路以及具体的代码实现。

    题目描述

    在 “Can I Win” 问题中,你正在和一个朋友玩一个游戏。游戏规则如下:

    1. 你们轮流从 1 到 maxChoosableInteger 中选择一个整数。
    2. 每次选择后,该整数将不能再被选择。
    3. 谁选到的整数之和首先达到或超过 desiredTotal,谁就获胜。

    你需要判断在先手的情况下,你是否能保证获胜。

    输入输出

    • 输入
      • maxChoosableInteger:可以选择的最大整数。
      • desiredTotal:目标和。
    • 输出
      • bool:如果你能保证获胜,返回 True,否则返回 False
  • LFU Cache,LeetCode,460

    好的,我们来详细讨论一下 LeetCode 上的第 460 题:LFU Cache(最不经常使用缓存)。

    题目描述

    LFU(Least Frequently Used)缓存是一种缓存替换策略,它根据数据访问的频率来淘汰数据。如果一个数据在缓存中很少被访问,那么它将会被优先淘汰。

    你需要实现以下功能:

    1. LFUCache(int capacity): 用以初始化一个大小为 capacity 的缓存。
    2. int get(int key): 如果键 key 存在于缓存中,则返回其值,同时增加该键的使用频率;如果不存在,则返回 -1
    3. void put(int key, int value): 如果键 key 已存在,则更新其值;如果不存在,则插入该键值对。当缓存容量达到上限时,应该淘汰使用频率最低的数据。

    示例

    LFUCache cache = new LFUCache( 2 / capacity / );

    cache.put(1, 1); cache.put(2, 2); cache.get(1); // 返回 1 cache.put(3, 3); // 去除 key 2 cache.get(2); // 返回 -1 (未找到key 2) cache.get(3); // 返回 3 cache.put(4, 4); // 去除 key 1 cache.get(1); // 返回 -1 (未找到 key 1) cache.get(3); // 返回 3 cache.get(4); // 返回 4

    解题思路

    为了实现 LFU 缓存,我们需要以下几个数据结构:

    1. 哈希表 keyToVal:用于存储键到值的映射。
    2. 哈希表 keyToFreq:用于存储键到其访问频率的映射。
    3. 哈希表 freqToKeys:用于存储频率到具有该频率的所有键的集合的映射。
    4. 变量 minFreq:用于记录当前最小的访问频率。

    详细实现

    初始化

    class LFUCache: def init(self, capacity: int): self.capacity = capacity self.keyToVal = {} self.keyToFreq = {} self.freqToKeys = collections.defaultdict(set) self.minFreq = 0

    获取值

    def get(self, key: int) -> int: if key not in self.keyToVal: return -1

        # 增加该键的使用频率
        old_freq = self.keyToFreq[key]
        self.keyToFreq[key] += 1
        new_freq = self.keyToFreq[key]
    
        # 更新 freqToKeys
        self.freqToKeys[old_freq].remove(key)
        self.freqToKeys[new_freq].add(key)
    
        # 如果 old_freq 是最小频率且其对应的集合为空,更新 minFreq
        if not self.freqToKeys[old_freq] and old_freq == self.minFreq:
            self.minFreq += 1
    
        return self.keyToVal[key]

    插入值

    def put(self, key: int, value: int) -> None: if self.capacity <= 0: return

        if key in self.keyToVal:
            # 更新值并增加频率
            self.keyToVal[key] = value
            self.get(key)  # 利用 get 方法增加频率
            return
    
        # 如果缓存已满,需要淘汰一个频率最低的键
        if len(self.keyToVal) >= self.capacity:
            evict_key = next(iter(self.freqToKeys[self.minFreq]))
            self.freqToKeys[self.minFreq].remove(evict_key)
            del self.keyToVal[evict_key]
            del self.keyToFreq[evict_key]
    
        # 插入新键值对
        self.keyToVal[key] = value
        self.keyToFreq[key] = 1
        self.freqToKeys[1].add(key)
        self.minFreq = 1

    总结

    LFU 缓存的实现相对复杂,需要维护多个映射关系以确保高效的查找和更新操作。通过使用哈希表和集合,我们能够在 O(1) 的时间复杂度内完成 getput 操作。关键在于合理管理频率信息,并在必要时淘汰最不经常使用的元素。

    希望这个详细的解析和代码实现能帮助你理解和解决 LeetCode 上的第 460 题。如果有任何进一步的问题,欢迎继续提问!

  • Repeated Substring Pattern,LeetCode,459

    题目描述:

    给定一个非空的字符串 s,检查它是否可以通过重复某个子串多次构成。你可以假设字符串 s 仅由小写英文字母组成,并且它的长度不会超过 10000。

    示例:

    输入: "abab" 输出: True 解释: 可由子串 "ab" 重复两次构成。

    输入: "aba" 输出: False

    输入: "abcabcabcabc" 输出: True 解释: 可由子串 "abc" 重复四次构成。 (或者子串 "abcabc" 重复两次构成。)

    解题思路:

    1. 字符串拼接法:
      • 将字符串 s 拼接一遍,即形成 s + s
      • 去掉拼接后的字符串的第一个字符和最后一个字符,形成新的字符串 new_s
      • 检查原字符串 s 是否在 new_s 中出现。
      • 如果出现,则说明 s 可以通过重复某个子串多次构成。
    2. 数学法:
      • 设字符串 s 的长度为 n
      • 遍历所有可能的子串长度 i(从 1 到 n/2),检查 n 是否能被 i 整除。
      • 如果 n 能被 i 整除,则检查长度为 i 的子串是否能够通过重复构成原字符串 s
      • 如果找到这样的子串,则返回 True;否则返回 False

    代码实现(字符串拼接法):

    class Solution: def repeatedSubstringPattern(self, s: str) -> bool: if not s: return False

        new_s = (s + s)[1:-1]
        return s in new_s

    代码实现(数学法):

    class Solution: def repeatedSubstringPattern(self, s: str) -> bool: n = len(s) for i in range(1, n // 2 + 1): if n % i == 0: substring = s[:i] if substring * (n // i) == s: return True return False

    解释:

    1. 字符串拼接法:
      • new_s = (s + s)[1:-1] 的目的是去掉拼接后的字符串的首尾字符,避免直接包含原字符串。
      • s in new_s 检查原字符串是否在新字符串中出现,如果出现则说明可以通过重复某个子串构成。
    2. 数学法:
      • n % i == 0 检查 n 是否能被 i 整除,确保子串长度 in 的因数。
      • substring = s[:i] 提取长度为 i 的子串。
      • substring * (n // i) == s 检查通过重复子串 substring 是否能构成原字符串 s

    复杂度分析:

    • 字符串拼接法:
      • 时间复杂度:O(n),其中 n 是字符串的长度。
      • 空间复杂度:O(n),因为生成了新的字符串 new_s
    • 数学法:
      • 时间复杂度:O(n * sqrt(n)),最坏情况下需要遍历所有可能的子串长度,并检查子串的重复构成。
      • 空间复杂度:O(1),只使用了常数额外空间。

    这两种方法各有优缺点,字符串拼接法实现简单且效率较高,而数学法更直观地展示了问题的本质。根据实际情况选择合适的方法即可。

  • Circular Array Loop,LeetCode,457

    LeetCode 457题 “Circular Array Loop” 是一个中等难度的算法题。题目要求我们检测一个数组中是否存在一个循环,该循环满足以下条件:

    1. 循环中的元素值必须一致(即循环中的所有元素值要么都是正数,要么都是负数)。
    2. 循环的长度必须大于1。

    数组的每个元素都指向数组中的一个位置,即 nums[i] 表示从位置 i 跳到位置 (i + nums[i]) % n(其中 n 是数组的长度)。

    题目描述

    给定一个含有正整数和负整数的数组 nums,如果数组中存在一个循环,则返回 true,否则返回 false

    示例

    输入: [2,-1,1,2,2] 输出: true 解释: 有一个循环,从索引 0 -> 2 -> 3 -> 0。

    输入: [-1,2] 输出: false 解释: 没有循环。

    解题思路

    我们可以使用快慢指针(Floyd’s Tortoise and Hare)算法来检测循环。具体步骤如下:

    1. 初始化快慢指针:慢指针 slow 和快指针 fast 都从当前索引 i 开始。
    2. 移动指针
      • 慢指针每次移动一步。
      • 快指针每次移动两步。
    3. 检查循环
      • 如果快指针或慢指针指向的值与当前值符号不同,则不存在循环。
      • 如果快慢指针相遇,检查循环长度是否大于1。
    4. 避免重复检查:使用一个集合或标记数组来记录已经检查过的索引。

    代码实现

    以下是使用Python实现的代码:

    def circularArrayLoop(nums): n = len(nums)

    def next_index(i):
        return (i + nums[i]) % n
    
    for i in range(n):
        if nums[i] == 0:
            continue
    
        slow, fast = i, i
        while True:
            slow = next_index(slow)
            fast = next_index(next_index(fast))
    
            if slow == fast:
                break
    
            if nums[slow] * nums[i] < 0 or nums[fast] * nums[i] < 0:
                break
    
        if slow == fast:
            # 检查循环长度是否大于1
            loop_length = 1
            fast = next_index(slow)
            while fast != slow:
                fast = next_index(fast)
                loop_length += 1
    
            if loop_length > 1:
                return True
    
    return False

    示例

    print(circularArrayLoop([2, -1, 1, 2, 2])) # 输出: True print(circularArrayLoop([-1, 2])) # 输出: False

    解释

    1. next_index函数:计算下一个索引位置。
    2. 外层循环:遍历每个元素作为起点。
    3. 内层循环:使用快慢指针检测循环。
    4. 循环长度检查:如果快慢指针相遇,检查循环长度是否大于1。

    复杂度分析

    • 时间复杂度:O(n),每个元素最多被访问两次。
    • 空间复杂度:O(1),使用了常数额外空间。

    通过上述方法,我们可以有效地检测数组中是否存在满足条件的循环。希望这个解释对你理解这道题目有所帮助!如果有更多问题,欢迎继续提问。

  • 4Sum II,LeetCode,454

    题目描述:

    给定四个包含整数的数组 nums1, nums2, nums3, nums4,计算有多少个元组 (i, j, k, l) 使得 nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

    示例:

    输入: nums1 = [1, 2] nums2 = [-2, -1] nums3 = [-1, 2] nums4 = [0, 2]

    输出: 2

    解释: 两个元组如下:

    1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
    2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

    解题思路:

    这道题可以使用哈希表来优化求解过程。具体步骤如下:

    1. 构建哈希表:
      • 创建一个哈希表 sum_map,用于存储 nums1nums2 中所有可能的两数之和及其出现的次数。
    2. 填充哈希表:
      • 遍历 nums1nums2,计算每一对数的和 sum,并将该和及其出现的次数存储在 sum_map 中。
    3. 查找匹配的元组:
      • 遍历 nums3nums4,计算每一对数的和的相反数 -sum
      • sum_map 中查找是否存在该相反数,如果存在,则将其出现的次数累加到结果中。

    代码实现:

    def fourSumCount(nums1, nums2, nums3, nums4): sum_map = {} result = 0

    # 构建哈希表,存储 nums1 和 nums2 的所有两数之和及其出现的次数
    for num1 in nums1:
        for num2 in nums2:
            sum_ = num1 + num2
            if sum_ in sum_map:
                sum_map[sum_] += 1
            else:
                sum_map[sum_] = 1
    
    # 遍历 nums3 和 nums4,查找匹配的元组
    for num3 in nums3:
        for num4 in nums4:
            target = -(num3 + num4)
            if target in sum_map:
                result += sum_map[target]
    
    return result

    示例

    nums1 = [1, 2] nums2 = [-2, -1] nums3 = [-1, 2] nums4 = [0, 2] print(fourSumCount(nums1, nums2, nums3, nums4)) # 输出: 2

    复杂度分析:

    • 时间复杂度: O(n^2),其中 n 是每个数组的长度。我们需要遍历 nums1nums2 来构建哈希表,再遍历 nums3nums4 来查找匹配的元组。
    • 空间复杂度: O(n^2),哈希表的大小取决于 nums1nums2 中所有可能的两数之和的数量。

    总结:

    这道题通过巧妙地使用哈希表,将四数之和的问题转化为两数之和的问题,大大减少了计算复杂度。这种方法在处理多数组求和问题时非常有效,值得借鉴。

  • Delete Node in a BST,LeetCode,450

    好的,我们来详细讨论一下 LeetCode 上的第 450 题:删除二叉搜索树(BST)中的节点。

    题目描述

    给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回删除后的二叉搜索树的根节点。

    解题思路

    删除二叉搜索树中的节点可以分为以下几种情况:

    1. 节点不存在:如果 root 为空或者找不到 key,直接返回 root
    2. 节点是叶子节点:直接删除该节点,返回 null
    3. 节点只有一个子节点:删除该节点,并用其子节点替代。
    4. 节点有两个子节点
      • 找到该节点的右子树中的最小节点(即后继节点)。
      • 将该节点的值替换为后继节点的值。
      • 删除后继节点。

    代码实现

    以下是使用递归实现的代码:

    class TreeNode: def init(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right

    class Solution: def deleteNode(self, root: TreeNode, key: int) -> TreeNode: if not root: return root

        if key < root.val:
            root.left = self.deleteNode(root.left, key)
        elif key > root.val:
            root.right = self.deleteNode(root.right, key)
        else:
            # 找到了要删除的节点
            if not root.left:
                return root.right
            elif not root.right:
                return root.left
            else:
                # 找到右子树中的最小节点
                min_node = self.findMin(root.right)
                root.val = min_node.val
                root.right = self.deleteNode(root.right, min_node.val)
    
        return root
    
    def findMin(self, node: TreeNode) -> TreeNode:
        while node.left:
            node = node.left
        return node

    详细解释

    1. 基本情况
      • 如果 root 为空,直接返回 null
    2. 递归查找
      • 如果 key 小于当前节点的值,递归地在左子树中查找并删除。
      • 如果 key 大于当前节点的值,递归地在右子树中查找并删除。
    3. 删除操作
      • 当找到要删除的节点时,根据其子节点的数量进行不同的处理:
        • 如果没有左子节点,返回右子节点。
        • 如果没有右子节点,返回左子节点。
        • 如果有两个子节点,找到右子树中的最小节点(后继节点),将其值赋给当前节点,并递归地删除后继节点。
    4. 辅助函数 findMin
      • 用于找到某个节点的右子树中的最小节点。

    复杂度分析

    • 时间复杂度:O(h),其中 h 是树的高度。在最坏情况下,树可能是一条链,时间复杂度为 O(n)。
    • 空间复杂度:O(h),递归调用栈的深度。

    总结

    删除二叉搜索树中的节点是一个经典问题,需要处理多种情况。通过递归的方式,我们可以简洁地实现这一功能。关键在于正确处理有两个子节点的节点,通过找到后继节点并进行替换,保证二叉搜索树的性质不变。

    希望这个详细的解答对你有所帮助!如果有任何进一步的问题,欢迎继续提问。

  • Serialize and Deserialize BST,LeetCode,449

    LeetCode 449题是关于二叉搜索树(BST)的序列化和反序列化问题。序列化是将一个数据结构或对象状态转换为可存储或传输的格式的过程,以便在需要时可以恢复该数据结构或对象状态。反序列化是序列化的逆过程。

    题目描述

    你需要设计一个算法来实现二叉搜索树的序列化和反序列化。序列化是将一棵二叉搜索树转换为一个字符串,而反序列化是将一个字符串转换回一棵二叉搜索树。

    要求

    • 序列化和反序列化的算法应该是高效的,即时间复杂度和空间复杂度尽可能低。
    • 序列化的结果应该是一个字符串,反序列化的输入也应该是一个字符串。

    解决思路

    由于二叉搜索树具有有序性,可以利用前序遍历和中序遍历的特性来进行序列化和反序列化。但更常见的方法是使用前序遍历结合null标记来进行序列化,这样可以简化反序列化的过程。

    1. 序列化

    使用前序遍历(根-左-右)来遍历树,并将每个节点的值转换为字符串,如果遇到null节点,则用特殊字符(如”#”)来表示。

    2. 反序列化

    将序列化后的字符串按前序遍历的顺序重新构建二叉搜索树。使用一个队列来存储节点值,逐个构建节点并按前序遍历的顺序连接左右子树。

    代码实现(Python)

    class TreeNode: def init(self, x): self.val = x self.left = None self.right = None

    class Codec: def serialize(self, root): """Encodes a tree to a single string.""" if not root: return ""

        def preorder(node):
            if not node:
                return "# "
            return str(node.val) + " " + preorder(node.left) + preorder(node.right)
    
        return preorder(root)
    
    def deserialize(self, data):
        """Decodes your encoded data to tree."""
        if not data:
            return None
    
        def build_tree(values):
            val = values.pop(0)
            if val == "#":
                return None
            node = TreeNode(int(val))
            node.left = build_tree(values)
            node.right = build_tree(values)
            return node
    
        values = data.split()
        return build_tree(values)

    Your Codec object will be instantiated and called as such:

    ser = Codec()

    deser = Codec()

    tree = deser.deserialize(ser.serialize(root))

    详细解释

    1. 序列化(serialize):
      • 使用递归函数preorder进行前序遍历。
      • 如果当前节点为null,返回”#”和一个空格。
      • 否则,返回当前节点的值和一个空格,然后递归处理左子树和右子树。
    2. 反序列化(deserialize):
      • 将输入的字符串按空格分割成列表values
      • 使用递归函数build_tree来构建树:
        • values中取出第一个元素,如果是”#”,则返回None。
        • 否则,创建一个新节点,递归构建其左子树和右子树。

    复杂度分析

    • 时间复杂度:序列化和反序列化均为O(n),其中n是树中节点的数量。
    • 空间复杂度:序列化使用O(h)的递归栈空间,反序列化使用O(n)的空间来存储节点值。

    这种方法利用了二叉搜索树的性质和前序遍历的特性,实现了高效的序列化和反序列化。

  • Arranging Coins,LeetCode,441

    LeetCode 441题 “Arranging Coins” 是一个中等难度的数学问题。题目描述如下:

    你总共有 n 枚硬币,并希望将它们排列成一个阶梯形状,第 k 行必须正好有 k 枚硬币。

    给你一个整数 n,返回你可以完整排列的行数。

    示例 1:

    输入: n = 5 输出: 2 解释: 因为第三行不完整,所以返回 2.

    示例 2:

    输入: n = 8 输出: 3 解释: 因为第四行不完整,所以返回 3.

    解题思路

    这个问题可以通过数学公式和二分查找来解决。

    方法一:数学公式

    我们知道,前 k 行需要的硬币总数是: [ S = \frac{k(k + 1)}{2} ]

    我们需要找到一个最大的 k,使得: [ \frac{k(k + 1)}{2} \leq n ]

    这可以通过解以下不等式得到: [ k^2 + k – 2n \leq 0 ]

    使用求根公式: [ k = \frac{-b \pm \sqrt{b^2 – 4ac}}{2a} ] 其中,a = 1b = 1c = -2n

    所以: [ k = \frac{-1 \pm \sqrt{1 + 8n}}{2} ]

    我们取正根: [ k = \frac{-1 + \sqrt{1 + 8n}}{2} ]

    最后取整即可。

    方法二:二分查找

    我们可以使用二分查找来找到最大的 k,使得: [ \frac{k(k + 1)}{2} \leq n ]

    初始化 left = 1right = n,然后进行二分查找。

    代码实现

    方法一:数学公式

    import math

    def arrangeCoins(n: int) -> int: return int((-1 + math.sqrt(1 + 8 * n)) // 2)

    示例

    print(arrangeCoins(5)) # 输出: 2 print(arrangeCoins(8)) # 输出: 3

    方法二:二分查找

    def arrangeCoins(n: int) -> int: left, right = 1, n while left <= right: mid = (left + right) // 2 current = mid * (mid + 1) // 2 if current == n: return mid elif current < n: left = mid + 1 else: right = mid - 1 return right

    示例

    print(arrangeCoins(5)) # 输出: 2 print(arrangeCoins(8)) # 输出: 3

    总结

    • 数学公式法:通过求解一元二次方程来直接计算结果,时间复杂度为 (O(1))。
    • 二分查找法:通过二分查找逐步逼近结果,时间复杂度为 (O(\log n))。

    两种方法各有优劣,数学公式法更直观且速度快,但可能存在浮点数精度问题;二分查找法更通用且精度高,但稍微复杂一些。

    希望这些详细的解释和代码示例能帮助你理解和解决这个题目!如果有更多问题,欢迎继续提问。

  • K-th Smallest in Lexicographical Order,LeetCode,440

    LeetCode 440题 “K-th Smallest in Lexicographical Order” 是一个中等难度的题目,主要考察的是字典序(lexicographical order)和数字的排列组合。

    题目描述

    给定整数 nk,找到从 1n 的所有整数按字典序排列后的第 k 个数。

    字典序排列是指将数字视为字符串,按照字典顺序排列。例如,对于 n = 13,字典序排列为 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9]

    解法思路

    这个问题可以通过递归和数学的方法来解决。核心思想是利用字典树的性质,逐步缩小范围,找到第 k 个数。

    步骤:

    1. 初始化:从根节点 1 开始。
    2. 计算子节点数量:对于当前节点 curr,计算从 currcurr + 1 之间的所有子节点的数量。
    3. 比较 k 和子节点数量
      • 如果 k 小于等于子节点数量,说明第 k 个数在当前节点的子树中,更新 currcurr * 10 并继续搜索。
      • 如果 k 大于子节点数量,说明第 k 个数不在当前节点的子树中,更新 currcurr + 1 并减去当前子树节点的数量,继续搜索。

    关键函数:

    • countSteps(curr, n):计算从 currcurr + 1 之间的所有子节点的数量。

    代码实现

    class Solution: def findKthNumber(self, n, k): curr = 1 k -= 1 # 因为从1开始计数,所以k减1

        while k > 0:
            steps = self.countSteps(curr, n)
            if steps <= k:
                # 第k个数不在当前子树中
                curr += 1
                k -= steps
            else:
                # 第k个数在当前子树中
                curr *= 10
                k -= 1
    
        return curr
    
    def countSteps(self, curr, n):
        steps = 0
        first = curr
        last = curr
    
        while first <= n:
            steps += min(n + 1, last + 1) - first
            first *= 10
            last = last * 10 + 9
    
        return steps

    示例

    sol = Solution() print(sol.findKthNumber(13, 2)) # 输出: 10 print(sol.findKthNumber(13, 5)) # 输出: 13

    解释

    1. 初始化curr1 开始,k1 是因为从 1 开始计数。
    2. 循环:当 k 大于 时,计算从 currcurr + 1 之间的子节点数量。
    3. 子节点数量计算
      • firstlast 分别表示当前层的起始和结束节点。
      • 通过循环扩展 firstlast 到下一层,累加每层的节点数量。
    4. 更新 currk
      • 如果 k 大于子节点数量,说明第 k 个数不在当前子树中,curr 增加 1k 减去当前子树节点数量。
      • 如果 k 小于等于子节点数量,说明第 k 个数在当前子树中,curr 扩展到下一层(curr * 10),k 减去 1(因为进入下一层)。

    通过这种方法,逐步缩小范围,最终找到第 k 个数。

    希望这个详细的解释和代码实现能帮助你理解并解决这个题目!如果有任何进一步的问题,欢迎继续提问。

  • Combination Sum IV,LeetCode,377

    题目描述:

    LeetCode 377题 “Combination Sum IV” 是一个关于组合数学的动态规划问题。题目要求给定一个由正整数组成的数组 nums 和一个目标值 target,找出所有可能的组合,使得这些组合中的数字之和等于 target。每个数字在组合中可以无限次使用,但组合中的顺序不同的序列被认为是不同的组合。

    示例:

    输入: nums = [1, 2, 3], target = 4 输出: 7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

    解题思路:

    这个问题可以通过动态规划来解决。我们定义一个数组 dp,其中 dp[i] 表示和为 i 的组合数。初始时,dp[0] = 1,因为和为0的组合只有一种,即什么也不选。

    接下来,我们遍历从1到target的每一个数,对于每一个数i,我们再遍历nums数组中的每一个数num,如果i >= num,则dp[i] += dp[i - num]。这是因为如果我们可以通过某种方式组合出i - num,那么再加上num就可以组合出i

    代码实现:

    def combinationSum4(nums, target):

    初始化dp数组,长度为target + 1,初始值全为0

    dp = [0] * (target + 1)
    # 和为0的组合只有一种,即什么也不选
    dp[0] = 1
    
    # 遍历从1到target的每一个数
    for i in range(1, target + 1):
        # 遍历nums数组中的每一个数
        for num in nums:
            # 如果当前数i大于等于num,则更新dp[i]
            if i >= num:
                dp[i] += dp[i - num]
    
    # 返回和为target的组合数
    return dp[target]

    示例

    nums = [1, 2, 3] target = 4 print(combinationSum4(nums, target)) # 输出: 7

    详细解释:

    1. 初始化dp数组:
      • dp数组的大小为target + 1,因为我们需要计算从0到target的所有可能的组合数。
      • dp[0] = 1是因为和为0的组合只有一种,即什么也不选。
    2. 外层循环:
      • 遍历从1到target的每一个数i,表示我们当前要计算和为i的组合数。
    3. 内层循环:
      • 遍历nums数组中的每一个数num,表示我们尝试使用num来组合出和为i的情况。
      • 如果i >= num,说明我们可以使用num来组合出和为i的情况,此时dp[i]应该加上dp[i - num],因为dp[i - num]表示和为i - num的组合数,加上num后就能组合出和为i的情况。
    4. 返回结果:
      • 最终dp[target]就是我们要求的和为target的组合数。

    时间复杂度:

    • 时间复杂度为O(target * n),其中nnums数组的长度。这是因为我们需要遍历从1到target的每一个数,并且对于每一个数,我们还需要遍历nums数组中的每一个数。

    空间复杂度:

    • 空间复杂度为O(target),因为我们使用了一个长度为target + 1的数组来存储中间结果。

    通过上述分析和代码实现,我们可以有效地解决LeetCode 377题 “Combination Sum IV”。