240318 Leetcode相关题

递归:LeetCode70、112、509
分治:LeetCode23、169、240
单调栈:LeetCode84、85、739、503
并查集:LeetCode547、200、684
滑动窗口:LeetCode209、3、1004、1208
前缀和:LeetCode724、560、437、1248
差分:LeetCode1094、121、122
拓扑排序:LeetCode210
字符串:LeetCode5、20、43、93
二分查找:LeetCode33、34 BFS:LeetCode127、139、130、529、815
DFS&回溯::LeetCode934、685、1102、531、533、113、332、337
动态规划:LeetCode213、123、62、63、361、1230
贪心算法:LeetCode55、435、621、452
字典树:LeetCode820、208、648
这么多吗;

class solution 隐去的输入处理部分
【面试算法题总结01】输入输出处理基础(Java) - TickTick123
【刷题总结与梳理】近期做题补充&二刷&输入输出整理(c++)
没找到相关资料▲

def函数参数

调用函数时 可使用的正式参数类型:必需参数,关键字参数,默认参数,不定长参数(无用知识点)

LeetCode70

假设你正在爬 n 阶楼梯,每次可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:
要记得先初始化 dp = [0] * (n + 1) # 因为dp[0]无用
dp[n]=dp[n-1]+dp[n-2]
dp[1]=dp[0]=1
优化:
这种2阶的差分,优化时会有交替存储的现象:

for i in range(2, n + 1):
        a, b = b, a + b

法2:记忆化递归
噢,原来前面看到的HJxx的解法是记忆化递归,def里面有个def× 帮忙处理好了输入而已,非ACM模式
看了很久 感觉还是别扭,模仿不来,这个复杂度不知道怎么样▲

class Solution:
  def climbStairs(self, n:int) -> int:

    def dfs(i:int, memo) -> int:       # i:阶梯数;memo: memory
        if i == 0 or i == 1:
            return 1
        if memo[i] == -1:              # 看起来还是dp,从低到高更新dp,
            memo[i] = dfs(i - 1, memo) + dfs(i - 2, memo)
        return memo[i]
    return dfs( n, [-1]*(n+1) )        # -1 需表示没有计算过,n 为最大索引,因此数组大小为 n+1
LeetCode112 树的递归(看还有没有类似题练一下)

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum。
· 叶子节点 指没有子节点的节点。
思路:树的操作不熟练

法2:递归
转化为小问题:是否存在从当前节点的子节点到叶子的路径,其路径和为 sum - val。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def hasPathSum(self, root: TreeNode, sum: int) -> bool:
        if not root:                            # 这是啥▲ 噢 若该二叉树连根都没有
            return False

            if not root.left and not root.right:     # 若已递归到叶子结点
            return sum == root.val
        return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val)	# 写在传参里,就不用path记录了

感觉有点抽象,还是不太熟
法1:广度优先搜索 即队列
★该写法要熟悉
★.deque

class Solution:
    def hasPathSum(self, root: TreeNode, sum: int) -> bool:
        if not root:
            return False

        que_node = collections.deque([root])
        que_val = collections.deque([root.val])
        while que_node:
            now = que_node.popleft()              # 移除并返回
            temp = que_val.popleft()
            if not now.left and not now.right:
                if temp == sum:
                    return True
                continue
            if now.left:
                que_node.append(now.left)
                que_val.append(now.left.val + temp)
            if now.right:
                que_node.append(now.right)
                que_val.append(now.right.val + temp)
        return False
deque

Python标准库的collections模块提供了双端队列/deque,支持从任一端增删元素.
其方法:

deque(初始值):创建一个双端队列
append(x): 在双端队列的右侧添加元素x。
appendleft(x): 在双端队列的左侧添加元素x。
pop(): 移除并返回双端队列右侧的元素。
popleft(): 移除并返回双端队列左侧的元素。
extend(iterable): 在双端队列的右侧添加可迭代对象中的所有元素。
extendleft(iterable): 在双端队列的左侧添加可迭代对象中的所有元素。
clear(): 移除所有元素,使deque变为一个空的双端队列。
rotate(n): 将所有元素向右循环移动n步(如果n是负数,则向左循环移动)。
LeetCode113

给你二叉树的根节点 root 和一个整数目标和 targetSum,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
怎么和112题一样的,只是要找所有的

法2:广度优先搜索
怎么会这么长▲ 这要考肯定都忘了

class Solution:
    def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
        ret = list()
        parent = collections.defaultdict(lambda: None)

        def getPath(node: TreeNode):
            tmp = list()
            while node:
                tmp.append(node.val)
                node = parent[node]
            ret.append(tmp[::-1])

        if not root:
            return ret
        
        que_node = collections.deque([root])
        que_total = collections.deque([0])

        while que_node:
            node = que_node.popleft()
            rec = que_total.popleft() + node.val

            if not node.left and not node.right:
                if rec == targetSum:
                    getPath(node)
            else:
                if node.left:
                    parent[node.left] = node
                    que_node.append(node.left)
                    que_total.append(rec)
                if node.right:
                    parent[node.right] = node
                    que_node.append(node.right)
                    que_total.append(rec)

        return ret

法1:深度优先搜索

class Solution:
    def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
        ret = list()
        path = list()                # 为什么设俩
        
        def dfs(root: TreeNode, targetSum: int):
            if not root:
                return
            path.append(root.val)     # 
            targetSum -= root.val
            if not root.left and not root.right and targetSum == 0:
                ret.append(path[:])      # 怎么没看到在哪有修改 奇奇怪怪的 喔 心不在焉 认知能力下降
            dfs(root.left, targetSum)
            dfs(root.right, targetSum)
            path.pop()
        
        dfs(root, targetSum)
        return ret
LeetCode509 斐波那契数
LeetCode23 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
思路:输入两个list,返回一个list
★class
法1:分治

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        cur = dummy = ListNode()  # 哨兵节点dummy简化代码逻辑 cur为新list
        while list1 and list2:                # 二合一绝对是做过的,但见到代码并未立即
            if list1.val < list2.val:
                cur.next = list1              # 把 list1 加到新链表中
                list1 = list1.next
            else:  # =时加哪个节点都可以
                cur.next = list2              # 把 list2 加到新链表中
                list2 = list2.next
            cur = cur.next                    # 指向下一节点
        cur.next = list1 if list1 else list2  # 拼接剩余链表
        return dummy.next

    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        m = len(lists)
        if m == 0: return None  # 注意输入的 lists 可能是空的
        if m == 1: return lists[0]  # 无需合并,直接返回
        left = self.mergeKLists(lists[:m // 2])  # 合并左半部分
        right = self.mergeKLists(lists[m // 2:])  # 合并右半部分
        return self.mergeTwoLists(left, right)  # 最后把左半和右半合并

这个合并过程中还用了二分,学一下该写法,易读

分治与递归

分治是一种算法思想,递归是实现这种思想的一种手段

法1:最小堆
初始把所有链表的头节点入堆,然后不断弹出堆中最小节点 x 如果 x.next 不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来

Leetcode169 为什么是分治 应该不是这题

给定一个大小为 n 的数组 nums ,返回其中的多数元素,即出现次数>⌊n/2⌋ 的元素。
· 保证数组非空,且总是存在多数元素。
思路:感觉做过类似题,+-还是怎么着,k=0,=0时遇任何元素+1,非当前元素就-1

LCR 169. 招式拆解 II

某套连招动作 记作仅由小写字母组成的序列 arr,字符arr[i] 为第 i 个招式。请返回第一个只出现一次的字符,如不存在返回空格。
思路:这个前面也做过吧,不过忘了,可能创建一个list,if i in list吧
这里用的标记;之前也用的标记吗

class Solution:
    def dismantlingAction(self, arr: str) -> str:
        hmap = {}
        for c in arr:
            hmap[c] = not c in hmap    # 字典,键值对,c的值由无变1变0
        for c in arr:
            if hmap[c]: return c
        return ' '
Leetcode240 搜索二维矩阵 II 为什么是分治

编写一个高效的算法来搜索 mxn矩阵matrix 中的一个目标值。该矩阵有以下特性:
每行元素从左到右升序排列;每列元素从上到下升序排列。
思路:dp吗× 靠数学逻辑
以右上角顶点为起始点,若当前值<目标值,行序加1即下移;若>目标值,列序减1即左移,直到找到或越界(非 i<r and j>=0)
从左下角开始增也是可以的
该法含义大概是,当行/列序移动,相当于排除了该行/列;若从右上角剔除行/列,剩余矩阵能具有相同的升序性质;而每次剔除的都能保证不是,因此O(n)次内能得到结果
按这么说,从任意固定点开始,好像都可以▲ 右上角优在什么地方

Leetcode84 柱状图中最大的矩形 单调栈 代码忘了已经

思路:做过 用的啥来着 数学
以某种方式计算得的值,往某个方向推后 之前的不必再计算
法3:想一下,最大的矩形,也就是两个端点,固定一端如左端后,右端s更大的,在左端右移后还可能会被替换吗,不知道× 管它替不替换,两个for遍历并用tmp比较就可以了,时O(n²)
为什么是栈呢
那会了该题后是不是能用于任意f
法2:单调栈
函数定义里老把类型声明看成某个新变量
★list.pop(index=-1)方法:默认=-1,移除末尾

class Solution:
    def largestRectangleArea(self, heights:List[int]) -> int:
        stack = []  # 用来暂存数组子串的下标
        heights = [0] + heights + [0]
        res = 0
        for i in range(len(heights)):
            #print(stack)
            while stack and heights[stack[-1]] > heights[i]: # 当出现比目前右端更低的柱形,将右端出栈,计算一下该柱形对应的最大面积;而且是while不断判断
                tmp = stack.pop()
                res = max(res, (i - stack[-1] - 1) * heights[tmp])
            stack.append(i)     # 每次都会入栈的
        return res

这看完怎么像滑动窗口 看不懂▲ 看评论区懂了一点;来看第二篇题解:
考虑以第i根柱子为最矮柱子所能延伸的最大面积,然后遍历i.
为此,我们需要:
左边看一下,看最多能向左延伸多长,找到≥当前高度的最左边元素的下标;
右边看一下,看最多能向右延伸多长;找到≥当前高度的最右边元素的下标。

from typing import List
class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        res = 0
        for i in range(len(heights)):
            cur_height = heights[i]
            left = i
            while left > 0 and heights[left - 1] >= cur_height:
                left -= 1

            right = i
            while right < size - 1 and heights[right + 1] >= cur_height:
                right += 1
            res = max(res, (right-left+1) * cur_height)
        return res

目前由于是for+while,时O(n²).
继续考虑以第i根柱子为最矮柱子所能延伸的最大面积:
通过一些实例,我们发现,只要是遇到了当前柱形的高度比它上一柱形的高度严格小的时候,一定可以确定它之前的某些柱形的最大宽度.
由此得出,当我们设法确定以当前柱形为右端对应的左边界时,找的是第一个严格小于当前柱形的下标。此时中间的柱形可以当做不存在。
这说明什么呢?这说明我们可以从最高的柱形开始考虑其最大面积× 又不是只有单凸的形状
这说明当我们遍历1次得知各柱形高度后,每次计算各高度对应最大宽度,不需要遍历,而是可以通过数学计算的,
具体是通过,将柱形下标依次入栈,如果递增就保留;
当我们遇到比目前右端更低的柱形,将右端出栈,计算一下该柱形对应的最大面积;不断判断(最后剩下的形状就是递增吧,毕竟有凸的必然在某个时刻出栈了)
实现:
该方法在实现中要处理两种特殊情况:

  1. 弹栈的时候,栈为空,这时没法取stack[-1]
  2. 遍历完成以后,栈中还有元素,那要单独算他们各自的矩形面积,比较麻烦

为此,
我们在输入数组后两端加上两个高度为 0 (<1即可)的柱形,可以回避上面这两种分类讨论。
有了这两个0:
左边的0,比任一元素小,不会出栈,因此栈一定不会为空;
右边的0,比任一元素小,最后通过while会使剩余元素出栈,最后剩下俩0

这俩0的柱形即哨兵(Sentinel);这里栈元素对应的柱形高,(发现在变动中)呈单调增加不减的形态,因此称为单调栈(Monotone Stack),它是暴力解法的优化。计算每个柱形对应的最大矩形的顺序,由出栈顺序决定。

另外,有人还进行了一点优化:

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = [-1]
        heights.append(0)
        mxarea = 0
        for i, v in enumerate(heights):
            while heights[stack[-1]] > v:    # 当出现比目前右端即[stack[-1]更低的柱形
                mid = stack.pop()
                area = heights[mid] * (i - 1 - stack[-1])
                if area > mxarea: mxarea = area
            stack.append(i)
        return mxarea

其优化说明:不想用[0] + heights,因为在数组前部追加的速度很慢,由于python没有优化,复杂度能有O(N);那么这么处理,一方面栈里初始放一个-1,另一方面在数组尾部添一个0。这样当数组遍历完后,heights[stack[-1]]=heights[-1]=0,也能使得剩余元素都出栈(前面的是通过=heights[0]=0).

不对啊 按这个说法,其优化的地方是在栈里面先放一个东西,以优化速度;另外他发现,放0放-1都行.本题暂完.

Leetcode85

给定一个仅包含 0 和 1 、大小 rows x cols 的二维二进制矩阵,找出只含 1 的最大矩形,并返回其面积。
思路:for i for j 如果递减 啊不是;得到每行/列最长的连续子串,设法拼起来×不一定都是最长子串拼起来,但每个最长子串都可以算得一个矩形
法1:84题的扩展。题解没那么好理解;图解一下就理解了,以第n行下底作为x轴,观察前n行,便是柱状图;计算完前1行的最大矩形面积后,下一行的heights可由前1行推出,便计算以第2行为底的最大矩形面积;最后max{}即可
用python的话代码短很多

class Solution:
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        if not matrix:return 0
        m,n=len(matrix),len(matrix[0])    # m行n列
        pre=[0]*(n+1)                     # heights,其上方有几个连续1;注意这里末尾补了0了
        res=0
        for i in range(m):
            for j in range(n):
                pre[j]=pre[j]+1 if matrix[i][j]=="1" else 0    # 自然地翻译 它说是前缀和
            stack=[-1]                    # 这个-1可能一直不会动
            for k,num in enumerate(pre):
                while stack and pre[stack[-1]]>num:  # 当栈尚为单调栈
                    index=stack.pop()
                    res=max(res,pre[index]*(k-stack[-1]-1))
                stack.append(k)

        return res
Leetcode739

给定一个整数数组 temperatures 表示每天温度,请返回数组,answer[i] 指对于第 i 天,下一个更高温度出现在几天后。若无则为0。

思路:就是问如何优化,感觉可以数组储存相邻的差值,但怎么判断±呢,<0则入栈 不行;噢好像想到了,用delta[]存储目前累积变化总和,出栈时将其保存到tmp里,每次可用出栈元素-tmp得到temperature队首的元素
法1:从后往前遍历并判断,遇到比栈顶大的数,栈顶就可以出栈并计算该点答案了;遇到比栈顶小的数,栈顶也已经可以计算该点答案,但并不出栈;也就是说,遇到一个数如5时,其右侧已经从出栈的都是≤5的数,不可能再被考虑了;而≥5的数不出栈的原因,是往前还可能遇到≥5但<该数的数。因此,栈内温度从后往前应当是递减的》

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        ans = [0] * n
        st = []                     # 栈仍是存的下标
        for i in range(n - 1, -1, -1):
            t = temperatures[i]
            while st and t >= temperatures[st[-1]]:  #
            # 哎呀 while、if、append顺序不能换 前面说错了
                st.pop()
            if st:                  # 若栈不空(这种特殊情况说明▲),说明t≤栈顶,符合题意,注意下标已知,可得该点答案
                ans[i] = st[-1] - i
            st.append(i)            # 注意入栈是都入的,只是出栈的时候一次性把较t小的栈顶数都出了,然后接下来的一个或几个for会通过if算得这些点的答案
        return ans

时O(n²),虽然有for+while,但每个元素只出入栈一次;空O(min{n,U})),U为temp最大差值(因为栈内不会存在相同元素)

法2:从前往后,原理一样的,一旦发现比栈顶高的,将栈顶就出栈,同时while输出相关答案,这个感觉好理解一点,代码也易读,t一旦<栈顶,意味着<栈内的所有数,即无法更新栈内元素
两个方法的共同点实际是:及时去掉后面无用的数据,保证栈中数据有序;计算内容涉及上一/下一更大/小的元素;题目要计算什么,while里就写什么.

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        ans = [0] * n
        st = []
        for i, t in enumerate(temperatures):
            while st and t > temperatures[st[-1]]:
                j = st.pop()
                ans[j] = i - j
            st.append(i)
        return ans
Leetcode42 接雨水

接雨水有前后缀、相向双指针做法,竖着算;也有单调栈做法,横着算每个小矩形
从最低的坑开始算,找稍高的坑,不断填坑
★enumerate的使用 看起来会偶尔用到

class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        st = []
        for i, h in enumerate(height):
            while st and h >= height[st[-1]]:  # 当目前的右端足够高,一定有某些矩形是可以算了的;就可以依次将栈顶作为左端 不断出栈并计算相应的横着的小矩形.
                bottom_h = height[st.pop()] # 目前坑的最低高度
                if not st:  # len(st) == 0
                    break
                left = st[-1]   # 为什么呢
                dh = min(height[left], h) - bottom_h  # 面积的高
                ans += dh * (i - left - 1)
            st.append(i)
        return ans

法1:前后缀分解 对于每个竖着的柱形i及其h,其装水后左端右端高度可以分别通过递推算得(左/右端没有更高的时左/右端高度记为h),最后对每个竖着的柱形,减去其h算得面积

class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre_max = [0] * n  # pre_max[i]:从height[0] 到 height[i] 的最大值
        pre_max[0] = height[0]
        for i in range(1, n):
            pre_max[i] = max(pre_max[i - 1], height[i])     # 装水后的高度是前缀pre中最高的那个高度

        suf_max = [0] * n  # suf_max[i]:从 height[i] 到 height[n-1] 的最大值
        suf_max[-1] = height[-1]
        for i in range(n - 2, -1, -1):
            suf_max[i] = max(suf_max[i + 1], height[i])

        ans = 0
        for h, pre, suf in zip(height, pre_max, suf_max):
            ans += min(pre, suf) - h  # 累加每个水桶能接多少水
        return ans

注:后两个 for 循环可以合并成一个循环,为了阅读没有合并

法2:相向双指针:在法2上进行优化,这里的pre_max感觉是指左柱形的右端,好像也不是 为什么可以直接-h;
回看ppt:还有些性质没用:有些suf_max的信息是多余的,不必求 写法:若从左往右的pre[i]>h[i],则pre的值可以延展到pre[i+1];同样地,若从右往左的suf>h,则...
而且不延展的时候,pre[i+1]就=h[i+1],故数组pre[i]可以O(1)的空间求得,时间仍O(n)

class Solution:
    def trap(self, height: List[int]) -> int:
        ans = left = pre_max = suf_max = 0
        right = len(height) - 1
        while left < right:
            pre_max = max(pre_max, height[left])
            suf_max = max(suf_max, height[right])
            if pre_max < suf_max:              # 这个if else什么意思呢 这是数学:如果前缀高度<后缀高度,那么无论形状如何,该点柱形高度都是前缀高度,同时前缀可以向右延展,变成下一柱形的前缀;后缀同理.
                ans += pre_max - height[left]  
                left += 1                      # 把前缀向右扩展
            else:
                ans += suf_max - height[right]
                right -= 1
        return ans
Leetcode46 全排列 回溯算法

它是for n[i] 下 n[0:i]+n[i+1:] 的全排列.那么已经选择的数字在 当前 要选择的数字中不能出现
★基本 >> 每个位向右移动,等于除以2
& 按位运算符

from typing import List
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def dfs(nums, size, depth, path, state, res):
            if depth == size:
                res.append(path)
                return

            for i in range(size):
                if ((state >> i) & 1) == 0:
                    dfs(nums, size, depth+1, path+[nums[i]], state^(1 << i), res)
# 通过传新的Path
        size = len(nums)
        if size == 0:
            return []

        state = 0
        res = []
        dfs(nums, size, 0, [], state, res)
        return res
from typing import List
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def dfs(nums, size, depth, path, used, res):
            if depth == size:        # 判定return顺序要在子问题前
                res.append(path[:])  # 需要append一个新的,否则遍历完成后path为[]
                return

            for i in range(size):
                if not used[i]:
                    used[i] = True
                    path.append(nums[i])

                    dfs(nums, size, depth+1, path, used, res)

                    used[i] = False
                    path.pop()

        size = len(nums)
        if len(nums) == 0:   # 考虑特例
            return []
        used = [False for _ in range(size)]
        res = []
        dfs(nums, size, 0, [], used, res)
        return res

if __name__ == '__main__':
    nums = [1, 2, 3]
    solution = Solution()
    res = solution.permute(nums)
    print(res)

较短的写法,将信息都放在了参数里,是吗▲

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res= []
        def f(a):
            if len(a)==len(nums):
                res.append(a)
            for i in range(len(nums)):
                if nums[i] not in a:
                    f(a+[nums[i]])
        f([])
        return res
Leetcode503 下一个更大元素 II

给定一个循环数组,返回 nums 中每个元素的 下一个更大元素 。若不存在,输出 -1

LeetCode547 基本的dfs,bfs题

有 n 个城市,其中一些彼此相连,另一些没有相连,isConnected[i][j] = 1 表示城市 i,j 直接相连,= 0 表示不直接相连。
间接相连:若城市 a 与 b 直接相连,b 与 c 直接相连,那么 a 与 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。返回矩阵中 省份 的数量。

思路:挺有意思 要想不重不漏的话怎么办呢,一个朴素的办法是取出来就置-1;
必然是深度/广度优先搜索,但怎么写呢
法1:深度优先
这里只用了一个参数,应该是先写的两个再优化成了一个×
这是个对称矩阵,不用两个参数好像;
就反复查[指定的i][j]就行了,写法:先判断合法并path记录,再递归

class Solution:
    # 看这个数组的类型声明 为List[List[int]]
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        def dfs(i: int):
            for j in range(cities): # 依次查[指定的i][j]
                if isConnected[i][j] == 1 and j not in visited:
                    visited.add(j)  # 写法:先判断合法并path记录,再递归
                    dfs(j)          #
        
        cities = len(isConnected)
        visited = set()     # 注意看.add(j)就行了 因为该列必定是同省的
        provinces = 0
        for i in range(cities):
            if i not in visited:
                dfs(i)
                provinces += 1
        return provinces

法2:广度优先
★当然是用队列+forfor实现,还得熟练
★还是用的deque
set()
这题用广度优先好易读啊,写法:先path记录,然后for,没有递归

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        cities = len(isConnected)
        visited = set()
        provinces = 0
        
        for i in range(cities):
            if i not in visited:
                Q = collections.deque([i])
                while Q:               # while Q
                    j = Q.popleft()    # 因为pop()了 所以要将相关元素 即相关行入列
                    visited.add(j)     # 写法:这里是先path记录,然后for,没有递归
                    for k in range(cities):
                        if isConnected[j][k] == 1 and k not in visited:
                            Q.append(k)
                provinces += 1
        return provinces

法3:并查集 把不同连通分量合并
实现是通过一维list存信息,找parent,改parent,有文章说是哈希表
感觉有点难写

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        def find(index: int) -> int:
            if parent[index] != index:
                parent[index] = find(parent[index])
            return parent[index]

        def union(index1: int, index2: int):    # union操作:二合一
            parent[find(index1)] = find(index2)	# 递归找到某城市parent,修改它为另一城市parent

        cities = len(isConnected)
        parent = list(range(cities)   # 所有城市的parent先设为本身
        for i in range(cities):
            for j in range(i + 1, cities):      # 下三角
                if isConnected[i][j] == 1:
                    union(i, j)
        provinces = sum(parent[i] == i for i in range(cities))
        return provinces
LeetCode200 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿:总是被水包围,且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
· 可假设网格四边被水包围

思路:老早见过这道,当时好像想用dfs,应该也是可以的,就是麻烦一点
那就是并查,岛屿扩充不断二合一吧,结构感觉也差不多,不同的是不是只有find和union呢×
不是,除了找parent、改parent,还有判定什么时候改parent;上一题是因为能容易地判定parent;这里find和union反而完全没变
法1:

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        def find(x) :
            if p[x] != x :
                p[x] = find(p[x])
            return p[x]
        def union(x, y) :                  # 改parent
            px, py = find(x), find(y)
            if px == py :
                return
            p[px] = py
        leny = len(grid)                   # 取矩阵行列长度
        lenx = len(grid[0])
        p = list(range(lenx * leny))       # 设parent也和上题一样
        for i, row in enumerate(grid) :
            for j, v in enumerate(row) :
                if v == '1' :
                    for a, b in [[0, -1], [-1, 0]] :     # 相当于两个for 或四个if 美观一点
                        x, y = i + a, j + b
                        if 0 <= x < leny and 0 <= y < lenx and grid[x][y] == '1':
                            union(i * lenx + j, x * lenx + y)
        ans = 0
        havesee = dict()
        for i in range(lenx * leny) :
            if find(i) not in havesee and grid[i // lenx][i % lenx] != '0':
                havesee[p[i]] = 1            # 连最后这个for都没怎么变
                ans += 1
        return ans

法2:dfs
for x,y in [[0,1],[1,0],[0,-1],[-1,0]]:

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        row = len(grid)
        col = len(grid[0])
        ans = 0

        def DFS(i,j):
            grid[i][j] = "0"           # path记录 遍历过就置0
            for x,y in [[0,1],[1,0],[0,-1],[-1,0]]:      # 这样写不错
                tmp_i = i + x
                tmp_j = j + y
                if 0 <= tmp_i < row and 0 <= tmp_j < col and grid[tmp_i][tmp_j] == "1":
                    DFS(tmp_i,tmp_j)            # 结构大概一样的 path记录完再递归
        for i in range(row):
            for j in range(col):
                if grid[i][j] == "1":   # path初始置1
                    DFS(i,j)
                    ans += 1
        return ans
LeetCode684

将树看作一个连通且无环 的无向图,给定一个 往一棵节点值 1~n 的树中添一条边后 的图。请找出一条可以删去的边,删后可使剩余部分是一个有着 n 节点的树。若多个答案,返回数组 edges 中最后出现的那个。
· 添加的边的两个顶点在 1 到 n 中间,且这条边不为已存在的边。
· 图的信息记录于长度为 n 的二维(×✓ [[]]确实是二维)数组 edges ,edges[i] = [ai, bi] 表示 ai 和 bi 之间有边。
示例:

edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
>>> [1,4]

思路:不是二叉树;要返回具体答案的;
几个概念
1.集合树:以代表节点为父节点的所有节点构成的多叉树,好像就是指多叉树
2.节点的代表节点:可理解为节点的父节点
3.集合的代表节点:可理解为根节点
vector[2] = 2,意味着2这个节点所在集合的代表节点就是2
该解释有点复杂,不继续看了

法1:

  1. 初始时,每个节点都属于不同的连通分量
  2. 遍历每一条 ,判断这条边连接的两个顶点是否属于相同的连通分量。
    2.a. 如果两个顶点属于不同的连通分量,则说明在遍历当前边之前,这两个顶点间不连通,因此该边不会导致环出现,即可合并这两个 顶点的连通分量
    2.b. 如果两个顶点属于相同的连通分量,则说明在遍历当前边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,即答案,将其返回。

find和union还是一样的
★再次,for node1, node2 in edges:

class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        parent = list(range(n + 1))

        def find(index: int) -> int:
            if parent[index] != index:
                parent[index] = find(parent[index])
            return parent[index]

        def union(index1: int, index2: int):
            parent[find(index1)] = find(index2)

        for node1, node2 in edges:
            if find(node1) != find(node2):
                union(node1, node2)
            else:
                return [node1, node2]     # 即为答案
        return []         # 说明未找到
Leetcode209

找出数组中满足总和≥ target 的长度最小的 连续子数组,并返回其长度。若不存在返回0.
思路:已知考滑动数组,可能需要个minLen保存当前最小长度
由数学,在找到首个满足条件的切片后,这些最小的连续子数组可以由滑动窗口得出,先滑出,再滑入至恰好≥
★enumerate

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)
        ans = n + 1  # 也可以写 inf
        s = left = 0
        for right, x in enumerate(nums):  # 枚举子数组右端点 一个标位置 一个
            s += x
            while s - nums[left] >= target:  # 若剪完满足target,左端点便右移;这里有点绕,最后得到的是固定右端点时满足target的最短切片,然后便执行下面的if
                s -= nums[left]
                left += 1                  #
            if s >= target:
                ans = min(ans, right-left+1)
        return ans if ans <= n else 0

里面也可以改成 且看起来更易读:

for right, x in enumerate(nums):
            s += x
            while s >= target:
                ans = min(ans, right - left + 1)
                s -= nums[left]
                left += 1

两个for吗×

for
fruits = ["apple", "banana", "orange"]
prices = [1.0, 2.0, 3.0]
fruits_dict = {}

若希望将这两个列表中的数据填充到字典中,就可以用

for fruit, price in zip(fruits, prices):
    fruits_dict[fruit] = price

复习:zip()函数返回一个迭代器,该迭代器是从每个可迭代对象中聚合元素
这种for方式等同于

for i in range(len(l):
    L1[i] = L2[i]
enumerate

可作用于任何可迭代对象,生成一个带索引的 键-值对格式的可迭代对象
语法: enumerate(iterable, start)
示例:

List = ["hello", "tutorialspoint", "python", "codes"]
enumerate_obj = enumerate(List)

print(enumerate_obj)
print(list(enumerate_obj))
>>> <enumerate object at 0x7f4e56b553c0>
>>> [(0, 'hello'), (1, 'tutorialspoint'), (2, 'python'), (3, 'codes')]

本题先到这里,耗了很多时间

Leetcode3

给定一个字符串s,请找出其中不含有重复字符的 最长子串的长度
思路:这个前面做过,用切片比,然后maxLen只增不减,for左端点、for右端点(左+maxLen)

# Step 4 - 情况1
#     如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度 
#     如果达到了,左指针移一个单位,保证右指针右移时,窗口长度不变,
#     左指针移动之前, 更新Step 1的维护变量

# Step 4 - 情况2
#     如果窗口长度可变: 一般涉及到窗口是否合法的问题
#     如果当前窗口不合法, 用一个while去不断移动左指针
#     在左指针移动之前,更新Step 1的维护变量

×题不一样,那题是找最长重复子串,这题是最长不重复子串
不,这题就是慢慢地移动左指针,不断移动右指针去记录频率并判断合法性,不行左指针就下一个
★又用到字典.get(,) 参数2是键找不到的时候吗 对的,字典.get(key, default)

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # Step 1: 维护变量max_len, 涉及去重故需要哈希表
        max_len, hashmap = 0, {}

        # Step 2: start慢慢移
        start = 0
        for end in range(len(s)):
            # Step 3: 更新维护的 hashmap, max_len
            # 把窗口末端元素加入哈希表,使其频率加1,并且更新最大长度
            hashmap[s[end]] = hashmap.get(s[end], 0) + 1

            if end - start + 1 == len(hashmap):        # 这个if的顺序可和while交换
                max_len = max(max_len, end - start + 1)

            # Step 4: 非法就不断移动左指针, 剔除直到窗口再次合法
            # ★while 窗口长度>哈希表长度,说明存在重复元素,不合法,左指针开始右移
            while end - start + 1 > len(hashmap):
                head = s[start]
                hashmap[head] -= 1
                if hashmap[head] == 0:
                    del hashmap[head]

                start += 1
        return max_len
Leetcode1004

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,返回连续 1 的最大个数
思路:若已知用滑动窗口,那就能想到这是个可变窗口,合法窗口 即 可含k个0,那很简单了

Leetcode1208 尽可能使字符串相等

规定字符a变b 需要ASCII码差值绝对值的开销,用于变更字符串的最大预算是 maxCost.
请返回将 s的子串 转化为 t中对应的子串时,可以转化的最大长度。
示例:

s = "abcd", t = "cdef", maxCost = 3
>>> 1
说明:s中任一字符 变成t中对应字符 的开销都是 2。因此最大长度为 1。

思路:那就是要确定四个点呗,即是确定两个点,两个端点变动时,结果的变化完全来自于边界的变化

LeetCode724

定义 数组的中心下标:其左侧所有元素的和 = 右侧所有元素的和。
· 若位于最左侧,其左侧数之和视为 0
请计算数组的中心下标,若有多个,返回最左侧的;若无返回 -1.
思路:感觉是简化版的窗口类型
★sum(数组)函数

LeetCode560
LeetCode437
LeetCode1248
posted on 2024-03-22 10:58  蠢的刘龙  阅读(1)  评论(0编辑  收藏  举报