代码随想训练营(两个月)

代码随想训练营

  • Day1 数组:二分搜索 + 移除元素
    • Leetcode 704 二分查找
    • Leetcode 27 移除元素
  • Day2 数组:有序数组平方 + 长度最小子数组 + 螺旋矩阵生成
    • Leetcode 977 有序数组的平方
    • Leetcode 209 长度最小的子数组
    • Leetcode 59 螺旋数组II
  • Day3 链表:移除链表元素 + 设计链表 + 链表翻转
    • Leetcode 203 移除链表元素
    • Leetcode 707 设计链表
    • Leetcode 206 链表翻转
  • Day4 链表 两两交换节点 + 删除倒数第n个节点 + 链表相交
    • Leetcode 24 两两交换链表中的节点
    • Leetcode 19 删除链表的倒数第n个节点
    • 面试题 02.07. 链表相交
    • Leetcode 142 环形链表
  • Day6 哈希表:有效的字母异位词 + 两个数组的交集 + 快乐数 + 两数之和
    • Leetcode242 有效的字母异位词
    • Leetcode 349 两个数组的交集
    • Leetcode 202 快乐数
    • Leetcode 1 两数之和
  • Day7 哈希表:四数相加II + 赎金信 + 三数之和 + 四数之和
    • Leetcode 四数相加II
    • Leetcode 383 赎金信
    • Leetcode 15 三数之和
    • Leetcode 18 四数之和
  • Day8 字符串:反转字符串 + 反转字符串II + 替换空格 + 翻转字符串里的单词 + 左旋转字符串
    • Leetcode 344 反转字符串
    • Leetcode 541 反转字符串II
    • 剑指offer 05 替换空格
    • Leetcode 151 反转字符串中单词(没做)
    • 剑指Offer58-II.左旋转字符串
  • Day9 KMP
  • Day10 字符串:重复的子字符串(KMP算法应用)
  • Day11 栈与队列:用栈实现队列 + 用队列实现栈 + 有效的括号 + 删除字符串中所有相邻重复项
    • Leetcode232 用栈实现队列
    • Leetcode225 使用队列来实现栈
    • Leetcode20 有效括号
    • Leetcode1047 删除字符串中的所有相邻重复项
  • Day13 栈与队列:逆波兰表达式求值 + 滑动窗口最大值 + 前K个高频元素
    • Leetcode150 逆波兰表达式求值
    • Leetcode239 滑动窗口最大值
  • Day38 斐波那契数列 + 爬楼梯 + 使用最小花费爬楼梯
    • Leetcode509 斐波那契数列
    • Leetcode70 爬楼梯
    • Leetcoda746 使用最小花费爬楼梯
  • Day39 不同路径 + 不同路径II
    • Leetcode62 不同路径
    • Leetcode63 不同路径II
  • Day41 整数拆分 + 不同的二叉搜索树
    • Leetcode343 整数拆分
    • Leetcode96 不同的二叉搜索树
  • Day42 01背包 + 分割等和子集
    • Leetcode416 分割等和子集
  • Day43 最后一块石头的重量II + 目标和 + 一和零
    • Leetcode474 一和零
    • Leetcode494 目标和
    • Leetcode1049 最后一块石头的重量II
  • Day44 完全背包 + 零钱兑换II + 组合总和IV
    • Leetcode518 零钱兑换II
    • Leetcode377 组合总和IV

前言:感觉基础算法的本质就是对数据结构的操作:增删改查

Day1 数组:二分搜索 + 移除元素

Leetcode 704 二分查找

第一次知道还要考虑区间,但是感觉双闭的区间和代码更对称,更好理解一些。就是while有没有等号的区别。

重点:leftrightmid的更新

  • mid:只有left <= right的时候更新,是最简单的,每次循环都会更新,left == right的时候就是要结束的时候了。
  • leftright:因为while的条件包含=,所以是双闭区间,在当前循环中,已经比较过nums[mid]target了,下次循环就不需要再考虑mid本身了。所以要向前或者向后缩小范围。

考虑到leftright两个整数相加得到的整数可能会溢出,所以采用mid = left + (right - left) // 2这种写法。

# 左闭右闭
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] < target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid - 1
            else:
                return mid
        return -1

Leetcode 27 移除元素

看到“原地”,就知道只能在原数组操作了,一开始只想到快慢指针的思路,但是相向双指针也可以。

本质:双指针交换元素

  • 同向双指针(快慢双指针):可以理解为在遍历两个数组,fast在遍历旧数组(因为跑得快,遍历完整个数组了,slow只跑了一段),slow遍历的是旧数组。因fast相当于是探路的,所以要对fast的值进行判定(if语句),然后根据结果,告诉slow少走一些弯路。
  • 相向双指针(对向):一个遇到val停下,一个没有遇到val就停下,然后交换。
#快慢指针
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        slow, fast = 0, 0
        while fast < len(nums):
            # if相当于更新slow,新数组的一个操作,fast理解为遍历旧数组
            # fast不管怎么都要遍历,而slow只有更新了之后才会向前(遍历)
            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1
                fast += 1
            fast += 1
        return slow
# 相向双指针
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        if nums is None or len(nums) == 0:
            return 0
        left, right = 0, len(nums)-1
        while left < right:
            # 这造成left只有遇到val才会跳出循环
            # right只有不是val的时候才会跳出
            # 刚好可以互换
            while nums[left] != val and left < right:
                left += 1
            while nums[right] == val and left < right:
                right -= 1
            nums[left], nums[right] = nums[right], nums[left]
        # 在中间边界存在一个问题:left == right情况
        if nums[left] == val:
            return left    
        else:
            return left + 1

感觉这种解法没有快慢指针那么优雅,所以还是放弃了,还要额外对left==right情况进行判定。

Day2 数组:有序数组平方 + 长度最小子数组 + 螺旋矩阵生成

Leetcode 977 有序数组的平方

一看到题目,就想着用双指针,只不过一开始有点被昨天的题目影响了,想在原数组进行操作,结果没操作出来。看了一眼答案,发现新定义了一个数组result来返回结果,只要在旧数组往中间遍历就行了。这样就简单多了。

不过一开始还是没注意,把平方写在条件判断外,导致多次平方。

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        n = len(nums)
        left, right = 0, n-1
        result = [-1] * n
        k = n - 1
        while left <= right:
            if nums[left]**2 > nums[right]**2:
                result[k] = nums[left]**2
                left += 1
                k -= 1
            else:
                result[k] = nums[right]**2
                right -= 1
                k -= 1
        return result

Leetcode 209 长度最小的子数组

滑动窗口可解,也是快慢指针,要有一个total来记录窗口内的元素之和,还要有一个index来记录窗口索引长度。

这么看这次要更新的有4个参数:slowfasttotalindex

  • slowtotal > target时更新
  • fast:跟着while循环更新
  • totalslow更新之前要减去nums[slow]
  • indextotal > target,要对当前的索引差与之前的最小值比较更新

可以看出来,基本都和total的变化相关,所以while内部,对total的值进行判定。

做题的时候,忘记考虑(输入数组为空)和(输入数字的全部数都小于target)的这两种情况了。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        slow, fast = 0, 0
        total, index =0, len(nums)+1
        while fast < len(nums):
            total += nums[fast]
            while total >= target:
                index = min(index, fast-slow+1) # 为啥+1?
                total -= nums[slow]
                slow += 1
            fast += 1
        return 0 if index == len(nums)+1 else index

Leetcode 59 螺旋数组II

记得前段时间写过这道题,但是现在也忘了(拍脑门)

但是有思路,应该是要模拟,要考虑边界问题,就像704二分查找的第二种解法,应该是左闭右开的沿着边走;

  • python里range()就是一个完美的左闭右开范围。
  • 声明一个n*n的数组
  • leftright,还有topbottom相等的时候,已经是到中心点了,循环就要结束了。
class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        left, right = 0, n-1
        top, bottom = 0, n-1
        res = [[0] * n for _ in range(n)] # 初始化数组
        num = 1 
        while left <= right and top <=bottom:
            for column in range(left, right+1):
                res[top][column] = num
                num += 1
            for row in range(top+1, bottom+1):
                res[row][right] = num
                num += 1
            if left < right and top < bottom:
                for column in range(right-1, left, -1):
                    res[bottom][column] = num
                    num += 1
                for row in range(bottom, top, -1):
                    res[row][left] = num
                    num += 1
            left, right, top, bottom = left+1, right-1, top+1, bottom-1
        return res

周末要整理一下怎么初始化数组。

Day3 链表:移除链表元素 + 设计链表 + 链表翻转

Leetcode 203 移除链表元素

和Day1 27题的移除元素(数组)差不多,不过这个是在链表上进行操作。

重点:虚拟头结点,因为链表的遍历是一次性的,到了结尾,如果没有设置备份和虚拟头结点的话,要返回处理过后的链表很麻烦。

class Solution:
   def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
       dummy = ListNode(0, head)  # 新建一个虚拟头结点,方便返回
       cur = dummy
       while cur.next:
           if cur.next.val == val:
               cur.next = cur.next.next
           else:
               cur = cur.next
       return dummy.next

Leetcode 707 设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index有效,则删除链表中的第 index 个节点。

这题得把题目放这,说实话,之前一看到这种题,我都是直接跳过了,因为太麻烦了(现在也非常抗拒),要写一个比较完整的的类了。为了让自己印象深刻一点,还是写详细一些这次。

这个类有3个属性,valnextindex和5个函数:

  • get(index):根据索引获取val
  • addAtHead(val):头部加入节点,之前链表的index也要跟着变化
  • addAtTail(val):尾部加入节点,
  • addAtIndex(index,val):根据索引值index,在中间插入节点
  • deleteAtIndex(index):删除某个索引的节点

就是说设计链表其实包含和插入链表和删除节点两个题了。

需要两个类,一个是节点Node类,另一个是链表MyLinkList类,它们的属性有:

  • class Node:节点的值val,节点指向的下一个节点next
  • class MyLinkList:链表的虚拟头_head,链表的长度_length
class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

class MyLinkedList:
    def __init__(self):
        self._head = Node(0)
        self._length = 0
    
    def get(self, index: int) -> int:
    def addAtHead(self, val: int) -> None:
    def addAtTail(self, val: int) -> None:
    def addAtIndex(self, index: int, val: int) -> None:
    def deleteAtIndex(self, index: int) -> None:

首先是根据索引获得值val

    def get(self, index: int) -> int: 
        if index < 0 or index >= self._length: # 根据index取值,需要遍历
            return -1
        
        post = self._head
        for _ in range(index + 1): # 需要考虑虚拟头结点
            post = post.next
        return post.val

接下来的三个成员函数实际上是一个功能,只要实现了addAtIndex就可以简单实现另外两个,增加和删除链表都要注意长度的变化。

   def addAtIndex(self, index: int, val: int) -> None:
        if index < 0: index = 0 # 前面条件判断
        elif index > self._length: return None 
        
        self._length += 1
        insert = Node(val)

        pre, post = None, self._head  # 前一个节点和后一个节点(index)
        for _ in range(index + 1):
            pre, post = post, post.next # 相当于在遍历两个链表, pre从self._head.next开始遍历
        else:
            pre.next, insert.next = insert, post

	def addAtHead(self, val: int) -> None:
        self.addAtIndex(0, val)

    def addAtTail(self, val: int) -> None:
        self.addAtIndex(self._length, val)

这里我不小心将self._length的增加操作放在范围判断之前了,导致出错。

    def deleteAtIndex(self, index: int) -> None:
       
        if index < 0 or index >= self._length: return None
        self._length -= 1
        
        pre, post = None, self._head
        for _ in range(index+1):  # 需要考虑虚拟头结点
            pre, post = post, post.next
        else:
            pre.next = post.next

Leetcode 206 链表翻转

感觉做完上一题设计链表,确实对链表的遍历理解加深了。prepost有种快慢链表的味道,只不过他们一直相差1,同时对它们俩进行遍历。

pre就相当于虚拟头结点dummy了,最后可以直接返回。

感觉就是每次的操作,只是将后一个节点的next先保存,然后指向前一个节点pre ,然后更新两个节点。

重点:要将

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        pre, post = None, head  
        while post:
            temp, post.next = post.next, pre  # 只将后一个节点的next先保存,然后指向前一个节点pre 
            pre, post = post, temp  # 都向后移动一位
        return pre  # 因为pre在head的前一个节点,所以相当于虚拟头结点了,这题因为翻转了,可以这么返回

Day4 链表 两两交换节点 + 删除倒数第n个节点 + 链表相交

Leetcode 24 两两交换链表中的节点

感觉画图会更好理解一点,其实根据原理和链表翻转类型,可以视为进阶题目。

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        cur = dummy = ListNode(0, head)
        while cur.next and cur.next.next:
            node_1, node_2 = cur.next, cur.next.next
            node_1.next, node_2.next, cur.next = node_2.next, node_1, node_2   # 从后往前更新
            cur = node_1  # 更新cur
        return dummy.next 

重点:是逆着箭头的走向进行更新。

Leetcode 19 删除链表的倒数第n个节点

之前刷过一次,使用了两次扫描,第一次得到链表的长度,第二次进行删除,删除操作和设计链表的删除功能差不多。

# 两次扫描
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        def getLength(head:ListNode)->int:
            length = 0
            while head:
                length += 1
                head = head.next
            return length
        cur = dummy = ListNode(0, head)  # dummy是虚拟头结点
        size = getLength(head)
        for i in range(1, size - n + 1):
            cur = cur.next
        cur.next = cur.next.next
        return dummy.next

这次试试单次扫描的方法,确实没想到,使用快慢指针,先让fast移动n步,然后fastslow同时移动,fast到结尾了,就删除slow的节点。
出错点:将dummy.next用来初始化slowfast了,感觉对边界还是不是很熟悉

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy = ListNode(0,head)
        slow = fast = dummy
        while n:  # 删除倒数第n个
            n -= 1 
            fast = fast.next
        
        while fast.next:
            slow, fast =slow.next, fast.next
        else:
            slow.next = slow.next.next
            return dummy.next

面试题 02.07. 链表相交

一开始确实没想到,感觉更像数学题目:

  • 遍历两次,求长度差,然后一个先走,一个后走,类似快慢指针
  • 这个方法太巧妙了,类似直接走headAheadB的总长度,如果有交点的话,第二段的最后肯定是相同。
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        if not headA or not headB:
            return None
        cura, curb = headA, headB
        while cura != curb:
            cura = cura.next if cura else headB
            curb = curb.next if curb else headA
        return cura

Leetcode 142 环形链表

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head
        while fast and fast.next:
            slow, fast = slow.next, fast.next.next
            if slow == fast:  # 相等,表示有环
                p, q = head, slow  # 重新设置起点,再次相遇,即为环的起点
                while p!=q:
                    p, q = p.next, q.next
                return p
        return None

Day6 哈希表:有效的字母异位词 + 两个数组的交集 + 快乐数 + 两数之和

Leetcode242 有效的字母异位词

一次就ac了,感觉还是第一次,没啥好说的。

  • 一维list的初始化,想说一下,但是还是有时间再说吧。
  • all和any函数的用法,感觉相当于for + if的感觉
class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        res = [0 for _ in range(26)]
        for i in s:
            res[ord(i) - ord("a")] += 1
        for i in t:
            res[ord(i) - ord("a")] -= 1
        return all([i == 0 for i in res])

Leetcode 349 两个数组的交集

set在Python里的相关用法,学习一下

  • 组合:set3 = set1.union(set2)或者set3 = set1 | set2
  • 添加:set1.add(item)
  • 相交:set3 = set1 & set2
  • 不同:set3 = set1.difference(set2),set1里有的而set2没有的
  • 清除:set1.clear()
  • 移除:set1.remove(item)
class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        set1, set2 = set(), set()
        for i in nums1:
            set1.add(i)
        
        for j in nums2:
            set2.add(j)
                
        return list(set1 & set2)

Leetcode 202 快乐数

Leetcode 202 快乐数
虽然挺简单的,但是一开始还是没思路

class Solution:
    def isHappy(self, n: int) -> bool:
        def sum_cal(num: int):
            _sum = 0
            while num:
                _sum += (num % 10) ** 2
                num = num // 10
            return _sum

        res = set()

        while True:
            n = sum_cal(n)
            if n == 1:
                return True
            elif n in res:
                return False
            else:
                res.add(n)
     if n in res:
     	return False
     else:
        res.add(n)
	return False if n in res else res.add(n)

这两种的输出结果为啥不一样。下面是相当于将res.add(n)作为一个参数返回了吗,所以是null

Leetcode 1 两数之和

python中的哈希map就是字典dict,将字典的构建和检索放到一块进行。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        nums_dict = dict()
        # 构建字典
        for index, num in enumerate(nums):
            if target - num not in nums_dict:
                nums_dict[num] = index
            else:
                return [nums_dict[target-num], index]

Day7 哈希表:四数相加II + 赎金信 + 三数之和 + 四数之和

Leetcode 四数相加II

第一眼还没想出来,看了视频,其实感觉和两数之和差不多,可能是双重循环ptsd了,感觉非常抗拒(笑),觉得答案不可能这么不优雅。

class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        foursum = dict()
        for i in nums1:
            for j in nums2:
                if i+j in foursum:
                    foursum[i+j] += 1
                else:
                    foursum[i+j] = 1
        count = 0
        for i in nums3:
            for j in nums4:
                if 0-i-j in foursum:
                    count += foursum[-i-j]
        return count

Leetcode 383 赎金信

这题挺简单的,基本和242差不多。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        m = dict()
        for i in magazine:
            if i in m:
                m[i] += 1
            else:
                m[i] = 1

        for j in ransomNote:
            if j in m:
                m[j] -= 1
            else:
                return False
        
        return False if any(i < 0 for i in m.values()) else True

Leetcode 15 三数之和

感觉这题的关键在于去重,一开始没注意审题,以为只要索引不同就行了,原来是要三元数组也要不一样才满足条件

  • nums[i]:相同的跳过就行了,但是考虑到是三元数组内是允许有相同元素的,所以还要考虑只有比遍历过的才跳过,所以要和nums[i-1]的进行比较。
  • nums[left]nums[right]也执行差不多的去重步骤。
# 这个是没有去重的,框架比较清晰
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n, res =len(nums), []
        nums.sort()
        for i in range(n):
            left, right = i + 1, n - 1
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                if total < 0: left +=1
                elif total > 0: right -= 1
                else:
                    res.append([nums[i], nums[left], nums[right]])
                    left += 1
                    right -= 1
        return res
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n, res =len(nums), []
        nums.sort()
        for i in range(n):
            left, right = i + 1, n - 1
            if nums[i] > 0: break # 剪枝
            if i >= 1 and nums[i] == nums[i-1]: continue # 去重 nums[i]
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                if total < 0: left +=1
                elif total > 0: right -= 1
                else:
                    res.append([nums[i], nums[left], nums[right]])
                    while left != right and nums[left] == nums[left + 1]: left += 1 # 去重nums[left]
                    while left != right and nums[right] == nums[right - 1]: right -= 1 # 去重nums[right]
                    left, right = left + 1, right -1
        return res

看到之前的解法,是用中间扩散法+后处理的去重,时间确实慢很多。

Leetcode 18 四数之和

确实和三数之和比较像,就是多了一层循环。

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        n, res = len(nums), []
        if n < 4: return [] # 剪枝
        for i in range(n):
            if i > 0 and nums[i] == nums[i - 1]: continue # 去重 a
            for k in range(i+1, n):
                if k > i + 1 and nums[k] == nums[k-1]: continue # 去重 b
                p, q = k + 1, n - 1
                while p < q:
                    if nums[i] + nums[k] + nums[p] + nums[q] > target: q -= 1
                    elif nums[i] + nums[k] + nums[p] + nums[q] < target: p += 1
                    else:
                        res.append([nums[i], nums[k], nums[p], nums[q]])
                        while p < q and nums[p] == nums[p + 1]: p += 1 # 去重 c
                        while p < q and nums[q] == nums[q - 1]: q -= 1 # 去重 d
                        p, q = p + 1, q - 1
        return res

Day8 字符串:反转字符串 + 反转字符串II + 替换空格 + 翻转字符串里的单词 + 左旋转字符串

Leetcode 344 反转字符串

感觉这题挺简单的

class Solution:
    def reverseString(self, s: List[str]) -> None:
        n = len(s)
        left, right = 0, n-1
        while left < right:
            s[left], s[right] = s[right], s[left]
            left, right = left + 1, right - 1

Leetcode 541 反转字符串II

这题也挺简单的,学了join()的用法

class Solution:
    def reverseStr(self, s: str, k: int) -> str:
        res = list(s)
        for cur in range(0, len(s), 2 * k):
            res[cur: cur + k] = reversed(res[cur: cur + k])
        return ''.join(res)

剑指offer 05 替换空格

新建了一个数组,挺简单的,如果有时间的话,做一下就在原数组上操作的解法。

class Solution:
    def replaceSpace(self, s: str) -> str:
        count = s.count(" ")
        res = [0 for _ in range(len(s) + count*2)]
        j = 0
        for i in s:
            if i == ' ':
                res[j:j+3] = '%20'
                j += 3
            else:
                res[j] = i
                j += 1
        return ''.join(res)

Leetcode 151 反转字符串中单词(没做)

这题简直超级加倍,一旦不能使用库函数split之后,就变得特别的麻烦,和设计链表有得一拼,一题更胜3题。

  • 去除数组内多余元素
  • 数组翻转
    我先跳过了,周日再做吧

剑指Offer58-II.左旋转字符串

就是反转再反转,如果使用切片或者反转函数reversed的话,还是很简单的。如果都不给用的话,就只能手写一个reversed函数了。

class Solution:
    def reverseLeftWords(self, s: str, n: int) -> str:
        def reversedsub(lst, left, right):
            while left < right:
                lst[left], lst[right] = lst[right], lst[left]
                left, right = left+1, right-1
            return lst

        res = list(s)
        end = len(res) - 1
        reversedsub(res, 0, n-1)
        reversedsub(res, n, end)
        reversedsub(res, 0, end)
        return ''.join(res)

终于把28号的题目大致刷完了。感觉可以对数组字符反转类的做一个总结。

Day9 KMP

Day10 字符串:重复的子字符串(KMP算法应用)

Day11 栈与队列:用栈实现队列 + 用队列实现栈 + 有效的括号 + 删除字符串中所有相邻重复项

又是两道模拟题,我估计最讨厌的就是模拟类型的题目了。

第一和第二题的相互实现,其实可以放在一起来讲。

首先要知道,队列和栈分别有什么操作:

  • queue.push
  • queue.pop
  • queue.peek
  • queue.empty

以上是队列的操作,栈的操作有 :

  • stack.push
  • stack.pop
  • stack.top
  • stack.empty

然后就是两种数据结构之间的操作是如何映射的。

Leetcode232 用栈实现队列

看了代码随想录的动画,要实现队列的实现需要两个栈,一个stack-in,一个stack-out,分别对进出队列进行处理。至于队列的操作和两个栈的更新有什么映射操作,我观察:

  • queue.push:只需要stack-in正常push就行了。(这就是stack-in的更新)
  • queue.pop:出队列就需要pop操作stack-out栈了,如果stack-out栈是空的话,就需要先将stack-in栈的元素给pop出来然后pushstack-out,如果stack-in也是空的话,那队列就是empty了。
  • queue.peek:就相当于stack-out.top了。
  • queue.emptystack-instack-out都是空的话,队列就是空的。

操作的重点在queue.pop上。

回到python的实现上,python没有内置的stack,只能使用list来代替实现。

class MyQueue:

    def __init__(self):
        self.stack_in = []
        self.stack_out = []

    def push(self, x: int) -> None:
        self.stack_in.append(x)

    def pop(self) -> int:
        if self.empty(): return None

        if self.stack_out: return self.stack_out.pop()
        else:
            for _ in range(len(self.stack_in)):
                self.stack_out.append(self.stack_in.pop())
            return self.stack_out.pop()

    def peek(self) -> int:
        ans = self.pop()
        self.stack_out.append(ans)
        return ans

    def empty(self) -> bool:
        return not (self.stack_in or self.stack_out)

Leetcode225 使用队列来实现栈

感觉python也只能用list来模拟队列

栈的操作:

  • stack.push:queue.push就可以实现
  • stack.poppop要将队尾的元素弹出,翻转队列,然后弹出,再翻转回来可以吗 (最优解:除了队尾的,全部pop一遍然后再push一遍,最后弹出头)
  • stack.empty:就是队列为空了
  • stack.size:就是队列的长度
class MyStack:

    def __init__(self):
        self.queue = deque()

    def push(self, x:int) -> None:
        self.queue.append(x)

    def pop(self):
        if self.empty(): return None
        for _ in range(len(self.queue) - 1):
            tmp = self.queue.popleft()
            self.queue.append(tmp)
        return self.queue.popleft()

    def empty(self):
        return len(self.queue) == 0

    def top(self):
        if self.empty(): return None
        return self.queue[-1]

Leetcode20 有效括号

括号只有配对了才有效:

  • 遇到左括号,就进栈
  • 遇到右括号,就将对应左括号出栈。不是的话就false

最后为空就true,不为空就false

主要就是左右括号的配对使用什么数据结构来存放。我想用dict,也就是set,然后将key存放右括号,value存放左括号

python的stack操作怎么实现,list

class Solution(object):
    def isValid(self, s):
        path = {")":"(", "]":"[", "}":"{"}
        stack = []

        for i in s:
            if i in path.keys():
                if stack and stack[-1] == path[i]:
                    stack.pop()
                else:
                    return False
            else:
                stack.append(i)

        return True if not stack else False

Leetcode1047 删除字符串中的所有相邻重复项

其实感觉和删除括号那题差不多。都是对称匹配的问题,使用stack来解决特别方便,遇到相同的就pop,遇到不同的就push

class Solution(object):
    def removeDuplicates(self, s):
        stack = []
        for i in s:
            if stack and stack[-1] == i:
                stack.pop()
            else:
                stack.append(i)
        return ''.join(stack)

终于把day11的补上了,周日勉强喘息了一下,但是kmp的两天还没有时间去看了。准备之后慢慢补,目前就按着进度继续打卡好了。

Day13 栈与队列:逆波兰表达式求值 + 滑动窗口最大值 + 前K个高频元素

Leetcode150 逆波兰表达式求值

好像之前做过,其实了解了这个逆波兰表达式的运算步骤的话,挺简单的了,但是还是出了一些小问题,没有对eval的结果取整,导致有部分测试的结果差1。

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        op, res= {"+", "-", "*", "/"}, []
        for i in tokens:
            if i in op:
                a, b = res.pop(), res.pop()
                res.append(int(eval(f"{b}{i}{a}")))
            else:
                res.append(i)
        return int(res.pop())

Leetcode239 滑动窗口最大值

第一眼看见题目,就想着使用暴力法求解,但是我看了一眼今日的标题,看起来事情并没有这么简单,得往栈和队列那里凑。好吧,想不出来。

看了题解,主要有两种常见的思路:

  • 优先队列(堆):大根堆,还是第一次听说,python有自带的小根堆
  • 单调队列:python没有,得自己实现。

优先队列

  • 优先队列的队首是优先级最高的。
  • 实现:可以使用不同的数据结构实现
    • 堆:使用堆来实现,入队和出队的复杂度都是O(logn)
    • 列表:入队是O(n),出队是O(1)的复杂度

Day38 斐波那契数列 + 爬楼梯 + 使用最小花费爬楼梯

Leetcode509 斐波那契数列

动态规划入门的基础题了,但是一开始还是没搞对,只考虑了n >= 2的情况,没有考虑n < 2的情况。果然是考虑不周到。动态规划比较重要的一点就是要将所有情况都考虑在内。

class Solution(object):
    def fib(self, n):
        f = [0, 1]
        if n < 2:
            return f[n]
        for i in range(2, n+1):
            f.append(f[i-1] + f[i-2])
        return f[-1]

这是看到的另外一个解法,就是仅仅针对一维数组的动态规划,双变量轮转法。时间和空间都非常好。

class Solution(object):
    def fib(self, n):
        if n < 2:
            return n
        a, b = 0, 1
        for i in range(2, n + 1):
            a, b = b, a + b
        return b

Leetcode70 爬楼梯

来了一道动态规划的题目,因为不熟悉,打算使用闫式DP分析法来分析一波,理清一下思路。

  • 应该是一维的动态规划,所以是f(n)
  • f(n)表示什么呢? -> 集合:所有爬上n阶楼梯的方法
  • 我们这题要求解的是什么呢? -> 属性:count:不同爬梯方法的计数
  • 状态方程
    • 最后一步上1阶的情况:f(n-1)
    • 最后一步上2阶的情况:f(n-2)

这么一通分析下来,其实和斐波那契数列509那题差不多,只是f[0]=1,这也是我考虑边界的时候出错的地方,以为上0节台阶,应该方法就为0,实际上不动也当成是一种方式。

当然这题因为是一维的动态规划,所以也可以用双变量轮转法。

class Solution(object):
    def climbStairs(self, n):
        f = [1, 1]  # 对于0阶,1阶,就是1和1
        if n < 2:
            return f[n]
        for i in range(2, n+1):
            f.append(f[i-1] + f[i-2])
        return f[-1]

Leetcoda746 使用最小花费爬楼梯

自己的错误思路 正确的思路
集合 爬上n阶楼梯的所有方案集合,
f(i)表示前i步最小的cost
同左,但是有一点没有考虑,cost数组长度为n
数组索引为n-1,就是说上n阶台阶,就要将n-1cost算上。
属性 min:最小的cost 同左
状态方程 1. 通过1步到达第i阶楼梯的:f(i-1)
2. 通过2步到达第i阶楼梯的:f(i-2)
1. f(i-1) + cost(i-1)
2. f(i-2) + cost(i-2)
final f(i) = min(f(i-1) + cost(i-1), f(i-2) + cost(i-2))
边界条件 没考虑清楚 0层和1层不需要爬,
因为可以直接到达2层,所以f(0)=f(1)=0
这个也是没考虑到的地方。

如果把上面的都理清楚的话,题解就呼之欲出了:

class Solution(object):
    def minCostClimbingStairs(self, cost):
        n = len(cost) + 1
        f = [0] * n
        for i in range(2, n):
            f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2])
        return f[-1]

Day39 不同路径 + 不同路径II

Leetcode62 不同路径

  • 这个一眼看过去就是二维动态规划f(m, n)
  • 集合:到达网格m-1,n-1的唯一线路集合
  • 属性:count,唯一线路计数
  • 状态方程:f(m, n) = f(m-1, n) + f(m, n-1)
  • 边界条件:f(0, 0), f(0, 1), f(1, 0)都是1

出错点:遍历我一开始从2开始遍历,实际上要从1开始。

class Solution(object):
    def uniquePaths(self, m, n):
        f = [[1] * n for _ in range(m)]
        for i in range(1, m):
            for j in range(1, n):
                f[i][j] = f[i-1][j] + f[i][j-1]
        return f[-1][-1]

但是上面这个是没有经过优化的,我们要对动态规划进行空间的优化,使用滚动数组进行优化:

我们分析状态方程f(m, n) = f(m-1, n) + f(m, n-1)可以知道,当前网格,只与正上方的网格和左边的网格相关。也就是说,我们可以将其优化成f(n) = f(n) + f(n-1), 因为:

  • 左边的f(n)表示当前值,右边的f(n)表示上一行的f(n)
  • f(n-1)则是左边的网格。
class Solution(object):
    def uniquePaths(self, m, n):
        f = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                f[j] = f[j] + f[j-1]
        return f[-1]

Leetcode63 不同路径II

  • 带障碍的路径,还是f(m, n)
  • 集合:到达网格m-1, n-1的路径和
  • 属性:count
  • 状态方程:f(m, n) = f(m-1, n) + f(m, n-1), ob(m-1, n) or ob(m, n-1) != 1

上面这个分析还行,和上面一题差不多,但是边界条件感觉被教育了一翻,大概是二维动态规划的边界条件和一维的还是有很大区别的。

我下面这段代码是错误的,因为没有正确的考虑第一行和第一列的边界条件,觉得只要决定f[0][0]f[0][1]f[1][0]的情况就行了。其实整一个第一列和第一行的边界都要考虑的,为的是避免输入矩阵只有一行或者一列的情况。

class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        """
        :type obstacleGrid: List[List[int]]
        :rtype: int
        """
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        f = [[1] * n for _ in range(m)]
        # 初始边界条件没考虑好:
        f[0][1] = 0 if obstacleGrid[0][1] == 1 else 1
        f[1][0] = 0 if obstacleGrid[1][0] == 1 else 1
        # 只能从1,1开始,没办法从0,1和1,0开始
        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 1:
                    f[i][j] = 0
                else:
                    f[i][j] = f[i-1][j] + f[i][j-1]
        return f[-1][-1]

正确的代码:

class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        f = [[0] * n for _ in range(m)]
        
        # 第0,0个格子
        f[0][0] = 0 if obstacleGrid[0][0] == 1 else 1
        if f[0][0] == 0: return 0

        # 第一行:1~n
        for i in range(1, n):
            if obstacleGrid[0][i] == 1:
                break
            f[0][i] = 1

        # 第一列:1~m
        for i in range(1, m):
            if obstacleGrid[i][0] == 1:
                break
            f[i][0] = 1

        # 只能从1,1开始,没办法从0,1和1,0开始
        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 0:
                    f[i][j] = f[i-1][j] + f[i][j-1]
        return f[-1][-1]

就是我们在这些网格中进行动态规划的话,必须要将边界条件分成3部分来考虑:

  • 第0,0个格子
  • 第一列,1~m个格子
  • 第一行:1~n个格子

Day41 整数拆分 + 不同的二叉搜索树

Leetcode343 整数拆分

自己的错误思路 正确的思路
集合 f(n)表示分成k个数的最大乘积 同左
属性 max:最大的乘积 同左
状态方程 f(n)可以分成1n-1,感觉有很多种分法都可以到达最后一步,
f(n) = max(f(1)*f(n-1), ..., f(n//2)f(n-n//2))
从小到大遍历的话应该可以
- 两个数相乘::f(i) = i-j * j
- 三个及以上数相乘:f(i) = f(i-j) * j)
final f(i) = max(f(i), max((i-j)*j, f(i-j)*j))
边界条件 不清楚 f(0)=f(1)=0, f(2)=1
class Solution(object):
    def integerBreak(self, n):
        f = [0] * (n + 1) 
        f[2] = 1
        for i in range(3, n + 1):
            for j in range(1, i//2+1):
                f[i] = max(f[i], max(j*(i-j), j*f[i-j]))
        return f[n]

首先,这题踩的坑还是有点多的,也有很多疑问点:

  • 为什么要分为2和3以上两种情况进行讨论呢?
    • 存在n本身比拆分n之后相乘还要大的情况。
  • 为什么f要初始化成n+1个元素呢?(初始条件)
    • 实际f[0]是没有任何意义的,也就是索引和n差了一位,就是f[n]对应数字n
    • 所以相比于之前设置f[0],f[1],这题的初始条件相当于是f[1], f[2]
  • 为什么还要将f[i]也要放进去比较大小?
    • max函数内的f[i]只是用来记录上一个状态的,但是是否所有的动态规划求极值都要有上一个状态?这个值得思考。

Leetcode96 不同的二叉搜索树

就是给定一个数字n,看1~n可组成几棵不同的二叉搜索树。

首先回顾一下二叉搜索树,left < root < right

这算是一道动态规划 + 二叉搜索树的题目。

这个分析我确实想不出来:

  • 集合:f(n)表示组成二叉搜索树的数量
  • 属性:count
  • 状态方程:f(n) = f(0)f(n-1) + f(1)f(n-2) + ... + f(n-1)f(0)

f(0) 表示根的左边没有元素,其实问题就是左右子树分配多少个元素。因为二叉搜索树的特性,左右子树的序列肯定是递增的,所以关键不在于root是什么元素,而在于左右子树分别分配多少长度的序列。不同长度的序列,组合成的子树结构数量是确定的。

但是这种状态方程,我也是第一次见到:

class Solution(object):
    def numTrees(self, n):
        f = [0] * (n + 1)  # 同343原因一样,因为f[0]没有任何实际意义
        f[0] = f[1] = 1
        for i in range(2, n+1):
            for j in range(1, i+1):
                f[i] += f[j-1] * f[i-j]
        return f[n]

以上纯递归的解法:

但是听说这种状态转移方程,在数学上叫做“卡特兰数”,有一个非常简单的递推公式可以方便的求得f(n)
C 0 = 1 , C n + 1 = 2 ( 2 n + 1 ) n + 2 ⋅ C n C_0 = 1, \qquad C_{n+1} = \frac{2(2n + 1)}{n+2} \cdot C_n C0=1,Cn+1=n+22(2n+1)Cn
使用这个公式来解答的话,感觉就更简单了:

class Solution(object):
    def numTrees(self, n):
        c = 1
        for i in range(0, n):
            c = c * 2 * (2 * i + 1) / (i + 2)
        return int(c)

Day42 01背包 + 分割等和子集

Leetcode416 分割等和子集

一开始碰到这道题,我也知道要用0-1背包,但是要怎么用,却有点小问题(我可能比较薄弱的环节就是问题抽象化和转化)。

其实可以转换成背包容量为sum(nums) // 2的背包问题,因为如果存在子序列的和为sum(nums) // 2,那么剩下的子序列的和也必然是sum(nums) // 2。这样就能等和的分割子集了。但是还有很多问题。

按照分析法梳理思路:

  • 集合:f(i, j)是指在0~i范围内,物体重量小于j的选择集合。也就是i表示物品选择的范围,某种意义是索引的上限,j也是表示范围,也是重量的上限,在别的题目里,可以是某种约束的上限。所以i, j其实都表示上限,默认的下限都是0
  • 属性:bool,如果存在就为True,不存在就为False
  • 状态方程:
    • j > nums[i]:就是要放入背包的物品重量没有超过了重量的上限。在这里就是选择的第i个数小于sum(nums) // 2了。
      • i个物品放入背包,也就是说相当于在0~i-1范围内选剩下的,重量的上限也变成了j - nums[i],因为部分背包的重量已经被第i个物品占据了:f(i, j) = f(i-1, j-nums[i])
      • i个物品不放入背包,也就是说在0~i-1范围内选择,但是重量的上限还是jf(i, j) = f(i-1, j)
    • j < nums[i]:第i个物体的重量已经超过背包上限了,所以必然不可能放进背包里面。
      • 同上面第二种情况:f(i, j) = f(i-1, j)
  • 边界条件:
    • f(0, 0) = True,因为不选择任何物体,物体限制也是0,所以肯定是True
    • f(0, 0~j) 第一行:只能够选择物体0,也就是只有重量nums[0]为True,f(0, nums[0]) = True,其他为False
    • f(0~i, 0) 第一列:重量限制为0,存在啥都不选的情况,使得结果为True,所以f(0~i, 0) = True
class Solution(object):
    def canPartition(self, nums):
        # 不满足的情况
        s, n, m, mid = sum(nums), len(nums), max(nums), sum(nums)//2
        if n < 2 or s & 1 or m > mid:
            return False
        f = [[False] * (mid+1) for _ in range(n)]  # 初始化二维数组, n x (target + 1)

        # 边界条件初始化
        for i in range(n):
            f[i][0] = True  # 00和第一列
        f[0][nums[0]] = True  # 第一行

        # 正式开始计算
        for i in range(1, n):
            for j in range(1, mid+1):
                if j >= nums[i]:
                    f[i][j] = f[i-1][j] | f[i-1][j-nums[i]]
                else:
                    f[i][j] = f[i-1][j]
        return f[-1][-1]

进行空间的优化:

上述代码的空间复杂度是 O ( n × m i d ) O(n×mid) O(n×mid)。但是可以发现在计算 f(i, j) 的过程中,每一行的 f(i, j) 值都只与上一行(i-1)行的 f(i-1, j) 值有关,因此只需要一个一维数组即可将空间复杂度降到 O ( m i d ) O(mid) O(mid)。此时的转移方程为:f[j]=f[j] ∣ f[j−nums[i]]

且需要注意的是第二层的循环我们需要从大到小计算,因为如果我们从小到大更新 j 值,那么在计算 f[j] 值的时候,f[j−nums[i]] 已经是被更新过的状态,不再是上一行的 j 值。

class Solution(object):
    def canPartition(self, nums):
        # 不满足的情况(和动态规划无关的条件)
        s, n, m, mid = sum(nums), len(nums), max(nums), sum(nums) // 2
        if n < 2 or s & 1 or m > mid:
            return False

        f = [False] * (mid + 1)  # 初始化数组, (mid + 1)

        f[nums[0]] = True  # 边界条件初始化

        # 正式开始计算
        for i in range(1, n):
            for j in range(mid, 0, -1):
                if j >= nums[i]:
                    f[j] = f[j] | f[j - nums[i]]
        return f[-1]

Day43 最后一块石头的重量II + 目标和 + 一和零

Leetcode474 一和零

有了上面对0-1背包的理解,立即就想到了使用0-1背包来解,而且也知道是三维动态规划。

分析一波:

  • 集合:f(i, jm, jn)表示0~i索引范围内, 0 0 0数量不大于jm 1 1 1数量不大于jn的子集
  • 属性:count
  • 状态方程:
    • stras[jm] < m and stras[jn] < n:
      • if[i][m][n] = f[i-1][m-jm][n-jn] + 1
      • 不选i: f[i][m][n] = f[i-1][m][n]
    • stras[jm] > m or stras[jn] > n
      • 不选i: f[i][m][n] = f[i-1][m][n]
  • 边界条件:就是三条边都要初始化
    • f[0][0][0] = 0
    • f[0][:m][0] = 0
    • f[0][0][:n] = 0
    • f[i][0][0] = 0

但是我忽略了max这个条件。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        f = [[[0] * (n+1) for _ in range(m+1)] for _ in range(len(strs)+1)]  # 初始化数组,顺序和遍历顺序相关
        # 为啥有些是len + 1,有些直接len
        
        for i in range(1, len(strs)+1):
            ones, zeros = strs[i-1].count('1'), strs[i-1].count('0')  # 为啥是i-1的
            for jm in range(m+1):
                for jn in range(n+1):
                    if jm < zeros or jn < ones:
                        f[i][jm][jn] = f[i-1][jm][jn]
                    else:
                        f[i][jm][jn] = max(f[i-1][jm][jn], f[i-1][jm-ones][jn-zeros] + 1)  # 没考虑到max
        
        return f[-1][-1][-1]

感觉现在对空间优化也小有心得:

  • 降维
  • 如果j有减操作,就将遍历顺序从大到小翻转
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        length = len(strs)
        dp = [[0] * (n+1) for _ in range(m+1)]

        for i in range(1, length+1):
            c0 = strs[i-1].count('0')     
            c1 = len(strs[i-1]) - c0      
            for j in range(m, -1, -1):    
                for k in range(n, -1, -1):
                    if j >= c0 and k >= c1:  
                        dp[j][k] = max( dp[j][k], dp[j-c0][k-c1] + 1 )
        return dp[-1][-1]

Leetcode494 目标和

使用动态规划的前提是先要进行一点小小的数学转换,将问题转化成动态规划问题。

就是我们假设,数组里面的数已经被赋予了符号,正数的和 p o s pos pos与负数的和 n e g neg neg(暂时都是正数): p o s − n e g = t a r g e t p o s + n e g = s u m pos - neg = target \\ pos + neg = sum posneg=targetpos+neg=sum所以我们可以得到: n e g = ( s u m + t a r g e t ) 2 neg = \frac{(sum + target)}{2} neg=2(sum+target)目标就变成了

  • 边界条件没有考虑周全
  • 使用len(nums) + 1来初始化数组,但是在使用索引遍历nums的时候,忘记-1
  • 忘记考虑neg为负数的情况就直接return 0
class Solution(object):
    def findTargetSumWays(self, nums, target):
        s = sum(nums)
        neg = (s + target) // 2
        if (s + target) % 2 == 1 or neg < 0:
            return 0

        f = [[0] * (neg+1) for _ in range(len(nums)+1)]

        f[0][0] = 1  # 边界条件

        for i in range(1, len(nums)+1):
            for j in range(neg + 1):
                if j < nums[i-1]:
                    f[i][j] = f[i-1][j]
                else:
                    f[i][j] = f[i-1][j-nums[i-1]] + f[i-1][j]
        return f[-1][-1]

空间优化后:

class Solution(object):
    def findTargetSumWays(self, nums, target):
        s = sum(nums)
        neg = (s + target) // 2
        if (s + target) % 2 == 1 or neg < 0:
            return 0

        f = [0] * (neg+1)
        f[0] = 1  # 边界条件

        for i in range(1, len(nums)+1):
            for j in range(neg, -1, -1):
                if j >= nums[i-1]:
                    f[j] = f[j-nums[i-1]] + f[j]
        return f[-1]

Leetcode1049 最后一块石头的重量II

这题和上一题挺像的,但是题目描述更贴近实际,就是抽象成数学问题要多走一步。

  • 集合:和leetcode 494差不多,也就是相当于将集合分成正负序列,因为对撞
  • 属性:max:最接近sum的一半
  • 状态转移:

出错点:

  • target值设置为了s//2+1,其实s//2才对。
  • f[j] = max(f[j], f[j-stones[i-1]] + stones[i-1])一开始这里忘记加回石头的重量了。
class Solution(object):
    def lastStoneWeightII(self, stones):
        s = sum(stones)
        target = s // 2  # 我原本设置为 s//2 + 1
        f = [[0] * (target + 1) for _ in range(len(stones) + 1)]

        for i in range(1, len(stones) + 1):
            for j in range(target + 1):
                if j < stones[i-1]:
                    f[i][j] = f[i-1][j]
                else:
                    f[i][j] = max(f[i-1][j], f[i-1][j-stones[i-1]] + stones[i-1])  # 这里也忘了加回石头重量
        return s - 2 * f[-1][-1]

空间优化:

class Solution(object):
    def lastStoneWeightII(self, stones):
        s = sum(stones)
        target = s // 2  # 我原本设置为 s//2 + 1
        f = [0] * (target + 1)

        for i in range(1, len(stones) + 1):
            for j in range(target, -1, -1):
                if j >= stones[i-1]:
                    f[j] = max(f[j], f[j-stones[i-1]] + stones[i-1])  # 这里也忘了加回石头重量
        return s - 2 * f[-1]

Day44 完全背包 + 零钱兑换II + 组合总和IV

Leetcode518 零钱兑换II

一开始按照0-1背包的思路去解,有一个k表示物品可以取无限次,没想到还能通过数学推导的方式化简掉。

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        
        n = len(coins)
        dp = [[0]*(amount+1) for _ in range(n+1)] 
        dp[0][0] = 1 
        
        for i in range(1, n+1):         
            for j in range(amount+1): 
                if j < coins[i-1]:      
                    dp[i][j] = dp[i-1][j]
                else:               
                    dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
        
        return dp[n][amount]

空间优化:

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        
        dp = [0]*(amount+1)         
        dp[0] = 1  
  
        for coin in coins:                 
            for j in range(coin, amount+1): 
                dp[j] += dp[j-coin]
        
        return dp[amount]

在完全背包的空间优化中,我理解了:

  • 内循环顺序:0-1背包和完全背包内循环顺序不一样,以及为啥不一样。
  • 内外循环是背包或者物体:主要根据要求的count是排列还是组合来进行区分。
    • 排列数:先遍历背包,再遍历物品
    • 组合数:先遍历物品,再遍历背包

但是amount内循环从coin开始遍历,我还是没理解完全。

Leetcode377 组合总和IV

你可能感兴趣的:(刷题,leetcode,算法,职场和发展)