算法面/笔试刷题

规律

基础操作

## set
s.add(x)

## string
s.replace(old,new) #不改变s
s.count(char)
s.find(substring) #不存在返回-1

## list
s.sort(key=lambda x:x[0],reverse=False) #默认递增,且改变原序列
s.reverse()
''.join(s)

## dict
d.pop(key)
#计数可以直接加1
from collections import defaultdict 
d = defaultdict(int)
for c in s:
	d[c] += 1
#计数
freq = collections.Counter(nums)
ans = [num for num, occ in freq.items() if occ == 1][0] #只出现一次

## 位运算
ord('A')  # ASCI
chr(ord('A'))  # ='A', chr翻译ASCI

## 声明nonlocal
nonlocal var

1 一次编辑

01.05

思路:

  • 两字符串 first , second 的长度之差 ≤1 ;
  • 当 first , second 长度相等时,两字符串各对应位置只有一个字符不同;
  • 当 first , second 长度之差为 11 时,「较短字符串」仅需在某位置添加一个字符,即可转化为「较长字符串」;

trick:保证first更短

2 旋转矩阵

01.07
(自己发现的规律非常复杂)

将矩阵旋转90度,找规律发现

  • (row, col) --> (col, n-row-1)
  • 继续应用该规律找到(col, n-row-1)元素的变换位置
    (col, n-row-1) --> (n-row-1, n-col-1)
  • (n-row-1, n-col-1) --> (n-col-1, row)
  • (n-col-1, row) --> (row, col)

3 螺旋矩阵

NC38

法一:模拟

  • 对于每一圈,先从左至右(top,left)->(top,right);从上到下(top+1,right)->(bottom,right);从右至左(bottom,right-1)->(bottom,left-1);从下至上(bottom,left)->(top+1,left)
  • 注意对于一行或者一列的情况,容易出现重复,所有后两个遍历需要加条件判断 right > left and bottom > top

法二:旋转90度

  • 每次取当前矩阵的第一行元素,将剩余元素旋转90度就是下一批要进入的
  • 利用zip重新组合成对应顺序
def spiralOrder(matrix: List[List[int]]) -> List[int]:
    res = []
    while matrix:
        res += matrix[0]
        matrix = list(zip(*matrix[1:]))[::-1]//每次取出来第一行,然后对剩下yuan
    return res

4 超过一半的数字

NC73
候选法:如果两个元素不相等,则消去这两个元素,最后遗留的就是众数

  • cand:候选元素 cnt:出现次数
  • if cnt=0,cand=cur cnt = 1
  • if cand==cur, cnt+=1; else cnt -=1

5 进制转换

NC112

digits = [str(i) for i in range(10)] + [chr(i + ord('A')) for i in range(6)]

6 腾讯二面:最大公约数

  • 辗转相除:保持a>b,计算余数c
def solution(a,b):
    if a < b:
        a, b = b, a
    c = a % b
    while c != 0:
        a, b = b, c
        c = a % b
    return b

7 快手二面:计算AUC

  • 时间复杂度 O ( M + N ) O(M+N) O(M+N)
def auc_calc(instances):
    pos, neg = [], []
    for i in instances:
        if i[0] == 1:
            pos.append(i[1])
        else:
            neg.append(i[1])
    pos.sort()
    neg.sort()
    M, N = len(pos), len(neg)
    n = 0
    cnt = 0
    for m in range(M):
        while n<N and pos[m] > neg[n]:
            n += 1
        cnt += n
        while n<N and pos[m] == neg[n]:
            cnt += 0.5
            n += 1
    return cnt/(M*N)

8 优美的排列

优美的排列 排序1-n使得相邻位置差值的绝对值有k种

答案为 [ 1 , 2 , ⋯ , n − k , n , n − k + 1 , n − 1 , n − k + 2 , ⋯ ] [1,2,⋯,n−k,n,n−k+1,n−1,n−k+2,⋯] [1,2,,nk,n,nk+1,n1,nk+2,]

def constructArray(n,k):
    ans = list(range(n+1))
    t = 0
    for i in range(n-k,n+1,2):
        ans[i] = n-k+t
        t += 1
    t = 0
    for i in range(n-k+1,n+1,2):
        ans[i] = n-t
        t += 1
    return ans[1:]

位运算

  • 按位与(a & b):每个对应的位都为 1 则返回 1,否则返回 0
  • 按位或(a | b):每个对应的位,只要有一个以上为 1 则返回 1,否则返回 0
  • 按位异或(a ^ b):每个对应的位,两个不相同则返回 1,相同则返回 0
  • 按位非(~a):反转被操作数的位,即将每一位的 0 转为 1,1 转为 0
  • 左移(a << b):a 的二进制串向左移动 b 位,右边移入 0,在数字没有溢出的前提下,左移一位都相当于乘以2的1次方,左移n位就相当于乘以 2 n 2^n 2n
  • 有符号右移(a >> b):a 的二进制串向右移动 b 位,高位的空位补符号位,即正数补零,负数补1,右移一位相当于除2,右移n位相当于除以 2 n 2^n 2n
  • 无符号右移(a >>> b):左侧直接补0,一定为正数
# 对于任意二进制位 x
x ^ 0 = x​ , x ^ 1 = ~x  # 异或
x & 0 = 0 , x & 1 = x # 与

# 判断奇偶
a & 1 === a % 2

# 第 i + 1 个二进制位
a |= 1 << i  # 设为1
a &= ~(1 << i)   # 设为0

1 只出现一次的数字

004

  • 对于出现三次的数字,各二进制位出现的次数都是 3 的倍数
  • 使用位运算(x >> i) & 1得到 x 的第 i 个二进制位,并将它们相加再对 3 取余
  • 细节:
  • 时间复杂度:O(nlogC),C 是元素的数据范围,在本题中 log C =32
  • 空间复杂度:O(1)
  • 扩展:如果其他数字出现两次,a ^ a = 0,可以利用该运算消去出现两次的
ans = 0
for i in range(32):
    total = sum((num >> i) & 1 for num in nums)
    if total % 3: # 此处可以修改成其他次数
        # 这里对于最高位需要特殊判断
        if i == 31:
            ans -= (1 << i)
        else:
            ans |= (1 << i)

链表

  • 经常使用 快慢指针
  • 链表的递归时间复杂度一般为O(n)
  • 递归函数结束的条件是什么?
    下一步的递归区间是什么?
  • 注意特殊情况,head=None,是否需要提前判断
## 删除node cur:
pre.next = pre.next.next/ cur.next

## 添加node cur:
pre.next = cur
pre = cur

1 回文链表

02.06

数组

有两种常用的列表实现,分别为数组列表和链表

  • 数组列表底层是使用数组存储值,我们可以通过索引在 O(1) 的时间访问列表任何位置的值,这是由基于内存寻址的方式
  • 链表存储的是称为节点的对象,每个节点保存一个值和指向下一个节点的指针。访问某个特定索引的节点需要 O(n)的时间,因为要通过指针获取到下一个位置的节点

法二:反转链表

空间复杂度由O(n)变成O(1)

  • 找到前半部分链表的尾节点:慢指针一次走一步,快指针一次走两步。当快指针移动到链表的末尾时,通过慢指针将链表分为两部分
  • 反转后半部分链表。反转链表
  • 判断是否回文。
## 反转链表
#### 迭代
def reverseList(head: ListNode) -> ListNode:
    pre, cur = None, head
    while cur:
        pos = cur.next
        cur.next = pre
        pre = cur
        cur = pos
    return pre
#### 递归
def reverseList(self, head: ListNode) -> ListNode:
    if not head or not head.next:
        return head
    ret = self.reverseList(head.next)
    head.next.next = head
    head.next = None
    return ret

2 链表相交

02.07

  • 如果两个链表由公共节点node,则a+(b-c) = b+(a-c)
  • 如果没有公共节点,最后两个链表停于公共点NULL,所以中间过程允许p1, p2为NULL
def getIntersectionNode(headA: ListNode, headB: ListNode) -> ListNode:
    A, B = headA, headB
    while A != B:
        A = A.next if A else headB
        B = B.next if B else headA
    return A

3 环路检测(快慢指针)

02.08

假设存在环路,且head->node长度为m,node环路长度为n,则

  • 慢指针一定在第一圈内就和快指针相遇,且相遇点距离node为x,快指针转过s圈:2(m+x)=m+sn+x
  • 慢指针从head出发,快指针从相遇点出发,按照相同速度会在node相遇:由上式,m=sn-x=(s-1)n+(n-x)

4 删除有序链表重复元素

I
II

  • 如果保留出现的第一个重复元素,则考察下一个元素是否要删除
  • 注意最后的pre.next=None,否则[1 2 2]会输出 [1 2 2]
def deleteDuplicates(head: ListNode) -> ListNode:
    if not head or not head.next:
        return head
    pre, cur = head, head.next
    while cur:
        if cur.val != pre.val:
            pre.next = cur
            pre = cur
        cur = cur.next
    pre.next = None
    return head

看上去不是特别复杂,但是处理起来细节很多

  • 假如pre的初始化为None,循环中需要判断pre是否为None,才能执行删除语句pre.next = cur.next;且该初始化无法处理类似[1 1 2]这种head就是重复元素的情况,最后return head,如果需要另外判断head,情况会非常复杂
  • 设置一个newHead->Head,pre=newHead,return newHead.next
  • 判断是否为重复元素:cur.val == cur.next.val,假如是首个重复元素,那么cur不断向后移,到最后一个重复元素执行删除语句pre.next = cur.next
def deleteDuplicates(head: ListNode) -> ListNode:
    res = ListNode(0)
    res.next = head
    pre, cur = res, head
    while cur and cur.next:
        if cur.val != cur.next.val:
            pre = cur
        else:
            while cur.val == cur.next.val:
                cur = cur.next
                if not cur.next:
                    break
            pre.next = cur.next
        cur = cur.next
    return res.next

5 三数之和

NC54

  • 排序后,对每一个num[i],查找后续是否能有Pair使得和为0
  • 细节: if i >= 1 and num[i] == num[i-1]: continue 避免出现重复的三元组,否则else里面的while失去作用
def threeSum(num):
    n = len(num)
    if n < 3:
        return []
    num.sort()
    res = []
    for i in range(n-2):
        if i >= 1 and num[i] == num[i-1]:
            continue
        left, right = i+1, n-1
        while left < right:
            target = num[i] + num[left] + num[right]
            if target < 0:
                left += 1
            elif target > 0:
                right -= 1
            else:
                res.append([num[i],num[left],num[right]])
                while left < right and num[left] == num[left+1]:
                    left += 1
                while left < right and num[right] == num[right-1]:
                    right -= 1
                left += 1; right -= 1
    return res

6 美团二面:接雨水

NC128
算法解析

核心原理:局部分析位置i,位置 i 能够装的水为:min(左边的最高柱子,右边的最高柱子)-当前柱子。

(1)暴力:对每个位置遍历取l_max和r_max
复杂度:时间 O ( N 2 ) O(N^2) O(N2)和空间 O ( 1 ) O(1) O(1)

for each i:
	l_max = max(height[0..i]), r_max = max(height[i..end])
	ans += min(l_max, r_max) - height[i]

(2)备忘录:提前用两个列表记录l_max和r_max,避免重复遍历
复杂度:时间 O ( N ) O(N) O(N)和空间 O ( N ) O(N) O(N)

for i from 1 to n:    # left to right
	l_max[i] = max(l_max[i-1], height[i])
for i from n to 1:    # right to left 
	r_max[i] = max(r_max[i+1], height[i])
for each i:
	ans += min(l_max[i], r_max[i]) - height[i]

(3) 双指针:l_max和r_max用两个指针left和right更新表示
l_max 和 r_max 代表的是 height[0…left] 和 height[right…end] 的最高柱子高度
复杂度:时间 O ( N ) O(N) O(N)和空间 O ( 1 ) O(1) O(1)

left, right = 0, n-1
while left <= right:
    l_max = max(l_max, height[left])
    r_max = max(r_max, height[right])

    if l_max < r_max:
        ans += l_max - height[left]
        left++
    else:
        ans += r_max - height[right]
        right--
  • left,right 分别更新当前子桶的左右两侧, 当两者最小高度大于原桶高度时,更新桶高度
  • 雨水数量由较矮的那个桶控制,当左侧桶更小时,left左移寻找更高区间
def maxWater(arr: List[int]) -> int:
    n = len(arr)
    left, right, H = 0, n-1, 0
    res = 0
    while left < right:
        minH = min(arr[left], arr[right])
        H = minH if minH > H else H
        if arr[left] >= arr[right]:
            res += H - arr[right]
            right -= 1
        else:
            res += H - arr[left]
            left += 1
    return res

思考:一维怎么推广到二维?

  • list的append和pop==栈;pop(0)==队列
  • 一维数组中找第一个满足某种条件的数”的场景:单调栈

1 双栈队列

NC76

借助栈的先进后出规则模拟实现队列的先进先出

  • push:直接插入 stack1
  • pop:当 stack2 不为空,弹出 stack2 栈顶元素,如果 stack2 为空,将 stack1 中的全部数逐个出栈入栈 stack2,再弹出 stack2 栈顶元素

pop时间复杂度均为 O(1)。每个元素只会「至多被插入和弹出 stack2 一次」,因此均摊下来每个元素被删除的时间复杂度仍为 O(1)。

2 表达式求值

NC137

  • 数字:可能有多位,保证取满

  • 表达式:将当前opt放入栈前,先进行运算,其前提是「栈内运算符」比「当前运算符」优先级高/同等,才进行运算。优先级可设置为d = {‘#’:0, ‘(’:0, ‘)’:0, ‘-’:1, ‘+’:1, ‘*’:2 },可以初始设置’#'防止判断

  • ‘(’:直接添加

  • ‘)‘:一直计算直到’('出栈

  • 细节:注意pop()对应的顺序不能反

3 滴滴三面:单调栈

柱状图中的最大矩形

  • 利用单调栈记录 i 两侧最近的小于其高度的位置,左侧哨兵为-1,右侧哨兵为n
  • 对位置 i 进行入栈操作时,确定了它的左边界;对位置 i 进行出栈操作时可以确定它的右边界
def largestRectangleArea(heights):
    n = len(heights)
    left, right = [-1]*n, [n]*n
    stack = []

    for i in range(n):
        while stack and heights[i] <= heights[stack[-1]]:
            right[stack.pop()] = i 
        left[i] = stack[-1] if stack else -1
        stack.append(i)

    ans = max((right[i] - left[i] - 1) * heights[i] for i in range(n)) if n > 0 else 0
    return ans     

1 BFS

NC15

  • 层序遍历可以用队列实现:node出队,node的左右children入队
  • python中list的append+pop(0)可以模拟队列
  • 时间和空间复杂度O(n)
def levelOrder(root: TreeNode) -> List[List[int]]:
    if not root:
        return []
    que, res = [root], []
    while que:
        tmp = []
        n = len(que)
        for i in range(n):
            cur = que.pop(0)
            tmp.append(cur.val)
            if cur.left:
                que.append(cur.left)
            if cur.right:
                que.append(cur.right)
        res.append(tmp)
    return res

2 三序遍历

NC45

  • 时空复杂度O(N)

法一:递归

  • 三种顺序只是在于访问根结点的顺序不同,三个序列在不同顺序插入root.val完成访问
def threeOrders(root: TreeNode) -> List[List[int]]:
    res = [[], [], []]
    def find(root):
        if not root:
            return root
        res[0].append(root.val)
        find(root.left)
        res[1].append(root.val)
        find(root.right)
        res[2].append(root.val)
    find(root)
    return res

法二:非递归

先序遍历:重复以下过程

  • 访问栈顶元素(先访问栈顶)
  • right入栈(先入后出),left入栈

中序遍历

  • left入栈,重复该过程,直到left不存在:模拟一直调用dfs(p.left)压栈
  • 栈顶元素出栈:模拟p.val,加入res
  • 判断当前节点的right是否存在,若存在则再次进入循环:模拟调用dfs(p.right)
def inorderTraversal(root):
    if not root:
        return []
    res, stack = [], []
    cur = root
    while stack or cur:
        while cur:
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()
        res.append(cur.val)
        cur = cur.right
    return res

后序遍历

和中序遍历的思想差不多,但是更加复杂,因为节点要在第三次时才会append

  • 需要继续遍历右子树,再次将栈顶元素根压入stack
  • 无右子树,或者右子树已被访问(pre记录,否则反复压入stack),则访问当前节点:更新pre,将本节点置为None,否则while循环将再次压入stack
def postorderTraversal(root):
    if not root:
        return []
    res, stack = [], []
    pre = None
    while stack or root:
        while root:
            stack.append(root)
            root = root.left
        root = stack.pop()
        if root.right and root.right != pre:
            stack.append(root)
            root = root.right
        else:
            res.append(root.val)
            pre = root
            root = None
    return res

3 DFS【搜索回溯】

岛屿数量
单词搜索
组合总和

模板

  • 题目为矩阵向相邻范围四个方向运动,外层遍历,内层递归
  • visit记录该位置是否访问,注意相邻区域访问之后需要还原visit状态
  • 外层遍历需要判断每个位置是否能作为起点
def dfs(r,c):
	# 待剪枝
    if (r>=m or r<0 or c>=n or c<0) or visit[r][c]==1:
        return 
    visit[r][c] = 1
    dfs(r+1,c)
    dfs(r-1,c)
    dfs(r,c+1)
    dfs(r,c-1)
    visit[r][c] = 0
for i in range(n):
	for j in range(m):
        dfs(i,j)

括号生成

  • 将整个过程形象化成一棵树,因为每次都有两种可能,该题等于DFS+剪枝
  • if left > 0: 左子树存在, 是否还能向左分裂
  • if right > left: 保证不出现’)(’,是否能分裂右子树
def generateParenthesis(n):
    res = []
    cur_str = ''
    def dfs(cur_str, left, right): #左右括号可以使用的个数
        if left == 0 and right == 0:
            res.append(cur_str)
        if left > 0:
            dfs(cur_str + '(', left - 1, right)
        if right > left:
            dfs(cur_str + ')', left, right -1)
    dfs(cur_str,n,n)
    return res

4 重建二叉树

NC12

法一:递归

  • pre第一个元素为根节点,根据其在vin中可以分成左右子树,从而利用递归
def reConstructBinaryTree( pre: List[int], vin: List[int]) -> TreeNode:
    if not pre:
        return None
    root = ListNode(pre[0])
    loc = vin.index(pre[0])
    root.left = reConstructBinaryTree(pre[1:loc+1], vin[:loc])
    root.right = reConstructBinaryTree(pre[loc+1:], vin[loc+1:])
    return root

法二:栈

  • 前序遍历的特点:第一次访问到根节点时的顺序。如果有左子树,那么下一个是它的左子树;如果没有左子树,那么下一个是它右子树或某个祖先的右子树。
  • 中序遍历的特点:第二次访问到根节点时的顺序。

5 所有路径和(先序遍历)

简单版本

def sumNumbers(root: TreeNode) -> int:
    def dfs(root, sums):
        if not root:
            return 0
        sums = sums*10 + root.val
        if not root.left and not root.right:
            return sums
        return dfs(root.left, sums) + dfs(root.right, sums)
    return dfs(root, 0)

NC8

  • 注意path记录每一条路径,如果当前节点所在路径不行满足要求需要再次pop()
  • res.append()内一定要传入path.copy(),否则res内的list最后都只保存了最终的path
def FindPath(root: TreeNode, target: int) -> List[List[int]]:
    def dfs(root):
        if not root:
            return 
        path.append(root.val)
        if not root.left and not root.right and sum(path) == target:
            res.append(path.copy())
        dfs(root.left)
        dfs(root.right)
        path.pop()
    res, path = [], []
    dfs(root)
    return res

排序

1 快速排序

快速排序用数组的第一个数作为key,将所有比它小的数都放到它左边,所有比它大的数都放到它右边;对所有部分重复排序

  • 初始化i=0,j=n-1,key=A[0]
  • 从j开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换
  • 从i开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换
  • 重复以上2步,直到i==j
def QuickSort(arr, low, high):
    if low >= high:
        return arr
    i, j = low, high
    key = arr[i]
    while i<j:
        while i<j and key<=arr[j]:
            j -= 1
        arr[i] = arr[j]
        while i<j and key>=arr[i]:
            i += 1
        arr[j] = arr[i]
    arr[i] = key
    QuickSort(arr, low, i-1)
    QuickSort(arr, i+1, high)

    return arr

【第K大】

第K大
参考快速排序的思想,注意该题找第K大,所以搜索条件相反,利用堆分治的思想,可以对左侧或者右侧的数据查找;可拓展为找中位数(阿里面试)

def findKth(a: List[int], n: int, K: int) -> int:
    i, j = 0, n-1
    key = a[i]
    while i < j:
        while i < j and a[j] <= key:
            j -= 1
        a[i] = a[j]
        while i < j and a[i] >= key:
            i += 1
        a[j] = a[i]
    a[i] = key
    if i < K - 1 :
        return findKth(a[i+1:n],(n-i-1),K-i-1)
    elif i > K - 1 :
        return findKth(a[:i], i, K)
    else:
        return a[i]

最小的K个数

2 归并排序

  • 递归划分 -> 合并
  • 注意合并阶段需要另外开辟空间tmp储存arr原顺序,否则相当于没排序
def MergeSort(arr):
        def merge(l,m,r):
            i, j = l, m+1
            k = l
            tmp[l:(r+1)] = arr[l:(r+1)]
            while i<=m and j<=r:
                if tmp[i] > tmp[j]:
                    arr[k] = tmp[j]
                    j += 1
                else:
                    arr[k] = tmp[i]
                    i += 1
                k += 1
            while i<=m:
                arr[k] = tmp[i]
                i += 1
                k += 1
            while j<=r:
                arr[k] = tmp[j]
                j += 1
                k += 1

        def mergesort(l, r):
            if l>=r:
                return 
            m = l + (r-l)//2
            # 递归划分
            mergeSort(l, m) 
            mergeSort(m+1, r)
            # 合并merge
            merge(l,m,r)
            return 
        
        tmp = [0]*len(arr)
        mergeSort(0, len(arr)-1)
        return arr

逆序对

逆序对 如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对,求出这个数组中的逆序对的总数

  • tmp[i] <= tmp[j] 构成逆序对的有i和[m+1…j],即j-(m+1)对,比arr[i]小的右侧数字个数;tmp[i] > tmp[j] 构成逆序对的有[i…m]和j,即m-i+1对,比arr[j]大的左侧数字个数
  • 计算分治过程中的L,R左右两侧数组逆序对,此后用排序后的数组继续计数,刚好覆盖所有数字
def reversePairs(arr):    
    def mergeSort(l, r):
        if l>=r:
            return 0
        m = l + (r-l)//2
        # 递归划分
        ans = mergeSort(l, m) + mergeSort(m+1, r)
        # 合并
        i, j = l, m+1
        tmp[l:(r+1)] = nums[l:(r+1)]
        for k in range(l,r+1):
            if i == m+1: 
                nums[k] = tmp[j]
                j += 1
            elif j == r+1 or tmp[i] <= tmp[j]:
                nums[k] = tmp[i]
                i += 1 
                ans += j-(m+1) #比左侧i小的右侧个数
            else:
                nums[k] = tmp[j]
                j += 1
                # ans += m-i+1  #比右侧j位置大的左侧数组个数
        return ans

    tmp = [0] * len(nums)
    return mergeSort(0, len(nums) - 1)

右侧小于当前元素

  • 右侧小于当前元素的个数 计算每个元素右侧小于当前元素的个数
    4种解法
    本题需要记录下每个元素的初始位置,以便将每个元素贡献的逆序对数目归功到对应的位置上,即nums不变,变的是index
# merge  index初始为[0,...,n-1]
for k in range(l,r+1):
    if i == m+1: 
        index[k] = tmp[j]
        j += 1
    elif j == r+1 or nums[tmp[i]] <= nums[tmp[j]]:
        index[k] = tmp[i]
        ans[tmp[i]] += j-(m+1)
        i += 1 
    else:
        index[k] = tmp[j]
        j += 1

二分查找

难点:● 跳出循环条件● 左右端点变化规则

while条件与更新规则
二分查找

【模板】

  • mid = (left + right)//2 or (left + right + 1)//2 控制search方向,前者尽量向左查找(l <= m < r ),后者尽量向右查找( l < m <= r)
  • while left <= right:查找区间为[left, right],更新left/right需要舍弃mid
  • while left < right:查找区间为[left, right) 或 (left, right],需要对应不同的更新规则
  • 浮点二分:判断条件和更新规则不再是1
  • 第一个大于/等于target:mid >= target;最后一个大于/等于target:mid <= target
#### 向左查找, 优先更新right
while l <= r:
	mid = (l+r)//2  # 向左查找
    if check(mid):	r = mid - 1  # check()判断mid是否满足性质  
    else:	l = mid + 1
# 查找区间为[left, right)
while l < r:
	mid = (l+r)//2  
    if check(mid):	r = mid  # target在[left, right)内, right肯定不满足
    else:	l = mid + 1

#### 向右查找,优先更新left
while l <= r:
	mid = (l+r+1)//2  # 向左查找
    if check(mid):	l = mid + 1  # check()判断mid是否满足性质  
    else:	r = mid - 1
# 查找区间为(left, right]	
while l < r:
	mid = (l+r+1)//2  
    if check(mid):	l = mid
    else:	r = mid - 1

# 浮点二分
while l <= r + eps:
	double mid = (l+r)/2
    if check(mid):	l = mid or r = mid
    else:	r = mid or l = mid

练习

def search(nums, target):
    l, r = 0, len(nums) - 1
    while l <= r: # 查找区间为[l,r]
        m = (l+r)//2 
        if nums[m] < target: # 降序 nums[m] > target
            l = m + 1 
        elif nums[m] > target: # 降序 nums[m] < target
            r = m - 1 # 每次更新舍弃m,否则死循环
        else:
            return m
    return -1
  • (二) 有重复数字的升序数组,第一个等于target
# 寻找左边界
def search(nums, target):
    l, r = 0, len(nums) - 1
    while l <= r:
        m = (l+r)//2
        if nums[m] >= target:  
            r = m - 1
        else:
        	l = m + 1
    # 第一个大于等于target的下标 l or r + 1
    return l if nums[l] == target else -1
    # 第一个小于target的下标 r of l - 1
    return r 
  
# 寻找右边界
def search(nums, target):
    l, r = 0, len(nums) - 1
    while l <= r:
        m = (l+r+1)//2
        if nums[m] <= target:
            l = m + 1 # 第一个小于等于target的下标+1
        else:  
            r = m - 1
    return r if nums[r] == target else -1
  • (三) 无重复数字的升序数组,第一个大于/等于target, 同(二)
  • (四) 有重复数字的升序数组,第一个和最后一个等于target,(二)的推广

1 字节面试实战:开n次方

开n次方,结果精确到小数点后4位

  • 奇偶次方差异,负数保存符号
  • 正数:边界情况(0,1)越乘越小,(1, ∞ \infty )越乘越大
  • while条件可以控制精度
def solution(x,n):
    neg = 1 if x<0 else 0
    x = abs(x)
    if x >= 1:
        left, right = 1, x
    else:
        left, right = x, 1
    while right - left >= 0.00001:
        mid = (left+right)/2
        if mid**n <= x:
            left = mid + 0.00001
        else:
            right = mid
    return right if neg == 0 else -right

2 腾讯笔试:区间红点

  • 输入[l,r],找到数组中在该范围内的对应下标
  • findL:找到 ≥ l \ge l l的第一个下标
  • findH:找到 ≤ r \le r r的第一个下标
  • 关键:m的更新(是否需要更接近上/下界) + x/y的更新(m是否还包含在可选范围内)
def findL(l,n,num):
    if num[-1] < l:
        return n
    x, y = 0, n-1 
    while x < y:
        m = x + (y-x)//2  #更接近x
        if num[m] > l:
            y = m  #m仍然有可能
        elif num[m] < l:
            x = m+1
        else:
            return m
    return y

def findH(r,n,num):
    if num[0] > r:
        return -1
    x, y = 0, n-1
    while x < y:
        m = x + (y-x+1)//2  #更接近y
        if num[m] < r:
            x = m
        elif num[m] > r:
            y = m-1
        else:
            return m
    return y

3 二维查找

NC29

  • 从左下角或者右上角开始搜索,以右上角为例
  • while row < n and col >= 0:array[row][col] < target,则row++; 否则,col–
while r < n and c >= 0:
    if array[r][c] == target:
        return  True
    if array[r][c] < target:
        r += 1
    else:
        c -= 1

4 旋转数组的最小数字

11

  • 当 nums[m] > nums[r] 时: 旋转点 x 一定在 [m+1, r] 闭区间内,因此执行 l = m + 1;
  • 当 nums[m] < nums[r] 时: 旋转点 x 一定在[l, m] 闭区间内,因此执行 r = m;
  • 当 nums[m] = nums[r] 时: 无法判断旋转点 x 在 [l, m]还是[m+1,r] 区间中。 执行 r = r - 1 缩小判断范围
n = len(numbers)
left, right = 0, n - 1
while left < right:
    mid = left + (right-left) // 2
    if numbers[mid] < numbers[right]:
        right = mid
    elif numbers[mid] > numbers[right]:
        left = mid + 1
    else:
        right -= 1
ans = numbers[left]

动态规划/贪心

  • dp一般是以当前结尾的xxxx
  • 边界条件
  • 状态转移方程:不一定是前1/2个,还可能是能已知的任意一个下标

思路提示:

  • 假如题目设置有多种状态,无法确定当前的最优选择,可以考虑用dp[n][s]来表示当前位置不同状态的选择
  • 状态压缩:如果状态转移方程只依赖dp[i-1],那么可以用变量直接指代上一状态,空间由O(n)至O(1), i-1可以推广至所有被记录的上一时刻状态

【贪心算法】的问题需要满足的条件:

  • 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,规模较大的问题的解只由其中一个规模较小的子问题的解决定;
  • 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
  • 贪心选择性质:从局部最优解可以得到全局最优解。

1 线性DP

1 二叉搜索树个数

DP5

  • dp:i个节点组成的搜索树个数
  • 分析:考虑有序数组,设以i为根节点的二叉搜索树的个数为 f(i),则 f(i) = dp[i-1]*dp[n-i];dp[i]=f(1)+…+f(i)=dp[0]*dp[i-1]+…+dp[i-1]*dp[0]
  • 状态转移方程:dp[i] += dp[j-1]*dp[i-j]
n = int(input())
dp = [0] * (n+1)
dp[0] = 1
for i in range(1,n+1):
    for j in range(1,i+1):
        dp[i] += dp[j-1] * dp[i-j]
print(dp[n])

2 最长连续有效括号

最长有效括号 输入只包含 ‘(’ 和 ‘)’ 的字符串,输出最长连续有效括号子串的长度

关键在于如何找到当前’)‘匹配的’(‘,由于dp[i-1]记录了长度信息,匹配的’('下标为 i-dp[i-1]-1;假如下标有效,则dp[i] = dp[j-1] + dp[i-1] + 2,注意不要遗漏dp[j–1],可能会出现 s1( s2 ) 的情况

dp = [0] * n
ans = 0
for i in range(1,n):
    if s[i] == ')':
        j = i-dp[i-1]-1
        if s[j] == '(' and j >= 0:
            dp[i] = dp[j-1] + dp[i-1] + 2
            ans = max(ans, dp[i])    

3 解码方法

解码方法 字母A-Z对应1-26,输入一串字符,有多少种解码方式

  • 当前数字不为0,可单独成码;非0开头且int()<=26,可和前一个数字组合成码
  • 边界条件:空字符=1
# 输入s, n
dp = [1] + [0] * n
for i in range(1, n+1):
	if s[i - 1] != '0':
		dp[i] += dp[i - 1]
	if i > 1 and s[i - 2] != '0' and int(s[i-2:i]) <= 26:
    	dp[i] += dp[i - 2]
print(dp[-1])

4 等差数列划分

413

  • dp[i]是以 i 为终点的等差数列的个数
  • if nums[i-2] - nums[i-1] == nums[i-1] - nums[i]: dp[i] = dp[i-1]+1
  • 注意最后结果是sum(dp),空间复杂度可以简化成O(1)
def numberOfArithmeticSlices(self, nums: List[int]) -> int:
    n = len(nums)
    dp = [0] * n
    for i in range(2,n):
        if nums[i-2] - nums[i-1] == nums[i-1] - nums[i]:
            dp[i] = dp[i-1] + 1
    return sum(dp)

5 串

长度不超过nn,且包含子序列“us”的、只由小写字母构成的字符串有多少个?答案对1e9+7取模
所谓子序列,指一个字符串删除部分字符(也可以不删)得到的字符串。
例如,“unoacscc"包含子序列"us”,但"scscucu"则不包含子序列"us"。

dp[i][0]表示 前i个字符串中没有u的情况
dp[i][1]表示 前i个字符串中有u,且不包含us的情况
dp[i][2]表示 前i个字符串,包含us子序列的情况

状态转移方程:

dp[i][0]=dp[i-1][0]*25
dp[i][1]=dp[i-1][0]+dp[i-1][1]*25
dp[i][2]=dp[i-1][1]+dp[i-1][2]*26

2 连续子序列/矩阵

子数组最大和/积

  • 连续子数组最大和 子数组更新规则:上一位置处的子数组最大和为负
  • 连续子数组最大积 子数组更新规则:最大值小于当前数或最小值小于当前数,注意此处不考虑pos/neg,选择max/min,避免考虑符号等问题
n = int(input())
a = list(map(int,input().split()))
m1, m2, ans = a[0], a[0], a[0]
for x in a[1:]:
	# m1, m2同时更新, 否则影响后更新的
    m1, m2 = max(m1*x, m2*x, x), min(m1*x, m2*x, x)
    ans = max(ans, m1)
  • 乘积为正数的最长连续子数组 以dp[i][0]和dp[i][1]分别表示以i结尾的乘积为负和正的最长连续子数组长度,讨论正数/0/负数的情况;注意当前数为正时更新dp[i][0] = dp[i-1][0] + 1 if dp[i-1][0] > 0 else 0
  • 环形数组最大和 = max(最大子数组和,数组总和-最小子数组和)

    case 1:子数组不是环状的,即连续子数组最大和
    case 2:转成求最小子数组,则等价于case1
    注意:当数组全为负数时,sum-min=0,此时需要输出max
n = int(input())
a = list(map(int,input().split()))
s1, s2, s, m1, m2 = 0, 0, 0, -10**4, 10**4
for x in a:
    s1, s2 = max(s1+x, x), min(s2+x, x)
    m1, m2 = max(m1, s1), min(m2, s2) 
    s += x
print(max(m1, s-m2) if m1>0 else m1)  # 注意数组全为负数的情况

最大子矩阵

最大子矩阵

  • 将二维转成一维,固定上下行,依次计算每列和,此时可以用最大子序列和求解;注意,此处不用提前求前缀和,重复求解;解法
# 输入n, A
ans = -128
for i in range(n):
    s = [0]*n
    for j in range(i,n):
        # 更新列和
        for k in range(n):
            s[k] += A[j][k]
        # 计算最大连续子数组
        m = 0
        for x in s:
            m = max(m,0) + x
            ans = max(m, ans)

3 矩阵路径

下/右

  • 矩阵的最小路径和

  • 过河卒 细节:每次需要判断(i,j)!=(x,y) 且 |i-x|+|j-y|==3, i!=x, j!=y

  • 龙与地下城游戏

    对于每一条路径,需要同时记录「从出发点到当前点的路径和」和「从出发点到当前点所需的最小初始值」, 且这两个值重要程度相同;

    如果按照从左上往右下的顺序进行动态规划,不满足「无后效性」,考虑右下往左上进行动态规划;

    令dp[i][j] 表示从坐标 (i,j) 到终点所需的最小初始值,该值可以表示该点至终点的路径和信息,两个信息合成一个处理

    状态转移方程为dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - A[i][j])

# 输入n,m, A
dp = [[0]*m for _ in range(n)]
dp[n-1][m-1] = max(1, 1-A[n-1][m-1]) # 注意初始化为1-Aij
for i in range(n-2,-1,-1):
    dp[i][m-1] = max(1, dp[i+1][m-1] - A[i][m-1])
for j in range(m-2,-1,-1):
    dp[n-1][j] = max(1, dp[n-1][j+1] - A[n-1][j])
    
for i in range(n-2,-1,-1):
    for j in range(m-2,-1,-1):
        dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - A[i][j])
        
print(dp[0][0])

上下左右

  • 滑雪 矩阵的最长递减路径

  • 矩阵的最长递增路径 DFS + 记忆化搜索

    DFS里的回溯(backtracking)常常是必要的,由于这类题目通常要求每个元素只能使用1次,因此我们需要维护一个visted矩阵(记录访问过的元素,并且在dfs压栈和退栈的时候反复设置状态),而每次选定的起始位置不同,visted的状态就不一样了,导致计算的结果不可重用,不能进行缓存。

    在内循环里,A[ii][jj] < A[i][j]确保之前已经访问过的元素根本就不会出现在后续的路径里,所以visited就没有存在的意义,因为不管你从哪个位置开始迭代,计算结果变成唯一的了——因此变得可以缓存了。

    时间复杂度:O(mn)。DFS的时间复杂度是 O(V+E),其中 V 是节点数,E 是边数。在矩阵中,O(V)=O(mn),O(E) ≈ \approx O(4mn) = O(mn)。

    空间复杂度:O(mn),主要取决于缓存和递归调用深度,缓存的空间复杂度是 O(mn),递归调用深度不会超过 mn。

# 输入n,m,A
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
def dfs(i,j):
    best = 1
    for x, y in DIRS:
        ii, jj = i+x, j+y
        if 0<=ii<n and 0<=jj<m and A[ii][jj] < A[i][j]:
            best = max(best, dfs(ii, jj) + 1)
    return best
    
ans = 0
for i in range(n):
    for j in range(m):
        ans = max(ans, dfs(i,j))
        
print(ans)

4 不连续子序列

该类题目一般只要求求解最长长度,通过二维dp记录起终点求解

最长上升子序列

  • 适用题型:选择/删除部分元素,求最大有序序列长度

一维

LIS 暴力解法:dp[i] 遍历前i-1个元素,时间复杂度O( n 2 n^2 n2)

  • 贪心:上升序列长 ↔ \leftrightarrow 序列上升慢 ↔ \leftrightarrow 上升子序列最后的数小
  • d[i]表示长度为 i 的最长上升子序列的末尾元素的最小值,len为当前最长长度
  • 如果 a[i] > d[len],len = len + 1,否则找到第一个比 a[i] 小的数 d[k] ,并更新 d[k+1] = a[i];由于d[i]关于i单调递增,所以可以使用二分查找
  • 关键在于二分查找函数
# 输入n,a, 定义search函数
d = [a[0]] # 长度为i的子序列结尾最小数字
for i in range(1,n):
    if a[i] > d[-1]:
        d.append(a[i])
    else:
        k = search(d, a[i])  # 第一个下标小于target
        d[k+1] = a[i]
print(len(d))

拦截导弹
拦截导弹 分别求解最长下降序列长度和最长上升序列长度,关键在于两个查找函数

Dilworth定理: 最少的下降序列个数就等于整个序列最长上升子序列的长度

二维

信封嵌套 嵌套要求长宽均严格递增,求最大嵌套数目

  • 根据长度排序,则在该维度已经递增,相当于所有信封在该维度直接选择
  • 保证长度递增,二维实际已经退化成一维情形,对排序后的宽度序列选最长上升子序列
  • 实现细节:如何保证严格递增?如何保证等长区间选择宽度最小?等长区间内宽度递减排序,保证选择能使序列增长的最小宽度
a.sort(key=lambda x:[x[0],-x[1]])
d = [a[0][1]]
for i in range(1,n):
    if a[i][1] > d[-1]:
        d.append(a[i][1])
    else:
        k = search(d, a[i][1])
        d[k+1] = a[i][1]
print(len(d))

最长公共子串

LCS

  • dp[i][j]表示以str1[i-1]和str2[j-1]结尾的最长公共子序列长度
  • 边界条件:dp[0][j] =0 dp[i][0]=0
  • 状态转移方程:
    if str1[i-1]==str2[j-1], dp[i][j] = dp[i-1][j-1] +1
    if str1[i-1]!=str2[j-1], dp[i][j] = max(dp[i][j-1], dp[i-1][j])
# 输入n,m,s1,s2
dp=[[0]*(m+1) for _ in range(n+1)]
ans = 0
for i in range(1,n+1):
    for j in range(1,m+1):
        if s1[i-1]==s2[j-1]:
            dp[i][j] = dp[i-1][j-1]+1
            if dp[i][j] > ans:
                ans = dp[i][j]
        else:
            dp[i][j] = max(dp[i][j-1], dp[i-1][j])

5 最长回文子序列

连续子序列

LPS 令s1, s2 = s, s[::-1],LCS求的不是回文子串, i1 - j1 和 i2 - j2需要对应

法一:动态规划

  • dp[i][j]表示 i 到 j 的子串是否是回文子串
  • 状态转移方程:dp[i,j] = (dp[i+1][j-1]) ∧ (S[i]==S[j])
    注意:由于状态转移方程方向与i,j循环递增不一致,i 变化方向相反
  • 边界条件 d p [ i ] [ i ] = = T r u e / d p [ i ] [ i + 1 ] = ( S [ i ] = = S [ i + 1 ] ) dp[i][i] == True / dp[i][i+1] = (S[i]==S[i+1]) dp[i][i]==True/dp[i][i+1]=(S[i]==S[i+1])
  • 时间复杂度O( n 2 n^2 n2),空间复杂度O( n 2 n^2 n2)
n = len(s)
dp = [[False]*n for _ in range(n)]
ans, idx = 1, 0
for i in range(n):
    dp[i][i] = True
    if i>0 and s[i-1] == s[i]:
        dp[i-1][i] = True
        ans, idx = 2, i-1
for i in range(n-1,-1,-1): 
    for j in range(i+2,n): 
        if s[i] == s[j] and dp[i+1][j-1]:
            dp[i][j] = True
            if j-i+1 > ans:
                ans = j-i+1
                idx = i
print(s[idx:idx+m])

法二:中心扩散

  • 每次以1/2个字符为中心向外扩散,对应’aba’,‘abba’
  • 时间复杂度O( n 2 n^2 n2),空间复杂度O(1)
def getLongestPalindrome(A: str) -> int:
    n = len(A)
    if A==A[::-1]:
        return n
    res = 1
    for i in range(1,n): 
        one = A[i-res:i+1]  #以i结尾的子串,长度为res+1
        two = A[i-res-1:i+1] #以i结尾的子串,长度为res+2
        if i-res>=0 and one == one[::-1]:
            res += 1
        if i-res-1>=0 and two == two[::-1]:
            res += 2
    return res

不连续子序列

最长回文子序列

dp = [[0]*n for _ in range(n)]
ans = 0
for i in range(n):
    dp[i][i] = 1
for i in range(n-1,-1,-1): 
    for j in range(i+1,n): # 可以合并dp[i][i+1]
        if s[i] == s[j]:
            dp[i][j] = dp[i+1][j-1] + 2
        else:
            dp[i][j] = max(dp[i+1][j], dp[i][j-1])
        ans = max(ans, dp[i][j])

6 买卖股票

leetcode题目解法汇总

  • (一) 买卖一次,区间更新规则(大区间优先,交易次数限制):出现新的最小值,更新每个区间对应的购入价格
  • (二) 买卖多次,区间更新规则(小区间优先):今日价格比昨日低,只要价格高就可以卖出购入
n = int(input())
prices = list(map(int,input().split()))
ans = 0
### (一)买卖一次
m = prices[0]
for i in range(1,n):
    if prices[i] < m:
        m = prices[i]
    else:
        ans = max(prices[i] - m, ans)
### (二)买卖多次
for i in range(1,n):
    # 只要今天比昨天的价格低,卖出
    if prices[i] > prices[i-1]: 
        ans += prices[i] - prices[i-1]
print(ans)
  • (三) 买卖两次,区间更新规则更复杂,按(一)更新忽略大区间收益更大的情况,按(二)更新忽略大区间内多个小区间收益更大的情况,关键在于在当前位置不能判断按照哪种方式买卖,可以记录不同处理对应的收益

    一共有5个状态,分别为不操作,第一次买入buy1,第一次卖出sale1,第二次买入buy2,第二次卖出sale2;利用dp[n][m]表示下标为n,状态为m对应的收益,后四个状态对应当前的最大收益

n = int(input())
prices = list(map(int,input().split()))
buy1, sale1, buy2, sale2 = float('-inf'), 0, float('-inf'), 0
for p in prices:
	# 每次buy和sale只有一组变;buy2和buy1间相差sale1的收益
    buy1 = max(buy1, -p) 
    sale1 = max(sale1, buy1 + p) 
    buy2 = max(buy2, sale1 - p) # buy2在第一次交易前buy1
    sale2 = max(sale2, buy2 + p) # sale2在第二次交易前等于sale1
print(sale2)
  • (四) 买卖k次,dp[2*k]分别对应buy和sale的k次交易

7 跳跃游戏【贪心】

  • (一) 能否到达最后,记录每个位置可达的最大距离m,比较m和当前位置
  • (二) 输出可达最后的最大积分,从右往左以dp记录当前位置能到达最后所得的最大积分,由于dp只记录上一个可达位置的积分所以可以压缩成变量;注意从左往右不能成立是因为不确定当前位置能否抵达最后,但是倒推法可以保证当前位置能到达最后;(一)同样可以用倒推法实现
n = int(input())
num = list(map(int,input().split()))
last = n-1 # 最早到达的位置
ans = num[n-1]
for i in range(n-2,-1,-1):
    if last <= i+num[i]:
        last = i  # (一)只需要更新last
        ans += num[i]
if last == 0:
    print(ans)
else:
    print(-1)

## dp法
dp = [-1] * n
dp[n-1] = num[n-1] # 边界条件
for i in range(n-2,-1,-1):
    if last <= i+num[i]:
        dp[i] = dp[last] + num[i]  # 状态转移方程
        last = i
print(dp[0])
  • (三) 到达最后的最少跳跃次数,贪心:更新当前可达范围最大的位置,相当于把[0,n-1]分成k个不相交的小区间,在每个小区间上找到可达的最大距离,线性复杂度
n = int(input())
num = list(map(int,input().split()))
start, end, m = 0, 1, 0 
ans = 0
while start < end and end < n and m < n - 1:
    for i in range(start, end):
        m = max(m, i + num[i])
    start, end = end, m + 1
    ans += 1
print(ans if m >= n-1 else -1)

8 不相邻取数

  • (一) 不相邻取数,dp[i] = max(dp[i-2]+num[i], dp[i-1])

  • (二) 环形数组,数组首尾相连

    问题:如何处理环形?假如不选首,则对应范围为[1,n-1];假如不选尾,则对应范围为[0,n-1];此时问题转化为(一)

n = int(input())
num = list(map(int,input().split()))
def rob(a,n): # (一)的对应函数
    dp = [0] * n
    dp[0] = a[0]
    for i in range(1,n):
        dp[i] = max(dp[i-2] + a[i], dp[i-1])
    return dp[-1]
print(max(rob(num[:-1],n-1), rob(num[1:],n-1)))
  • (三) 树形结构,不能同时选中父节点和相连子节点
  • (四) 选择 a i a_i ai,会删除所有 a i ± 1 a_i\pm1 ai±1,求最大积分,生成一个num数组记录每个数对应的积分,此时可用(一)不相邻取数求解

9 前缀和

扩展:一维/二维前缀和,二维以dp[i][j]表示左上角为(1,1),右下角为(i,j)的子矩阵和

后缀和

abb

思路:

  • 对于每个字母 c,找到后面和 c 不等的两个相等字母的个数 cnt
  • 开一个后缀和数组 dp[n][26],dp[i][j]代表 j 对应的字母,在坐标 i 到 n出现的次数
  • 注意:生成后缀和数组,dp[i][ord(a[i+1])-ord(‘a’)] += 1 避免每次在循环中判断,否则会超时
n = int(input())
a = input()
dp = [[0]*26 for _ in range(n)]
for i in range(n-2,-1,-1):
    for j in range(26):
        dp[i][j] = dp[i+1][j]
    dp[i][ord(a[i+1])-ord('a')] += 1
                
ans = 0
for i in range(n):
    for j in range(26):
        if ord(a[i]) - ord('a') != j:
            m = dp[i][j]
            ans += m*(m-1)/2 
print(int(ans))

差分

差分

思路:

  • 用差分数组减少区间操作的复杂度, a l , . . . , a r a_l,...,a_r al,...,ar全加上k等价于 d [ l ] + = k , d [ r + 1 ] − = k , a [ i ] = a [ i ] + d [ i ] d[l] += k, d[r+1] -=k,a[i] = a[i] + d[i] d[l]+=k,d[r+1]=ka[i]=a[i]+d[i]
  • 注意最后更新数组,需要对差分数组求前缀和
n, m = list(map(int,input().split()))
a = list(map(int,input().split()))
d = [0]*n
for _ in range(m):
    l, r, k = list(map(int,input().split()))
    d[l-1] += k
    if r < n:
        d[r] -= k
# 前缀和
for i in range(1,n):
    d[i] += d[i-1]
for i in range(n):
    a[i] += d[i]
print(' '.join(str(x) for x in a))

二维差分

二维差分

  • 联系二维前缀和,关键在于怎么确定每次操作如何确定dp的变化,最后对dp矩阵做二维前缀和
  • 左上角(x1,y1)和右下角(x1,y2)的子矩阵:dp[x1][y1] += k, dp[x1][y1+1] -= k, dp[x2+1][y1] -=k, dp[x2+1][y2+1] +=k; -k是为了抵消dp[x1][y1]+k的影响,dp[x2+1][y2+1] 是为了和2次-k抵消
n, m, q = list(map(int,input().split()))
A = [[0]*(m+1)]
for _ in range(n):
    A.append([0]+list(map(int,input().split())))
dp = [[0]*(m+1) for _ in range(n+1)]
for _ in range(q):
    x1,y1,x2,y2,k = list(map(int,input().split()))
    dp[x1][y1] += k
    if y2 < m:
        dp[x1][y2+1] -= k
    if x2 < n:
        dp[x2+1][y1] -= k
    if y2 < m and x2 < n:
        dp[x2+1][y2+1] += k
# 二维前缀和
for i in range(n):
    for j in range(m):
         dp[i+1][j+1] += dp[i][j+1] + dp[i+1][j] - dp[i][j]
for i in range(1,n+1):
    for j in range(1,m+1):
        A[i][j] += dp[i][j]
    print(' '.join(str(x) for x in A[i][1:]))

12 01背包

模板

01背包

  • dp[j]:背包容量恰好为 j 的最大价值
  • 状态转移方程:w<= j ,dp[j]=max(dp[j-w] + v, dp[j])
  • 注意 dp 初始化为 0 对应容量<=j, 初始化为 -inf 对应容量==j
  • dp需要倒序更新,否则物品会被重复选择
dp = [-float('inf') ] * (V+1)  
dp[0] = 0		
for i in range(n):
    v, w = vw[i]
    for j in range(V,v-1,-1):
        dp[j] = max(dp[j-v]+w,dp[j])

NC145 01背包

1 零钱兑换

  • (一) 给定零钱和金额S,求组成S所需最少硬币数量
    状态转移方程: F ( S ) = m i n F ( S − c i ) F(S) = min F(S-c_{i}) F(S)=minF(Sci) + 1 s.t. S − c i ≥ 0 S-c_{i}\ge 0 Sci0
def minMoney(amount,coins) :
    dp = [10000]*(amount+1)
    dp[0] = 0
    for c in coins:
        for s in range(c,amount+1):
            dp[s] = min(dp[s],dp[s-c]+1)
    return dp[-1] if dp[-1]>amount else -1
  • (二) 所有可能组合数
    coin不能嵌套在内层循环,否则会重复计算不同的排列
def change(amount,coins):
    dp = [0]*(amount+1)
    dp[0] = 1
    for c in coins:
        for s in range(c,amount+1):
            dp[s] += dp[s-c] 
    return dp[-1]

13 分糖果

NC130

贪心遍历

  • 从左往右遍历一遍,如果右边孩子的评分比左边的高,则 c a n d y [ i ] = c a n d y [ i − 1 ] + 1 candy[i]=candy[i-1]+1 candy[i]=candy[i1]+1;
  • 从右往左遍历一遍,如果左边孩子的评分比右边的高,且 c a n d y [ i − 1 ] = m a x ( c a n d y [ i − 1 ] , c a n d y [ i ] + 1 ) candy[i-1]=max(candy[i-1],candy[i]+1) candy[i1]=max(candy[i1],candy[i]+1)

序列

  • 递增序列每次比前次加一
  • 递减序列的糖果数取决于递减序列长度与递增序列长度之间的大小关系:
    (1)若递减序列长度decLen小于递增序列长度incLen,则糖果数为1,2,3,……,decLen;
    (2)否则在此基础上还需要更新递增序列最后一个元素的值,从incLen更新为decLen。
  • 每次遇到相同的得分,可视为清零重新开始。

动态更新

  • 如果右边孩子的评分比左边的高,糖果+1
  • 如果右边孩子的评分更低,则从左孩子开始判断需要依次多加一颗糖的场景
  • 如果右边孩子的评分和左边相同,【糖果=1】
if arr[i-1] < arr[i]:
    candy[i] = candy[i-1] + 1
elif arr[i-1] > arr[i]:
    candy[i] = 1
    p = i-1
    while p >= 0 and arr[p] > arr[p+1] and candy[p] == candy[p+1]:
        res += 1
        candy[p] += 1 
        p -= 1
else:
    candy[i] = 1
res += candy[i]

14 可行矩阵

1605 给定行列和求可行矩阵

  • 思路:mat[i][j] = min(rowSum[i], colSum[j]),同时更新rowSum[i]和colSum[j]

15 编辑距离

(一) 三种操作:插入/删除/替换,使两个字符串相同的最少操作次数
(二) 三种操作的cost不只是1,输入对应的ic/dc/rc

本质不同的操作实际上只有三种:

  • 在A 中插入一个字符 == 在B中删除一个字符:dp[i-1][j]+ic
  • 在A 中删除一个字符 == 在B中插入一个字符:dp[i][j-1]+dc
  • 修改A 的一个字符 == 修改B的一个字符:dp[i-1][j-1]+rc
def minEditCost(str1: str, str2: str, ic: int, dc: int, rc: int) -> int:
    n, m = len(str1), len(str2)
    dp = [[0]*(m+1) for _ in range(n+1)]
    for i in range(m+1):
        dp[0][i] = i*ic
    for i in range(n+1):
        dp[i][0] = i*dc
    for i in range(1,n+1):
        for j in range(1,m+1):
            if str1[i-1] == str2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(dp[i-1][j]+dc, dp[i][j-1]+ic,dp[i-1][j-1]+rc)
    return dp[-1][-1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值