笔记:蓝桥python搜索(3-1)——DFS基础和DFS回溯

  • 搜索算法:穷举问题解空间部分/所有情况,从而求出问题的解

  • 深度优先搜索:

  • 本质上是暴力枚举

  • 深度优先:尽可能一条路走到底,走不了再回退

  • 目录

    二、DFS和n重循环

    三、例题分析

    例题 P4124 分糖果

    例题 P3505 买瓜

    四、回溯法

    五、回溯模板

    六、例题分析

    例题 P1508 N皇后

    例题 P182 小朋友崇拜圈

    例题 P178 全球变暖

    输入描述


    二、DFS和n重循环

  • 给定一个数字x,将其拆分成3个正整数,后一个要求大于等于前一个,给出方案。

  • 最简单的思想:三重循环暴力求解

  • 如果是拆分成n个正整数?

  • 就需要实现n重循环

  • n重循环=特定的树状结构=DFS搜索

  •  给定一个数字x,将其拆分成n个正整数,后一个要求大于等于前一个,给出方案。

  • n重循环:n层的树
  • DFS:从上往下找一条合法的路径(路径值不递减、长度为n,和为x)

  • def dfs(depth):
        """
        :param depth: 当前为第几重循环
        :return:
        """
        global cnt
        cnt = cnt + 1
        #第0重 - 第n-1重都已经选好数字, 此时只需要判断答案
        if depth == n:
            #条件一: 数字需要递增
            for i in range(1, n):
                if a[i] >= a[i - 1]:
                    continue
                else:
                    return
            #条件二: 数字和为x
            if sum(a)!= x:
                return
            #此时是答案
            print(a)
            return
        #第depth层进行选择数字:[1,x]进行枚举
        for i in range(1, x + 1):
            #选择第depth层的数字
            a[depth] = i
            #递归进入下一层
            dfs(depth + 1)
       
    x, n = map(int, input().split())   
    #记录每层选择数字   
    a = [0] * n   
    #计算次数: cnt   
    cnt = 0   
    dfs(0)   
    print("累计计算次数={}".format(cnt))

  • 把条件放在枚举的时候判断可以降低计算量:剪枝
  • def dfs(depth,last_val):
        """
    
        :param depth:当前第几重循环
        :param last_val: 从上一层的开始枚举而不是从 1 开始(递增)
        :return:
        """
        global cnt
        cnt=cnt+1
        # 第0重-第n-重都已经选好了数字,此时只需要判断答案
        if depth==n:
            # 条件二:数字和为x
            if sum(a)!=x:
                return
            print(a)
            return
        #第depth层进行选择数字:[1,x]进行枚举
        for i in range(last_val,x+1):
            a[depth]=i
            dfs(depth+1,i)
    x, n = map(int, input().split())
    #记录每层选择数字
    a = [0] * n
    #计算次数: cnt
    cnt = 0
    dfs(0,1)    # 只枚举正整数
    print("累计计算次数={}".format(cnt))
    

     模板

    def dfs(depth):
        """
        :param depth: 当前为第几重循环
        :return:
        """
        if depth == N:
            #N重循环最内层执行的代码
            return
        #每重循环进行的枚举选择
    

    三、例题分析

    例题 P4124 分糖果


    两种糖果分别有 9 个和 16 个,要全部分给 7 个小朋友,每个小朋友得到的糖果总数最少为 2 个最多为 5 个,问有多少种不同的分法。糖果必须全部分完。

    只要有其中一个小朋友在两种方案中分到的糖果不完全相同,这两种方案就算作不同的方案。

    答案提交

    这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。


    7重循环,每重循环表示1个小朋友 每个小朋友枚举所有的糖果情况

    ans = 0
    
    
    def dfs(depth, n, m):
        """
        :param depth: 第depth个小朋友
        :param n: 第一类糖果数量
        :param m: 第二类糖果数量
        :return:
        """
        if depth == 7:
            # 分完后没有糖果
            if n == 0 and m == 0:
                global ans
                ans += 1
            return
        # 每个小朋友的情况
        # 枚举第一种糖果
        for i in range(0, 6):
            # 枚举第二种糖果
            for j in range(0, 6):
                # 第depth个小朋友有第i个第一种,j个第二种
                if 2 <= i + j <= 5 and i <= n and j <= m:
                    dfs(depth + 1, n - i, m - j)
    
    
    dfs(0, 9, 16)
    print(ans)

    例题 P3505 买瓜


    小蓝正在一个瓜摊上买瓜。瓜摊上共有 n 个瓜,每个瓜的重量为 Ai​。

    小蓝刀功了得,他可以把任何瓜劈成完全等重的两份,不过每个瓜只能劈一刀。

    小蓝希望买到的瓜的重量的和恰好为 m。

    请问小蓝至少要劈多少个瓜才能买到重量恰好为 m 的瓜。如果无论怎样小蓝都无法得到总重恰好为 m 的瓜,请输出 −1。

    输入格式

    输入的第一行包含两个整数 n,m,用一个空格分隔,分别表示瓜的个数和小蓝想买到的瓜的总重量。

    第二行包含 n 个整数 Ai​,相邻整数之间使用一个空格分隔,分别表示每个瓜的重量。

    输出格式

    输出一行包含一个整数表示答案。


    N重循环,每重循环三种情况:

  • 买1个
  • 买一半
  • 不买
  • def dfs(depth, s, cnt):
        """
        :param depth: 第depth层循环
        :param s: 当前累计的重量
        :param cnt: 当前的劈的西瓜个数
        :return:
        """
        # 递归出口
        # 1、当前累计重量已经超过m, 不需要继续下去
        if s > m:
            return
        # 2、当前累计重量等于m, 直接更新答案
        if s == m:
            global ans
            ans = min(ans, cnt)
        # 3、第depth层时, 停止继续递归
        if depth == n:
            return
        # 买一个
        dfs(depth + 1, s + w[depth], cnt)
        # 买半个
        dfs(depth + 1, s + w[depth] // 2, cnt + 1)
        # 不买
        dfs(depth + 1, s, cnt)
    
    
    n, m = map(int, input().split())
    m *= 2  # 避免小数
    w = list(map(int, input().split()))
    w = [x * 2 for x in w]
    ans = n + 1
    
    dfs(0, 0, 0)
    if ans == n + 1:
        ans = -1
    print(ans)

    目前的算法能通过65%


    四、回溯法

  • 回溯:就是DFS的一种,在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

  • 回溯更强调:此路不通,另寻他路,走过的路需要打标记

  • 回溯法一般在DFS的基础上加上一些剪枝策略

  • 五、回溯模板

  • 排列要求数字不重复——每次选择的数字需要打标记——vis数组
  • 要输出当前排列——记录路径——path数组
  • 回溯:先打标记、记录路径、然后下一层、回到上一层、清除标记
  • def dfs(depth):
        #depth:第depth个数字
    
        if depth == n:
            print(path)
            return
       
        for i in range(1,n+1):
            #选择的数字必须未标记
            if vis[i]:
                continue
            #第depth个数字选择i
            vis[i]=True
            path.append(i)
            dfs(depth+1)
    # 回溯操作,处理下一个数字,当前数字i标记为待使用
            vis[i]=False
            path.pop(-1)
    
    n=int(input())
    vis=[False]*(n+1)
    path=[]
    dfs(0)
    def dfs(depth):
        #当前为第depth个数字, 0到depth - 1已经设置好
        if depth == n:
            print(path)
            return
        
        #枚举第depth个数字
        for i in range(1, n + 1):
            #数字i必须先前未选择
            if vis[i] is False:
                #标记当前状态
                vis[i] = True
                #记录当前路径
                path.append(i)
                #进行下一层搜索
                dfs(depth + 1)
                #回溯,清空标记
                vis[i] = False
                path.pop(-1)
       
    n = int(input())   
    path = []   
    vis = [False] * (n + 1)   
    dfs(0)
    

     给定N个数字,求子集

    def dfs(depth):
        #depth:第depth个数字
        if depth == n:
            print(path)
            return
    
        # 选
        path.append(a[depth])
        dfs(depth + 1)
        path.pop(-1)
    
        # 不选
        dfs(depth + 1)
    
    n=int(input())
    a = list(map(int, input().split()))
    # vis=[False]*(n+1)
    path=[]
    dfs(0)

    六、例题分析

    例题 P1508 N皇后

    在 N×N 的方格棋盘放置了 N 个皇后,使得它们不相互攻击(即任意 2 个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成 45° 角的斜线上。你的任务是,对于给定的 N,求出有多少种合法的放置方法。

    输入描述

    输入中有一个正整数 N≤10,表示棋盘和皇后的数量

    输出描述

    为一个正整数,表示对应输入行的皇后的不同放置数量。


    dfs枚举每一行放置的列 标记:

  • 每列只能放一个

  • 主对角线:x + y

  • 副对角线:x – y + n

  • def dfs(x):
        if x==n+1:
            global ans
            ans=ans+1
            return
    
        for y in range(1,n+1):
            #列、主对角线、副对角线不能被攻击到
            if vis1[y]  or vis2[x+y]  or vis3[x-y+n]:
                continue
    
            #(x,y)合法
            #打标记,进入下一层搜索,回到该层,清空标记
            # 标记当前状态
            vis1[y] = vis2[x+y] = vis3[x-y+n] = True
            dfs(x + 1)
            # 清除标记
            vis1[y] = vis2[x+y] = vis3[x-y+n] = False
    
    
    n = int(input())
    ans = 0
    # 分别表示列、主对角线、副对角线是否被标记
    vis1 = [False] * (n + 1)
    vis2 = [False] * (2 * n + 1)
    vis3 = [False] * (2 * n + 1)
    # 如果下标设置从一开始
    dfs(1)
    print(ans)
    
    def dfs(depth):
        # 当前为第depth行数字, 0到depth - 1行已经设置好
        if depth == n:
            global ans
            ans = ans + 1
            return
    
        # 枚举第depth行放在哪列
        for i in range(1, n + 1):
            # 坐标为: (depth, i)
            # 第i列先前未选择、主对角线未选择、副对角线未选择
            if vis1[i] is False and vis2[depth + i] is False and vis3[depth - i + n] is False:
                # 标记当前状态
                vis1[i] = True
                vis2[depth + i] = True
                vis3[depth - i + n] = True
                dfs(depth + 1)
                # 清除标记
                vis1[i] = False
                vis2[depth + i] = False
                vis3[depth - i + n] = False
    
       
    n = int(input())   
    ans = 0   
    vis1 = [False] * (n + 1)   
    vis2 = [False] * (2 * n + 1)   
    vis3 = [False] * (2 * n + 1)   
    dfs(0)   
    print(ans)
    
    

    例题 P182 小朋友崇拜圈

    班里 N 个小朋友,每个人都有自己最崇拜的一个小朋友(也可以是自己)。

    在一个游戏中,需要小朋友坐一个圈,每个小朋友都有自己最崇拜的小朋友在他的右手边。

    求满足条件的圈最大多少人?

    小朋友编号为 1,2,3,⋯N。

    输入描述

    输入第一行,一个整数 N(3<N<10^5)。

    接下来一行 N 个整数,由空格分开。

    输出描述

    要求输出一个整数,表示满足条件的最大圈的人数。


    dfs(x, length):走到 x,已经走了 length 步

    import sys
    #扩栈:递归层数过大,需要设置最大栈空间
    sys.setrecursionlimit(100000)
    
    
    #当前位于x,步长为length
    def dfs(x, length):
        #记录当前x步长length步
        vis[x] = length
    
        #走下一个点
        #如果下一点已经走过,则说明当前已经进入了一个圈
        if vis[a[x]]!=0:
            #更新最大圈长
            global ans
            ans = max(ans, length - vis[a[x]] + 1)
        #下一步没走过,走下一步
        else:
            dfs(a[x], length + 1)
    
    
    n = int(input())
    a = list(map(int, input().split()))
    a = [0] + a
    vis = [0] * (n + 1)
    ans = 0
    for i in range(1, n + 1):
        # 对于每个单独的圈都要做一次dfs
        if vis[i] == 0:
            dfs(i, 1)
    print(ans)
    import sys
    sys.setrecursionlimit(100000)
    
    
    def dfs(x, length):
        # print(x, length)
        # 记录当前x,已经走了length步
        vis[x] = length
    
        # 如果下一步已经走过,则说明当前已经进入了一个圈
        if vis[a[x]]:
            # 更新最大圈
            global ans
            ans = max(ans, length - vis[a[x]] + 1)
        # 下一步没走过,走下一步
        else:
            dfs(a[x], length + 1)
    
    
    n = int(input())
    a = list(map(int, input().split()))
    a = [0] + a
    vis = [0] * (n + 1)
    ans = 0
    for i in range(1, n + 1):
        if vis[i] == 0:
            dfs(i, 1)
    print(ans)

    例题 P178 全球变暖

    你有一张某海域 NxN 像素的照片,"."表示海洋、"#"表示陆地,如下所示:

    …….

    .##….

    .##….

    ….##.

    ..####.

    …###.

    …….

    其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有 2 座岛屿。

    由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。具体来说如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。

    例如上图中的海域未来会变成如下样子:

    …….

    …….

    …….

    …….

    ….#..

    …….

    …….

    请你计算:依照科学家的预测,照片中有多少岛屿会被完全淹没。

    输入描述

    第一行包含一个整数 N (1≤N≤1000)N。

    以下 N 行 N 列代表一张海域照片。

    照片保证第 1 行、第 1 列、第 N 行、第 N 列的像素都是海洋。、

    输出一个整数表示答案。


    1、求解当前有多少个岛屿

    2、如果当前岛屿中不存在“高地”,则该岛屿会被淹没

    import sys   
    sys.setrecursionlimit(1000000)
       
    # 方向数组,用于表示上下左右四个方向   
    dir = [(1, 0), (0, 1), (-1, 0), (0, -1)]
       
    # 深度优先搜索函数   
    def dfs(x, y, N, Map, vis, flag):
        # 标记当前点
        vis[x][y] = 1
        # 判断当前点是否为高地
        is_high_ground = True
        for dx, dy in dir:
            new_x, new_y = x + dx, y + dy
            # 检查边界和是否为陆地
            if 0 <= new_x < N and 0 <= new_y < N and Map[new_x][new_y]!= '#':
                is_high_ground = False
                break
        if is_high_ground:
            flag = 1
        # 把四周的未遍历的土地继续遍历
        for i in range(4):
            xx, yy = x + dir[i][0], y + dir[i][1]
            # 检查边界、是否为陆地以及是否未访问
            if 0 <= xx < N and 0 <= yy < N and Map[xx][yy] == '#' and vis[xx][yy] == 0:
                flag = dfs(xx, yy, N, Map, vis, flag)
        return flag
       
    N = int(input())   
    Map = []   
    vis = []   
    for i in range(N):
        Map.append(list(input()))
        vis.append([0] * N)
       
    ans = 0   
    for i in range(N):
        for j in range(N):
            if Map[i][j] == '#' and vis[i][j] == 0:
                flag = 0
                flag = dfs(i, j, N, Map, vis, flag)
                if flag == 0:
                    ans += 1
       
    print(ans)

    作者:寒舍书生

    物联沃分享整理
    物联沃-IOTWORD物联网 » 笔记:蓝桥python搜索(3-1)——DFS基础和DFS回溯

    发表回复