第一次知道还要考虑区间,但是感觉双闭的区间和代码更对称,更好理解一些。就是while有没有等号的区别。
重点:left
,right
和mid
的更新
mid
:只有left <= right
的时候更新,是最简单的,每次循环都会更新,left == right
的时候就是要结束的时候了。left
和right
:因为while的条件包含=
,所以是双闭区间,在当前循环中,已经比较过nums[mid]
和target
了,下次循环就不需要再考虑mid
本身了。所以要向前或者向后缩小范围。考虑到left
,right
两个整数相加得到的整数可能会溢出,所以采用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
看到“原地”,就知道只能在原数组操作了,一开始只想到快慢指针的思路,但是相向双指针也可以。
本质:双指针交换元素
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
情况进行判定。
一看到题目,就想着用双指针,只不过一开始有点被昨天的题目影响了,想在原数组进行操作,结果没操作出来。看了一眼答案,发现新定义了一个数组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
滑动窗口可解,也是快慢指针,要有一个total
来记录窗口内的元素之和,还要有一个index
来记录窗口索引长度。
这么看这次要更新的有4个参数:slow
,fast
,total
,index
slow
:total > target
时更新fast
:跟着while循环更新total
:slow
更新之前要减去nums[slow]
index
:total > 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
记得前段时间写过这道题,但是现在也忘了(拍脑门)
但是有思路,应该是要模拟,要考虑边界问题,就像704二分查找的第二种解法,应该是左闭右开的沿着边走;
range()
就是一个完美的左闭右开范围。n*n
的数组left
和right
,还有top
和bottom
相等的时候,已经是到中心点了,循环就要结束了。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
周末要整理一下怎么初始化数组。
和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
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性: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个属性,val
,next
,index
和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
感觉做完上一题设计链表,确实对链表的遍历理解加深了。pre
和post
有种快慢链表的味道,只不过他们一直相差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的前一个节点,所以相当于虚拟头结点了,这题因为翻转了,可以这么返回
感觉画图会更好理解一点,其实根据原理和链表翻转类型,可以视为进阶题目。
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
重点:是逆着箭头的走向进行更新。
之前刷过一次,使用了两次扫描,第一次得到链表的长度,第二次进行删除,删除操作和设计链表的删除功能差不多。
# 两次扫描
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步,然后fast
和slow
同时移动,fast
到结尾了,就删除slow
的节点。
出错点:将dummy.next
用来初始化slow
和fast
了,感觉对边界还是不是很熟悉
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
一开始确实没想到,感觉更像数学题目:
headA
和headB
的总长度,如果有交点的话,第二段的最后肯定是相同。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
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
一次就ac了,感觉还是第一次,没啥好说的。
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])
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 快乐数
虽然挺简单的,但是一开始还是没思路
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
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]
第一眼还没想出来,看了视频,其实感觉和两数之和差不多,可能是双重循环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
这题挺简单的,基本和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
感觉这题的关键在于去重,一开始没注意审题,以为只要索引不同就行了,原来是要三元数组也要不一样才满足条件
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
看到之前的解法,是用中间扩散法+后处理的去重,时间确实慢很多。
确实和三数之和比较像,就是多了一层循环。
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
感觉这题挺简单的
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
这题也挺简单的,学了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)
新建了一个数组,挺简单的,如果有时间的话,做一下就在原数组上操作的解法。
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)
这题简直超级加倍,一旦不能使用库函数split之后,就变得特别的麻烦,和设计链表有得一拼,一题更胜3题。
就是反转再反转,如果使用切片或者反转函数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号的题目大致刷完了。感觉可以对数组字符反转类的做一个总结。
又是两道模拟题,我估计最讨厌的就是模拟类型的题目了。
第一和第二题的相互实现,其实可以放在一起来讲。
首先要知道,队列和栈分别有什么操作:
queue.push
:queue.pop
:queue.peek
:queue.empty
:以上是队列的操作,栈的操作有 :
stack.push
stack.pop
stack.top
stack.empty
然后就是两种数据结构之间的操作是如何映射的。
看了代码随想录的动画,要实现队列的实现需要两个栈,一个stack-in
,一个stack-out
,分别对进出队列进行处理。至于队列的操作和两个栈的更新有什么映射操作,我观察:
queue.push
:只需要stack-in
正常push
就行了。(这就是stack-in
的更新)queue.pop
:出队列就需要pop
操作stack-out
栈了,如果stack-out
栈是空的话,就需要先将stack-in
栈的元素给pop
出来然后push
给stack-out
,如果stack-in
也是空的话,那队列就是empty
了。queue.peek
:就相当于stack-out.top
了。queue.empty
:stack-in
和stack-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)
感觉python也只能用list来模拟队列
栈的操作:
stack.push
:queue.push就可以实现stack.pop
: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]
括号只有配对了才有效:
最后为空就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
其实感觉和删除括号那题差不多。都是对称匹配的问题,使用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的两天还没有时间去看了。准备之后慢慢补,目前就按着进度继续打卡好了。
好像之前做过,其实了解了这个逆波兰表达式的运算步骤的话,挺简单的了,但是还是出了一些小问题,没有对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())
第一眼看见题目,就想着使用暴力法求解,但是我看了一眼今日的标题,看起来事情并没有这么简单,得往栈和队列那里凑。好吧,想不出来。
看了题解,主要有两种常见的思路:
优先队列
O(logn)
O(n)
,出队是O(1)
的复杂度动态规划入门的基础题了,但是一开始还是没搞对,只考虑了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
来了一道动态规划的题目,因为不熟悉,打算使用闫式DP分析法来分析一波,理清一下思路。
f(n)
吧f(n)
表示什么呢? -> 集合:所有爬上n阶楼梯的方法f(n-1)
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]
自己的错误思路 | 正确的思路 | |
---|---|---|
集合 | 爬上n 阶楼梯的所有方案集合,f(i) 表示前i 步最小的cost |
同左,但是有一点没有考虑,cost 数组长度为n ,数组索引为 n-1 ,就是说上n 阶台阶,就要将n-1 的cost 算上。 |
属性 | 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]
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]
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部分来考虑:
自己的错误思路 | 正确的思路 | |
---|---|---|
集合 | f(n) 表示分成k 个数的最大乘积 |
同左 |
属性 | max :最大的乘积 |
同左 |
状态方程 | f(n) 可以分成1 ,n-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]
首先,这题踩的坑还是有点多的,也有很多疑问点:
n
本身比拆分n
之后相乘还要大的情况。f
要初始化成n+1
个元素呢?(初始条件)
f[0]
是没有任何意义的,也就是索引和n
差了一位,就是f[n]
对应数字n
。f[0],f[1]
,这题的初始条件相当于是f[1], f[2]
。f[i]
也要放进去比较大小?
max
函数内的f[i]
只是用来记录上一个状态的,但是是否所有的动态规划求极值都要有上一个状态?这个值得思考。就是给定一个数字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)
一开始碰到这道题,我也知道要用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
范围内选择,但是重量的上限还是j
:f(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
,其他为Falsef(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]
有了上面对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
:
i
:f[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]
使用动态规划的前提是先要进行一点小小的数学转换,将问题转化成动态规划问题。
就是我们假设,数组里面的数已经被赋予了符号,正数的和 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 pos−neg=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]
这题和上一题挺像的,但是题目描述更贴近实际,就是抽象成数学问题要多走一步。
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]
一开始按照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]
在完全背包的空间优化中,我理解了:
count
是排列还是组合来进行区分。
但是amount
内循环从coin
开始遍历,我还是没理解完全。