【数据结构4】递归



2017年08月24日    Author:Guofei

文章归类: 0x80_数据结构与算法    文章编号: 541

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2017/08/24/recursion.html


算法流程

用递归的一般方法:(我总结的)

def 判断是否到达叶节点(已选解集合):
  # 已选解集合符合最终要的结果,例如符合某种查找
    return 是否符合最终要的结果


结果集 = list()


def myfunc(已选解集合selected, 每个阶段的可选解selectable):
    if 判断是否到达叶节点(已选解集合):
        结果集.append(已选解集合)
        return

    # 遍历每个阶段的可选解集合
    for 可选解 in 每个阶段的可选解:
        # 进入下一个阶段
        myfunc(已选解集合+[可选解], 下个阶段可选的空间解)

recursion 本质上是树型结构上的一个 DFS,以树的视角来看,思路往往更清晰。

公式

def meet_leef(selected):
    # 自定义代码块1,用来判断是否递归到了叶节点
    return True or False


res = list()


def myfunc(selected, selectable):
    if meet_leef(selected):
        res.append(selected)
        return

    for next_item in selectable:
        # 这个是下一阶段的迭代逻辑,中高难度题这2行需要仔细设计
        next_selected = selected + [next_item]  # 自定义代码块2,子节点接收的树
        next_selectable = selectable  # 自定义代码块3,子节点的可用解
        myfunc(next_selected, next_selectable)

回溯算法(某些场景下能用,比较上面的方法节省内存)

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径) # 有时候要copy
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

参见:46. Permutations

DFS 做 count 类的题目

  • 区别是return 这个 count 值,
  • 往往也需要用 visited (也就是DP方法)做个缓存,否则经常超时
  • 典型题目70/91题。

DFS+DP解决count类问题

visited = dict()

def dfs(s):
    if s in visited:
      return visited[s]
    if 结束条件1:
      res = 计算数量()
    elif 结束条件2: # 注意这里为了放入缓存,用elif,而不是直接 return
      res = 计算数量()
    else:
      res = dfs(s[1:]) + dfs(s[2:])

    visited[s]=res
    return res

BFS

  • 如果我们只是要找到一种符合的情况,那么深度优先DFS的效率更高
  • 而如果我们要找到所有的符合的情况,那么广度优先BFS的效率更高。
  • (从做的几个题看)count 类的,似乎 DFS+DP更好写。虽然BFS也可以,但有些麻烦,例如 70/91题。
  • DFS用递归更好写
  • BFS用队列+while循环更好写。例如,树的level order。

典型题目 https://leetcode-cn.com/problems/shortest-path-in-binary-matrix/

BFS:

queue = [root]
visited = {0:0} # 用于记录已访问的位置,也可以用来存放求结果的必要信息。有的场景可以简化为 list


while queue:
    next_queue=set()
    for item in queue:
        如果到了终点做某个操作
        update_visted(visited) # 更新记录
        next_items=generate_next_item(item) # 生成下一步的节点,这一步过滤掉不可能的节点。
        new_queue.update(next_items) # 更新队列

细节:

  1. 写树的level order的时候,queue用的是 list,这对于树没问题。如果某个节点有多个父节点,queue 要用 set,否则queue中的节点会无意义的重复(每个父节点都产生一个)。不过树的父节点只有1个,用list反而更快,不用hash了。
  2. 用每次迭代+set 来代替 queue 操作,代码更清晰一些,目前见过的场景都能这么做(不确定是否所有场景都可以)。

套用公式

套公式的示例1:放回抽样,从6个数里面抽取3个。改动自定义代码块1。

nums, total = [1, 2, 3, 4, 5, 6], 3


def meet_leef(selected):
    if len(selected) >= 3:
        return True


res = list()


def myfunc(selected, selectable):
    if meet_leef(selected):
        res.append(selected)
        return

    for next_item in selectable:
        next_selected = selected + [next_item]
        next_selectable = selectable
        myfunc(next_selected, next_selectable)


myfunc(list(), nums)

res

套公式的示例2:无放回抽样,从6个数里面抽取3个。
有两种方案,两种方案在其它题目中各有优劣,都写一份。
改动自定义代码块2:

nums, total = [1, 2, 3, 4, 5, 6], 3


def meet_leef(selected):
    if len(selected) >= 3:
        return True


res = list()


def myfunc(selected, selectable):
    if meet_leef(selected):
        res.append(selected)
        return

    for next_item in selectable:
        if next_item not in selected:
            next_selected = selected + [next_item]
            next_selectable = selectable
            myfunc(next_selected, next_selectable)


myfunc(list(), nums)

res

或者改动自定义代码块3:

nums, total = [1, 2, 3, 4, 5, 6], 3


def meet_leef(selected):
    if len(selected) >= 3:
        return True


res = list()


def myfunc(selected, selectable):
    if meet_leef(selected):
        res.append(selected)
        return

    for next_idx, next_item in enumerate(selectable):
        next_selected = selected + [next_item]
        next_selectable = selectable.copy()
        next_selectable.pop(next_idx)
        myfunc(next_selected, next_selectable)


myfunc(list(), nums)

res

应用案例

求阶乘

def myfunc(n):
    if n == 0:
        return 1
    return n * myfunc(n - 1)

蛙跳问题

一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,问要跳上第 n 级台阶有多少种跳法?

@functools.lru_cache()
def myfun(n):
    if n <= 2:
        return n
    return myfun(n - 1) + myfun(n - 2)

还有其它解法

  • 动态规划
  • 直接遍历

镜像二叉树

经典问题:https://leetcode.com/problems/invert-binary-tree/

递归法很简单

def invertTree(self, root):
    if not root:return None
    root.left,root.right=self.invertTree(root.right),self.invertTree(root.left)
    return root

但我们不想仅仅用递归法,想到树上的 level order 算法,稍微改一改:

def invertTree(self, root):
    q=[root]
    while q:
        for node in q:
            if node:
                node.left,node.right=node.right,node.left
        q=[child for node in q if node for child in [node.left,node.right]]
    return root

TODO: 上面是 level order,可以试试按照对应的套路,改成 DFS

求一个数列的全部组合

res = list()

nums = [1, 2, 3]


def myfunc(selected, selectable):
    if len(selected) == len(nums):
        res.append(selected)
        return res
    for i in selectable:
        if i not in selected:
            myfunc(selected + [i], selectable)


myfunc([], nums)

用 level order 改编套路,改成非递归形式

ret, q = [[]], [[i] for i in nums]
for i in range(len(nums)):
    ret = [ret_ + [i] for i in nums for ret_ in ret if i not in ret_]

背包问题

套公式

selectable = [3, 4, 6, 8]
max_weight = 10

res = list()


def myfunc(selected, selectable):
    if sum(selected) > max_weight:
        res.append([selected[:-1]])
        return

    for i in selectable:
        if i not in selected:
            myfunc(selected + [i], selectable)

myfunc([],selectable)

四皇后问题

问题描述:
如何在N×N的棋盘上放N个皇后,使得:

  1. 不能有两个皇后出现在一条横线上
  2. 不能有两个皇后出现在一条竖线上
  3. 不能有连个皇后出现在一条斜线上

(下面的代码还有改进的空间)

import numpy as np


def iscorrect(i, j, M):
    m, n = M.shape
    if any(M[i, :]): return False
    if any(M[:, j]): return False
    if any(np.diag(M, j - i)): return False
    if any(np.diag(np.fliplr(M), n - 1 - j - i)): return False
    return True


m = 4
n = 4
M = np.zeros(shape=(m, n))


def queen(k, m, M):
    for l in range(k+1,m*m):
        i=l%m
        j=l//m
        if iscorrect(i,j,M):
            M[i,j]=1
            queen(l,m,M)
            M[i,j]=0
    if M.sum()==4:
        print(M)


queen(-1,m, M)

逻辑教授三学生问题

整数划分问题

定义整数的一个 划分 :
$n=n_1+n_2+…+n_k, (n_1 \geq n_2 \geq n_3… \geq n_k \geq 1)$
例如,6=2+1+1+1+1是一个划分。

问题 写一段程序,输入任意正整数,输出这个正整数有多少划分。

分析
设$P(n,m)$表示正整数m的所有的划分中,不大于m的划分个数。
例如,$P(6,1)=1$,因为6=1+1+1+1+1+1

可以建立以下的递推关系:

  1. $P(n,1)=1$
  2. $P(n,m)=P(n,n), m> n$
  3. $P(n,n)=P(n,n-1)+1$
  4. $P(n,m)=P(n,m-1)+P(n-m,m)$

下面很容易了:

def P(n,m):
    if m==1:return 1
    elif m>n:return P(n,n)
    elif m==n:return P(n,n-1)+1
    elif m<n:return P(n,m-1)+P(n-m,m)

print(P(6,6))

上楼梯问题

已知楼梯有20阶台阶,上楼可以一步1阶,也可以一步2阶,计算有多少种上楼的方法

c = [0]


def stepproblem(step, totalstep):
    totalstep = totalstep + step  # 这一个节点所要做的操作
    if totalstep > 20:  # 不符合要求的叶
        return 0
    if totalstep == 20:  # 符合要求的叶
        c[0] = c[0] + 1  # 利用list共享内存的特性
        return 1
    if totalstep < 20:  # 节点
        stepproblem(1, totalstep)
        stepproblem(2, totalstep)


stepproblem(0, 0)
print(c)

全排列问题

A是一个list,生成A的所有排列

n = 5
A = list(range(n))


def makelist(A, B):  # B是选出来的序列,A是还没用到的数

    if len(A) == 0:  # 叶
        print(B)
    else:  # 结
        for i in A:
            A_copy = A.copy()
            B_copy = B.copy()
            A_copy.remove(i)
            B_copy.append(i)
            makelist(A_copy, B_copy)


B = []
makelist(A, B)

幻方问题

幻方是这样一种方阵,每一行、每一列以及两个对角线之和均相等
用全排列问题给出的方法进行全排列,然后给出幻方

import numpy as np


def ismagic(M):
    M = np.array(M)
    m, n = M.shape
    temp_sum = M[0].sum()
    for i in range(1, m):  # 列
        if not M[i].sum() == temp_sum:
            return False
    for i in range(n):
        if not M[:, i].sum() == temp_sum:
            return False
    if not np.trace(M) == temp_sum:
        return False
    M_c = np.fliplr(M)
    if not np.trace(M_c) == temp_sum:
        return False
    return True


def list2sqrt(M):
    M = np.array(M)
    m, = M.shape
    M.shape = m ** 0.5, m ** 0.5
    return M


def makelist(A, B):  # B是选出来的序列,A是还没用到的数

    if len(A) == 0:  # 叶
        M = list2sqrt(B)
        if ismagic(M):
            print(M)
    else:  # 结
        for i in A:
            A_copy = A.copy()
            B_copy = B.copy()
            A_copy.remove(i)
            B_copy.append(i)
            makelist(A_copy, B_copy)


B = []
n = 9
A = list(range(1, n + 1))
makelist(A, B)

递归法求幂

计算$m^n$时,把m连乘n次,这种算法效率是很低的,注意到以下迭代关系:

\[m^n= \left \{ \begin{array}{ccc} 1&n=0\\ m&n=1\\ m^k * m^k&n=2k\\ m*m^k&n=2k+1 \end{array}\right.\]

立即得出结果:

def power_func(m, n):
    if n == 0:
        return 1
    elif n == 1:
        return m
    elif n % 2 == 0:
        return power_func(m, n / 2) * 2
    elif n % 2 == 1:
        return m * power_func(m, n - 1)

汉诺塔问题

经典问题,不描述。

首先,确定这是一个递归问题,(要思考一番)
其次,确定这个递归问题如何表示(要思考更长)

def move(n, x, y, z):  # 将n个盘子从x移动到z
    if n == 1:
        print('{} to {}'.format(x, z))
    else:
        move(n - 1, x, z, y)
        print('{} to {}'.format(x, z))
        move(n-1,y,x,z)

move(3,'A','B','C')

您的支持将鼓励我继续创作!