算法分析与设计

算法概述

  • 算法
    • 输入
    • 输出
    • 确定性
    • 有限性
    • 可行性
  • 算法与程序的区别
  • 算法的渐进复杂性
    • 时间复杂性
      • OOO 渐进上界
      • Ω\OmegaΩ 渐进下界
      • Θ\ThetaΘ 确界
    • 空间复杂性
    • 规模与复杂性
      • lognlognlogn, nnn, nlognnlognnlogn, n2n^2n2, 2n2^n2n, n!n!n!
    • 渐进复杂性的运算
      • 对减法和除法不满足
  • 算法的实现
    • 数据结构
      • 数组与链表
      • 栈与队列
      • 集合
      • 字典
      • 图与树

排序

  • 冒泡排序
    • 无序部分小的元素向前移动直到有序部分末尾
  • 插入排序
    • 无序部分第一个元素插入到有序部分的合适位置
  • 选择排序
    • 选择无序部分的最小元素放到有序部分的末尾
  • 快速排序
    • 找支点 + 划分
  • 实现
    def bubbleSort(arr):
        # 无需部分小的元素向前移动直到有序部分末尾
        n = len(arr)
        for i in range(n):
            for j in range(n-1, i, -1):
                if arr[j] < arr[j-1]:
                    arr[j], arr[j-1] = arr[j-1], arr[j]
    
    def insertSort(arr):
        # 无序部分第一个元素插入到有序部分的合适位置
        n = len(arr)
        for i in range(n):
            k = i
            for j in range(i):
                if arr[j] >= array[i]:
                    k = j
                    break
            for j in range(i, k, -1):
                arr[j], arr[j-1] = arr[j-1], arr[j] 
    
    def selectionSort(arr):
        # 选择无序部分的最小元素放到有序部分的末尾
        n = len(arr)
        for i in range(n):
            k = i
            for j in range(i, n):
                if arr[j] < arr[k]:
                    k = j
            arr[i], arr[k] = arr[k], arr[i]
    
    def quicksort(arr):
        n = len(arr)
        dfs(arr, 0, n - 1)
    def dfs(arr, left, right):
        if left >= right:
            return
        j = left - 1
        for i in range(left, right):
            if arr[i] < arr[right]:
                arr[i], arr[j + 1] = arr[j + 1], arr[i]
                j = j + 1
        arr[right], arr[j + 1] = arr[j + 1], arr[right]
        dfs(arr, left, j)
        dfs(arr, j + 2, right)
    

1.分治

  • 思想
    • 原问题分解为相似的子问题,先求解子问题,再将子问题组合为原问题
  • 主定理
    • T(n)=aT(n/b)+O(nk)T(n)=aT(n/b)+O(n^k)T(n)=aT(n/b)+O(nk)
      • logb(a)>klog_b(a)>klogb(a)>k, O(nlogb(a))O(n^{log_b(a)})O(nlogb(a))
      • logb(a)=klog_b(a)=klogb(a)=k, O(nlogb(a)log(n))O(n^{log_b(a)}log(n))O(nlogb(a)log(n))
      • logb(a)<klog_b(a)<klogb(a)<k, O(nk)O(n^{k})O(nk)
  • 案例
    • 大数相乘
      • T(n)=3T(n/2)+O(n)T(n)=3T(n/2)+O(n)T(n)=3T(n/2)+O(n)
    • 矩阵乘法
      • T(n)=7T(n/2)+O(n2)T(n) = 7T(n/2)+O(n^2)T(n)=7T(n/2)+O(n2)
    • 二分查找
      • 针对已排序的数组
      • T(n)=T(⌈n/2⌉)+O(1)T(n)=T(\lceil n/2 \rceil) + O(1)T(n)=T(⌈n/2⌉)+O(1)
    • 归并排序
      • 划分+排序+归并
      • T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)
    • 快速排序
      • 找支点+划分+排序
      • 最好情况:T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)
      • 最坏情况:T(n)=T(0)+T(n−1)+O(n)T(n)=T(0)+T(n-1)+O(n)T(n)=T(0)+T(n1)+O(n)
      • 平均情况:T(n)=1n∑i=0n−1(T(i)+T(n−i−1))+O(n)T(n)=\frac{1}{n} \sum_{i=0}^{n-1}(T(i)+T(n-i-1))+O(n)T(n)=n1i=0n1(T(i)+T(ni1))+O(n)
    • selection问题
      • 要求不能使用排序
      • 理想情况:T(n)=T(n/2)+O(n)T(n)=T(n/2)+O(n)T(n)=T(n/2)+O(n)
      • 平均情况:T(n)≤T(3n/4)+O(n)T(n) \le T(3n/4)+O(n)T(n)T(3n/4)+O(n)
    • 快速幂
      • T(n)=T(n/2)+O(1)T(n)=T(n/2)+O(1)T(n)=T(n/2)+O(1)
    • 循环排序列表的最小值
      • T(n)=2T(n/2)+O(1)T(n)=2T(n/2)+O(1)T(n)=2T(n/2)+O(1)
    • k路合并
      • T(k)=2T(k/2)+O(nk)T(k)=2T(k/2)+O(nk)T(k)=2T(k/2)+O(nk)
    • 两个升序列表中第k小的元素
      • T(m+n)={T(m/2+n)+O(1)T(m+n/2)+O(1)T(m+n)=\left\{\begin{align*} & T(m/2+n)+O(1) \\ & T(m+n/2)+O(1) \end{align*} \right.T(m+n)={T(m/2+n)+O(1)T(m+n/2)+O(1)
    • 数组的主元素
      • T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)

2.贪心算法

  • 思想
    • 通过每一步的最优解达到全局的最优解
  • 适用条件
    • 最优子结构
    • 贪心选择性质
  • 案例
    • 活动安排
      • 每次选择最早结束的活动
    • 最优装载
      • 每次选择最轻的货箱
    • 分数背包问题
      • 每次选择性价比最高的物品
    • 找零问题
      • 每次选择面值最大的硬币
    • Huffman编码
      • 每次合并频率最小的两棵子树
      • 满二叉树
    • 单源最短路径
      • Dijkstra算法
      • 每条边的权重都是非负值
      • 每次选择未访问且距离最近的点,对其出发的所有边进行松弛操作
    • 最小生成树
      • Kruskal算法
        • 每次选择距离最短且不在同一个连通分支的边
        • 并查集
      • Prim算法
        • 每次选择横跨切割且距离最短的边
        • 已选择的顶点集合,未选择的顶点集合
    • 多机调度问题
      • NP-complete
      • 每次选择时间最长的作业
    • 图着色问题,使用Δ(G)+1\Delta(G)+1Δ(G)+1种颜色
      • 每次选择可用的最小颜色
    • 磁带最优存储问题
      • 每次选择单位访问概率下最长的文件
  • 实现
    • Kruskal
      class Edge:
          def __init__(self, x, y, val):
              self.x = x
              self.y = y
              self.val = val
      
      def build_graph(n):
          edges = []
          edges.append(Edge(1, 2, 1))
          edges.append(Edge(1, 3, 1))
          edges.append(Edge(1, 5, 2))
          edges.append(Edge(2, 6, 1))
          edges.append(Edge(2, 4, 2))
          edges.append(Edge(2, 3, 2))
          edges.append(Edge(3, 4, 1))
          edges.append(Edge(4, 5, 1))
          edges.append(Edge(5, 6, 2))
          edges.append(Edge(5, 7, 1))
          edges.append(Edge(6, 7, 1))
          return edges
      
      class UnionFind:
          def __init__(self, n):
              self.father = [i for i in range(n)]
              
          def find(self, u):
              if u != self.father[u]:
                  self.father[u] = self.find(self.father[u])
              return self.father[u]
          
          def union(self, u, v):
              u = self.find(u)
              v = self.find(v)
              if (u == v):
                  return
              self.father[v] = u
              
      def kruskal(edges, n):
          result = 0
          edges.sort(key=lambda edge: edge.val)
          uf = UnionFind(n + 1)
          for edge in edges:
              x = uf.find(edge.x)
              y = uf.find(edge.y)
              if x != y:
                  print("x: {}, y: {}, val: {}".format(edge.x, edge.y, edge.val))
                  result += edge.val
                  uf.union(x, y)
          return result
      
      graph = build_graph(n)
      kruskal(graph, n)
      
    • Prim
      def build_graph(n):
          graph = [[1e6 for _ in range(n + 1)] for _ in range(n + 1)]
          graph[1][2] = 1
          graph[1][3] = 1
          graph[1][5] = 2
          graph[2][6] = 1
          graph[2][4] = 2
          graph[2][3] = 2
          graph[3][4] = 1
          graph[4][5] = 1
          graph[5][6] = 2
          graph[5][7] = 1
          graph[6][7] = 1
          return graph
      
      def prim(graph, n):
          result = 0
          minDist = [1e6 for _ in range(n + 1)]
          isInTree = [False for _ in range(n + 1)]
          for _ in range(1, n + 1):
              cur = -1
              minVal = 1e6 + 1
              for j in range(1, n + 1):
                  if not isInTree[j] and minDist[j] < minVal:
                      cur = j
                      minVal = minDist[j]
              isInTree[cur] = True
              print('cur: {}, minVal: {}'.format(cur, minVal))
              result += minVal
              for j in range(1, n + 1):
                  if not isInTree[j] and graph[cur][j] < minDist[j]:
                      minDist[j] = graph[cur][j]
          return result
      
                      
      n = 7
      graph = build_graph(n)
      prim(graph, n)
      

3.动态规划

  • 思想
    • 通过子问题自底向上求解原问题
  • 适用条件
    • 最优子结构
    • 子问题重叠
  • 案例
    • 最短路径

      • s=(i)s=(i)s=(i), i为第i个节点
      • f(i)=minj∈adj(i)dij+f(j)f(i)=\underset{j \in adj(i)}{min}{d_{ij}+f(j)}f(i)=jadj(i)mindij+f(j), f为第i个节点到目标节点的最短距离
    • 01背包

      • N种物品,有各自的重量和价值。现在有一个容量为V的背包,问背包能装物品的最大价值
      • 物品只能取一次
      • dp(i)dp(i)dp(i):容量为 i 的背包的最大价值
      • 状态转移方程:dp(i)=max(dp[i],dp(i−w[j])+v[j])dp(i) = max(dp[i], dp(i - w[j]) + v[j])dp(i)=max(dp[i],dp(iw[j])+v[j])
    • 完全背包问题

      • n种物品,有各自的重量和价值。现在有一个容量为V的背包,问背包能装物品的最大价值
      • 物品可以重复选取
      • dp(i)dp(i)dp(i):容量为 i 的背包的最大价值
      • 状态转移方程:dp(i)=max(dp[i],dp(i−w[j])+v[j])dp(i) = max(dp[i], dp(i - w[j]) + v[j])dp(i)=max(dp[i],dp(iw[j])+v[j])
    • 系统可靠性问题

      • k种部件,每种部件之间串联,部件内部并联。现有总金额V用于购买部件,求一个购买方案使系统可靠性最高
      • dp(i,j)dp(i,j)dp(i,j):购买前 i 个部件,总金额为 j 时的最大可靠性
      • 状态转移方程:
        • dp(i,j)=maxk(dp(i−1,j−k∗p[i−1])∗pki),pki=1−(1−r[i−1])kdp(i,j) = max_{k}(dp(i - 1, j - k * p[i - 1]) * pki), pki = 1 - (1 - r[i - 1]) ^ kdp(i,j)=maxk(dp(i1,jkp[i1])pki),pki=1(1r[i1])k
    • 编辑距离

      • 两个字符串的差异程度的量化,一个字符串需要多少次操作后变成另一个字符串,操作包括插入删除或修改字符
      • dp(i,j)dp(i,j)dp(i,j):str1前 i 个字符与str2前 j 个字符的最小差异
      • 状态转移方程:
        • str1[i−1]=str2[j−1]str1[i - 1] = str2[j - 1]str1[i1]=str2[j1]
          • dp(i,j)=dp(i−1,j−1)dp(i, j) = dp(i - 1, j - 1)dp(i,j)=dp(i1,j1)
        • str1[i−1]≠str2[j−1]str1[i - 1] \ne str2[j - 1]str1[i1]=str2[j1]
          • dp(i,j)=min(dp(i−1,j−1)+cu,dp(i−1,j)+cd,dp(i,j−1)+ci)dp(i, j) = min(dp(i - 1, j - 1) + c_u, dp(i - 1, j) + c_d, dp(i, j - 1) + c_i)dp(i,j)=min(dp(i1,j1)+cu,dp(i1,j)+cd,dp(i,j1)+ci)
    • 最长公共子序列

      • 可以不连续
      • s=(i,j)s=(i,j)s=(i,j),str1前 i 个字符与str2的前 j 个字符的最长公共子序列长度
      • 状态转移方程:
        • str1[i]=str2[j]str1[i] = str2[j]str1[i]=str2[j]
          • f(i,j)=f(i−1,j−1)+1f(i,j) = f(i-1,j-1)+1f(i,j)=f(i1,j1)+1
        • str1[i]≠str2[j]str1[i] \ne str2[j]str1[i]=str2[j]
          • f(i,j)=maxf(i,j−1),f(i−1,j)f(i,j) = max{f(i,j-1), f(i-1,j)}f(i,j)=maxf(i,j1),f(i1,j)
    • 最长重复子数组

      • 连续
      • dp(i,j)dp(i, j)dp(i,j):str1前 i 个字符与str2的前 j 个字符的最长公共子串长度
      • 状态转移方程:
        • str1[i]=str2[j]str1[i] = str2[j]str1[i]=str2[j]
          • f(i,j)=f(i−1,j−1)+1f(i,j) = f(i-1,j-1)+1f(i,j)=f(i1,j1)+1
        • str1[i]≠str2[j]str1[i] \ne str2[j]str1[i]=str2[j]
          • f(i,j)=0f(i,j) = 0f(i,j)=0
  • 实现
    • 01背包
      volume = 12
      weight = [4, 6, 2, 2, 5, 1]
      value = [8, 10, 6, 3, 7, 2]
      N, V = len(weight), volume
      dp = [0 for i in range(V + 1)]
      for j in range(N):
          for i in range(V, weight[j] - 1, -1):
              dp[i] = max(dp[i], dp[i - weight[j]] + value[j])
      print(dp[V])
      
    • 完全背包
      volume = 12
      weight = [4, 6, 2, 2, 5, 1]
      value = [8, 10, 6, 3, 7, 2]
      N, V = len(weight), volume
      dp = [0 for i in range(V + 1)]
      for j in range(N):
          for i in range(weight[j], V + 1):
              dp[i] = max(dp[i], dp[i - weight[j]] + value[j])
      print(dp[V])
      
    • 系统可靠性
      import math
      p = [30, 15, 20]  # 金额
      r = [0.9, 0.8, 0.5]  # 可靠性
      N, V = len(p), 105
      dp = [[0 for _ in range(V + 1)] for _ in range(N + 1)]
      for i in range(N + 1):
          dp[i][0] = 0
      for j in range(V + 1):
          dp[0][j] = 1
      for i in range(1, N + 1):
          for j in range(1, V + 1):
              for k in range(math.floor(j / p[i - 1]) + 1):
                  pki = 1 - (1 - r[i - 1]) ** k
                  dp[i][j] = max(dp[i][j], dp[i - 1][j - k * p[i - 1]] * pki)
      print(dp[N][V])
      
    • 编辑距离
      str1 = 'love'
      str2 = 'lolpe'
      cost_insert = 1
      cost_delete = 1
      cost_update = 2
      M, N = len(str1), len(str2)
      dp = [[0 for _ in range(N + 1)] for _ in range(M + 1)]
      for i in range(M + 1):
          dp[i][0] = cost_insert * i
      for j in range(N + 1):
          dp[0][j] = cost_delete * j
      for i in range(1, M + 1):
          for j in range(1, N + 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 - 1] + cost_update, dp[i][j - 1] + cost_insert, dp[i - 1][j] + cost_delete)
      print(dp[M][N])
      
    • 最长公共子序列
      str1 = 'love'
      str2 = 'lolpe'
      M, N = len(str1), len(str2)
      dp = [[0 for _ in range(N + 1)] for _ in range(M + 1)]
      for i in range(1, M + 1):
          for j in range(1, N + 1):
              if str1[i - 1] == str2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1] + 1
              else:
                  dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
      print(dp[M][N])
      
    • 最长重复子数组
      str1 = 'love'
      str2 = 'lolpe'
      M, N = len(str1), len(str2)
      maxlen = 0
      dp = [[0 for _ in range(N + 1)] for _ in range(M + 1)]
      for i in range(1, M + 1):
          for j in range(1, N + 1):
              if str1[i - 1] == str2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1] + 1
              else:
                  dp[i][j] = 0
              if dp[i][j] > maxlen:
                  maxlen = dp[i][j]
      print(maxlen)
      

4.搜索和遍历

  • 图的搜索
    • 无知搜索
      • dfs:用栈实现
      • bfs:用队列实现
    • 有知搜索:先验知识
    • 启发式搜索
      • A*:用优先队列实现
  • 二叉树遍历
    • 前序遍历、中序遍历、后序遍历、层序遍历
  • 双连通分量
    • 双连通图:一个无向连通图是双连通图当且仅当不含有割点(关节点)
    • 割点的判断
      • 根节点:至少包含两个孩子
      • 非根节点:孩子的最低深度优先数大于等于自己的深度优先数
          1. 求出深度优先生成树
          1. 后序遍历顺序求出最低深度优先数
        • low[u]=min{d[u],low[w],d[x]}low[u]=min\{d[u],low[w],d[x]\}low[u]=min{d[u],low[w],d[x]}
  • 实现
    • dfs
      def build_graph(n):
          graph = [[0 for _ in range(n + 1)] for _ in range(n + 1)]
          graph[1][2] = 1
          graph[1][3] = 1
          graph[1][4] = 1
          graph[1][5] = 1
          graph[2][4] = 1
          graph[3][5] = 1
          graph[4][5] = 1
          return graph
      
      def dfs(graph, n, start):
          visited = [False for _ in range(n + 1)]
          stack = []
          visited[start] = True
          stack.append(start)
          while len(stack) != 0:
              i = stack.pop()
              print(i)
              for j in range(n + 1):
                  if not visited[j] and graph[i][j]:
                      visited[j] = True
                      stack.append(j)
      
      n = 5
      graph = build_graph(n)                
      dfs(graph, n, 1)
      
    • bfs
      def build_graph(n):
          graph = [[0 for _ in range(n + 1)] for _ in range(n + 1)]
          graph[1][2] = 1
          graph[1][3] = 1
          graph[1][4] = 1
          graph[1][5] = 1
          graph[2][4] = 1
          graph[3][5] = 1
          graph[4][5] = 1
          return graph
      
      def bfs(graph, n, start):
          visited = [False for _ in range(n + 1)]
          queue = []
          visited[start] = True
          queue.append(start)
          while len(queue) != 0:
              i = queue.pop(0)
              print(i)
              for j in range(n + 1):
                  if not visited[j] and graph[i][j]:
                      visited[j] = True
                      queue.append(j)
      
      n = 5
      graph = build_graph(n)                
      bfs(graph, n, 1)
      
    • 二叉树遍历(递归)
      def inorder(root):
          if root is None:
              return
          inorder(root.left)
          print(root.value, end=' ')
          inorder(root.right)
      
      def preorder(root):
          if root is None:
              return
          print(root.value, end=' ')
          preorder(root.left)
          preorder(root.right)
      
      def postorder(root):
          if root is None:
              return
          postorder(root.left)
          postorder(root.right)
          print(root.value, end=' ')
      
    • 二叉树的遍历(迭代)
      def inorder(root):
          stack = []
          if (root != null):
              stack.append(root)
          while len(stack) != 0:
              node = stack.pop()
              if node != null:
                  if node.right != null:
                      stack.append(node.right)
                  stack.append(node)
                  stack.append(null)
                  if node.left != null:
                      stack.append(node.left)
              else:
                  tmp = stack.pop()
                  print(tmp.val, end=' ')
      
      def preorder(root):
          stack = []
          if (root != null):
              stack.append(root)
          while len(stack) != 0:
              node = stack.pop()
              if node != null:
                  if node.right != null:
                      stack.append(node.right)
                  if node.left != null:
                      stack.append(node.left)
                  stack.append(node)
                  stack.append(null)
              else:
                  tmp = stack.pop()
                  print(tmp.val, end=' ')
      
      def postorder(root):
          stack = []
          if (root != null):
              stack.append(root)
          while len(stack) != 0:
              node = stack.pop()
              if node != null:
                  stack.append(node)
                  stack.append(null)
                  if node.right != null:
                      stack.append(node.right)
                  if node.left != null:
                      stack.append(node.left)
              else:
                  tmp = stack.pop()
                  print(tmp.val, end=' ')
      
      def levelorder(root):
          queue = []
          if (root != null):
              queue.append(root)
          while len(stack) != 0:
              count = len(stack)
              while count > 0:
                  node = queue.pop(0)
                  print(node.val, end=' ')
                  if node.left != null:
                      queue.append(node.left)
                  if node.right != null:
                      queue.append(node.right)
                  count = count - 1
      

5.回溯

  • 算法思想
    • 按深度优先的方式搜索解空间,不满足限界函数时回溯,找出满足条件的所有解
  • 形式
    • 递归回溯
    • 迭代回溯
  • 案例
    • 汉诺塔问题
      • 求出一个方案,将起始柱子上的盘子全部移到目标柱子上
      • 证明:当存在第三个空的缓冲柱子时,能将n个盘子从起始柱子移到目标柱子
      • 数学归纳法:
        • 当n==1时,显然成立
        • 设当n==k时,若存在第三个空的缓冲柱子,能将k个盘子从起始柱子移到目标柱子
        • 当n==k+1时,先将k个盘子从起始柱子移到第三个空的缓冲柱子(利用假设条件),再将剩下的一个盘子移到目标柱子,最后将缓冲柱子上的k个盘子移到目标柱子(再次利用假设条件)
    • 装载问题
      • n个集装箱装上两艘轮船,求出装载方案
    • 符号三角形问题
      • 加号和减号的数量相同
    • N皇后问题
      • 行、列、斜线不允许出现两个皇后,求出所有排列方案
    • 0-1背包问题
      • 背包容量有限,每个物品装或者不装,求所装物品的最大价值
    • 最大团问题
      • 最大团:顶点数量最多的完全子图
    • 着色问题
      • 图中相邻顶点的颜色不允许重复,求颜色种类的最小值
    • 旅行商问题
      • 哈密顿环:遍历图中每个顶点有且仅有一次,最后回到出发点
    • 圆排列问题
      • n个圆的排列中,找出最短长度的圆排列
    • 连续邮资问题
      • n种邮票,最多贴m张,允许重复,能够表示的最大连续邮资区间
    • 批处理作业调度问题
      • n个作业需要先后在两台机器上处理,设计调度方案,使完成时间和最小
  • 实现
    • 汉诺塔问题
      class Plate:
          def __init__(self, size):
              self.size = size
      
      class Pole:
          def __init__(self, name, n):
              self.name = name
              self.plates = [Plate(i) for i in range(n, 0, -1)]
      
      class Hanoi:
          def __init__(self, n):
              self.start = Pole('start', n)
              self.end = Pole('end', 0)
              self.temp = Pole('temp', 0)
          def move(self, start, end):
              plate = start.plates.pop()
              end.plates.append(plate)
              print('move {} from {} to {}'.format(plate.size, start.name, end.name))
          def move_n(self, start, end, temp, n):
              if n == 0:
                  return
              self.move_n(start, temp, end, n - 1)
              self.move(start, end)
              self.move_n(temp, end, start, n - 1)
      
      n = 3
      hanoi = Hanoi(n)
      hanoi.move_n(hanoi.start, hanoi.end, hanoi.temp, n)
      
    • N皇后
      class n_queens:
          def __init__(self, n):
              self.n = n
              self.solutions = []
              self.places = [0 for i in range(n)]
          def valid(self, k):
              for i in range(k):
                  if abs(self.places[i] - self.places[k]) == abs(i - k) or self.places[i] == self.places[k]:
                      return False
              return True
          def dfs(self, k):
              if k == self.n:
                  print('places:', self.places)
                  self.solutions.append(self.places.copy())
                  return
              for j in range(self.n):
                  self.places[k] = j
                  if self.valid(k):
                      self.dfs(k + 1)
          def solve(self):
              self.dfs(0)
      
    • 子集
      class sumOfSub:
          def __init__(self, arr, target):
              self.arr = arr
              self.target = target
              self.select = [0 for _ in range(len(arr))]
              self.total = 0
              self.solutions = []
          def dfs(self, k):
              if self.total >= self.target:
                  if self.total == self.target:
                      self.solutions.append(self.select.copy())
                  return
              if k < len(self.arr):
                  self.select[k] = 1
                  self.total = self.total + self.arr[k]
                  self.dfs(k+1)
                  self.select[k] = 0
                  self.total = self.total - self.arr[k]
                  self.dfs(k+1)
          def solve(self):
              self.dfs(0)
      

6.分支限界

  • 算法思想
    • 按广度优先的方式搜索解空间,不满足上界函数时剪枝,找出满足条件的一个解
  • 形式
    • FIFO队列
    • 优先队列
  • 案例
    • 装载问题
      • 优先队列:存储当前节点的重量上界
      • 扩展:每次选择重量上界最高的节点进行扩展
    • 01背包问题
      • 预处理:物品按单位重量价值降序排列
      • 优先队列:存储当前节点的价值上界
      • 扩展:每次选择价值上界最高的节点进行扩展
    • 单源最短路径
      • 问题描述:两点之间的最短路径
      • 优先队列:存储当前节点已经走过的路径长度
      • 扩展:每次选择路径长度最短的节点进行扩展
    • 布线问题
    • 最大团问题
    • 旅行商问题
    • 电路板排列问题
    • 批处理作业调度问题

NP完全性理论

  • 问题的复杂度
    • O(log⁡n),O(n),O(nlog⁡n),O(n2),O(2n),O(n!)O(\log n), O(n), O(n\log n), O(n^2), O(2^n), O(n!)O(logn),O(n),O(nlogn),O(n2),O(2n),O(n!)
    • 当问题规模很大时,O(2n),O(n!)O(2^n), O(n!)O(2n),O(n!)算法在计算机上无解
  • P、NP、NPC
    • P问题
      • 线性图灵机在多项式时间内找到一个解
      • P问题是NP问题的子集
    • NP问题
      • 线性图灵机在多项式时间内验证一个解
      • P=NP?P=NP?P=NP?
    • NPC问题
      • NP问题
      • 所有NP问题可以在多项式时间内归约到它
      • 归约:A问题可以归约为B,表示可以用B的解法来解决A,其中B的复杂度大于等于A
      • 第一个NPC问题:逻辑电路问题
    • NP-Hard问题
      • 不一定是NP问题
      • 所有NP问题可以在多项式时间内归约到它

参考资料

  • Cormen T H, Leiserson C E, Rivest R L, et al. Introduction to algorithms[M]. MIT press, 2022.
  • Sahni S, Horowitz E, Rajasekaran S. Computer Algorithms[M]. WH Freeman Press, 1998.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值