DFS搜索(python)

目录

一、DFS搜索基础

1、dfs简介

2、dfs和n重循环

数字拆分的例子

数字拆分例题变形

dfs和n重循环的模版

dfs和n重循环的典型例题

二、回溯法

1.回溯法简介

2、回溯模版——求排列

3、回溯模版——求组合

4、集合问题

5、二选一问题,二进制

6、N皇后问题

三、联通块问题

题目链接:小怂爱水洼

题目链接:全球变暖

四、迷宫染色问题


一、DFS搜索基础

1、dfs简介

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

深度优先搜索:①:本质上是暴力枚举

                         ②:尽可能一条路走到底,走不了再回退。(一条路走到底,走不了再回退)

2、dfs和n重循环

数字拆分的例子

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

        最简单的思路:三重循环暴力求解。

        当将数字拆分为n个数字时,显然使用n重循环是不合理的。

       n重循环 = 特定的数形结构 = dfs搜索

直接上代码

import sys
sys.setrecursionlimit(100000)
x, n = map(int, input().split())
li = []


def dfs(depth):
    # depth:表示拆分的第depth个数字
    if depth == n:
        if sum(li) == x:
            # 判断是否符合后面的大于等于前面的数字
            for i in range(n - 1):
                if li[i] > li[i + 1]:
                    return
                else:
                    continue
            # 答案
            print(li)
            return
        return

    for i in range(1, x + 1):
        # 加入元素
        li.append(i)
        dfs(depth + 1)
        # 恢复现场
        li.pop()


dfs(0)

"""
输入样例:7 3
输出样例:
[1, 1, 5]
[1, 2, 4]
[1, 3, 3]
[2, 2, 3]
"""

代码的优化:

x, n = map(int, input().split())

li = []


# 优化的过程多加了一个形参,保证当前选的数字大于等于前一个数字
def dfs(depth, val):
    if sum(li) > x:
        return
    if depth == n:
        if sum(li) == x:
            print(li)
            return

    for i in range(val, x + 1):
        li.append(i)
        dfs(depth + 1, i)
        li.pop()


# 拆分的数字是正整数所以val传入的数字是1而非0
dfs(0, 1)
数字拆分例题变形

题目链接:自然数的拆分问题

题目分析:

       ①、 由于数字的范围不大直接使用dfs即可

        ②、本题与上面例题的区别就在于没有规定拆分数字的个数, 并在输出中加入了等号。

        ③、直接看代码代码的注释比较详细

import locale

n = int(input())

# li:用来存储拆分的数字
li = []

def dfs(depth, val):
    # depth:表示拆分的第的普惠个数字,
    # val与上一个例题优化的代码相似,为了保证当下拆分的数字大于等于前面拆分的数字

    if depth == n:
        if sum(li) == n:
            print('+'.join(map(str, li)))
            return
        return
    # 在下面的循环中注意是不包含n个(有题目的样例输出可以知道)
    for i in range(val, n):
        # 选择第i个数字
        li.append(i)
        dfs(depth + 1, i)
        li.pop()
    # 不选择第i个数字
    dfs(n, val)


dfs(0, 1)
dfs和n重循环的模版

        由于dfs并没有通用的模版,因此只能给出相对模糊的模版概念

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

题目链接:分糖果

下面的代码运行的较慢,由于本题是一个填空题,只需要在本地跑出结果再提交即可。

li = []
ans = 0


def dfs(depth):
    # depth:表示第depth个小朋友
    if depth == 7:
        x = 0
        y = 0
        # 统计方案的第一种和第二种糖果的总数量,如果符合题目则ans += 1
        for x_d, y_d in li:
            x += x_d
            y += y_d
        if x == 9 and y == 16:
            global ans
            ans += 1
            return
        return
    # 枚举第一种糖果
    for i in range(6):
        for j in range(6):
            # 枚举第二种糖果
            if 2 <= i + j <= 5:
                li.append((i, j))
                dfs(depth + 1)
                li.pop()


dfs(0)
print(ans)

"""
运行的结果:5067671
"""

代码的优化:

代码进行了优化后比上面的运行速度快10倍不止。

ans = 0


def dfs(depth, n, m):
    # depth:表示第depth个小朋友
    # n:表示第一种糖果
    # m:表示第二种糖果
    if depth == 7:
        if n == 0 and m == 0:
            global ans
            ans += 1
        return

        # 枚举第一种糖果
    for i in range(6):
        # 枚举第二种糖果
        for j in range(6):
            # 根据题目:每个小朋友得到的糖果总数最少为2个最多为个
            if 2 <= i + j <= 5 and n - i >= 0 and m - j >= 0:
                dfs(depth + 1, n - i, m - j)


dfs(0, 9, 16)
print(ans)
"""
运行结果:5067671
"""

最后一题:

题目链接:买瓜

这一题类似为集合问题,直接看题目

本题给的代码并不能通过全部的样例,大概只能通过65%

直接看代码,通过代码来理解做题的思路

import sys
sys.setrecursionlimit(10000)
inpurt = sys.stdin.readline
def dfs(depth, weight, ant):
    # depth:表示第depth个瓜
    # weight:表示买的瓜的重量
    # ant:表示劈瓜的次数

    # 剪枝:当质量大于所要的质量m直接返回
    if weight > m:
        return
    global ans
    # 统计答案最少劈的次数
    if weight == m:
        ans = min(ans, ant)
    if ans < ant:
        return
    # 递归出口
    if depth == n:
        return
    # 不买
    dfs(depth + 1, weight + 0, ant)
    # 买一半
    dfs(depth + 1, weight + a[depth] // 2, ant + 1)
    #  买一个
    dfs(depth + 1, weight + a[depth], ant)


n, m = map(int, input().split())
# 在这里使用特殊的技巧,让质量乘2,避免质量为奇数时除以2,出现精度误差
m *= 2
a = list(map(int, input().split()))
a = [x * 2 for x in a]
# ans:初始化表示每个瓜都劈一刀
ans = n + 1
dfs(0, 0, 0)
if ans == n + 1:
    ans = -1
print(ans)

二、回溯法

1.回溯法简介

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

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

        回溯法是一般dfs的好基础上加上一些剪枝策略。

2、回溯模版——求排列

        排列要求数字不重复——每次选择的数字需要打标记——vis数组

        要输出当排列——记录路径——path数组。

        回溯:先打标记,记录路径、然后下一层,回到上一层,清除标记

代码模版:

def dfs(depth):
    # depth:第depth个数字

    if depth == n:
        print(li)
        return

    for i in range(1, n + 1):
        if vis[i] == 0:
            li.append(i)
            vis[i] = 1
            dfs(depth + 1)
            vis[i] = 0
            li.pop()


n = int(input())
# vis:数组
vis = [0] * (n + 1)
li = []
dfs(0)

"""
输入样例:3
输出样例:
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
"""

根据上面的模版来道例题

题目链接:全排列问题

上面这个全排列问题就是全排列数的模版,通过例题课题看出全排列数的用处

n = int(input())
lis = []
vis = [0] * (n + 1)
li = [0] * n


def dfs(depth):
    if depth == n:
        for i in li:
            print('{:>5}'.format(i), end='')
        print()
        return

    for i in range(1, n + 1):
        if vis[i] == 0:
            vis[i] = 1
            li[depth] = i
            dfs(depth + 1)
            vis[i] = 0

dfs(0)

3、回溯模版——求组合

排列和组合的不同之处:组合与顺序无关,排列和顺序有关

组合的代码模版:

# 在n个数中选出k个数
n, k = map(int, input().split())
li = []
vis = [0] * (n + 1)


def dfs(depth, val):
    # depth:表示第depth个数字
    # val:为了避免顺序而考虑的形参

    if depth == k:
        print(li)
        return
    #  上面的代码中遇到过多次相似的情况,以下就不写注释了。
    for i in range(val, n + 1):
        if vis[i] == 0:
            vis[i] = 1
            li.append(i)
            dfs(depth + 1, i + 1)
            vis[i] = 0
            li.pop()


dfs(0, 1)

来个例题熟悉一下组合模版

例题链接:组合的输出

这个题就是组合的版子题,只是在熟悉一下组合的模版

# 在n个数中选出k个数
n, k = map(int, input().split())
li = []
vis = [0] * (n + 1)


def dfs(depth, val):
    # depth:表示第depth个数字
    # val:为了避免顺序而考虑的形参

    if depth == k:
        for i in li:
            print('{:>3}'.format(i), end='')
        print()
        return
    #  上面的代码中遇到过多次相似的情况,以下就不写注释了。
    for i in range(val, n + 1):
        if vis[i] == 0:
            vis[i] = 1
            li.append(i)
            dfs(depth + 1, i + 1)
            vis[i] = 0
            li.pop()


dfs(0, 1)

4、集合问题

子集的问题相对于组合和排列用的更多,子集的模版特别简单直接看代码

n = int(input())
li = list(map(int, input().split()))
# lis:用来存储答案
lis = []
def dfs(depth):
    # depth:表示第depth个数
    if depth == n:
        print(lis)
        return

    # 选第depth个
    lis.append(li[depth])
    dfs(depth + 1)
    lis.pop()

    # 不选depth个
    dfs(depth + 1)
dfs(0)
"""
输入样例:
3
1 2 3
输出样例:
[1, 2, 3]
[1, 2]
[1, 3]
[1]
[2, 3]
[2]
[3]
[]
"""

我们来看一个有关子集的题目

例题链接:串变换

import sys

sys.setrecursionlimit(5000)


def check(il):
    S = s.copy()
    for j in il:
        if op[j][0] == 1:
            x = op[j][1]
            v = op[j][2]
            S[x] = (S[x] + v) % 10
        else:
            x = op[j][1]
            y = op[j][2]
            S[x], S[y] = S[y], S[x]
    if S == t:
        print('Yes')
        sys.exit()


def dfs(depth):
    # depth:表示的depth个元素

    if depth == k:
        check(lis)
        return

    for i in range(k):
        if vis[i] == 0:
            lis.append(i)
            vis[i] = 1
            dfs(depth + 1)
            vis[i] = 0
            lis.pop()
    dfs(k)


n = int(input())
s = list(map(int, list(input())))
t = list(map(int, list(input())))
k = int(input())
op = []
for i in range(k):
    op.append(list(map(int, input().split())))
vis = [0] * (k + 1)
lis = []

dfs(0)
print('No')

5、二选一问题,二进制

模版

n = int(input())

vis = [0] * n


def dfs(depth):
    # depth:第depth个数
    if depth == n:
        print(vis)
        return
    # 第depth个选0
    dfs(depth + 1)
    # 第depth个选1
    vis[depth] = 1
    dfs(depth + 1)
    vis[depth] = 0


dfs(0)

"""
输入样例:3
输出样例:
[0, 0, 0]
[0, 0, 1]
[0, 1, 0]
[0, 1, 1]
[1, 0, 0]
[1, 0, 1]
[1, 1, 0]
[1, 1, 1]
"""

上一个例题来例题来理解这个算法怎使用

例题链接:笨笨的机器人

        这题的官方题解好像使用的位运算来做的,当然我们完全可以使用上面的模版进行解题。这个代码相对于来说比较好理解,我就没有加注释,读者可自行阅读。

n = int(input())
a = list(map(int, input().split()))
ant = 0
m = 0


def check(li):
    ans = 0
    for i in range(n):
        # 当为1是往左走,0时则相反。
        if li[i] == 1:
            ans = (ans - a[i]) % 7
        else:
            ans = (ans + a[i]) % 7

    if ans == 0:
        # ant:表示所以符合题目要求的方案数。即可以回到原点
        global ant
        ant += 1


vis = [0] * n


def dfs(depth):
    if depth == n:
        # m为所有可能出现的方案数
        global m
        m += 1
        check(vis)
        return

    vis[depth] = 0
    dfs(depth + 1)
    vis[depth] = 1
    dfs(depth + 1)


dfs(0)
print('{:.4f}'.format(ant / m + 0.0000001))

接下来来一个特别经典的题目。

6、N皇后问题

        通过上图可以,发现i和j在主对角线和副对角线的规律。因此可以构造vis1标记数组来表示主对角线,vis2表示副对角线的标记数组。

        不太好理解可以看一下面这个视频N皇后问题

N = int(input())


def dfs(x):
    # x表示第x层皇后,也可是说是第x行的皇后
    # 如过能够走到最后一层则答案加1。
    if x == N:
        global ans
        ans += 1
        return
    # 第x层的皇后枚举每一列
    for y in range(N):
        # 当前的坐标为(x, y),要求当前列,当前主对角线,副对角线不能被攻击到
        if vis[y] == 0 and vis1[x - y + N] == 0 and vis2[x + y] == 0:
            # 标记为1
            vis[y] = 1
            vis1[x - y + N] = 1
            vis2[x + y] = 1
            dfs(x + 1)
            # 恢复现场
            vis[y] = 0
            vis1[x - y + N] = 0
            vis2[x + y] = 0


# 三个标记数组分别表示列,主对角线,副对角线。
vis = [0] * N
vis1 = [0] * (2 * N)
vis2 = [0] * (2 * N)
# ans:用来存储答案。
ans = 0

dfs(0)
print(ans)

三、联通块问题

联通快问题是比较常见的问题,直接通过题目来体会这一类的问题

题目链接:小怂爱水洼

上图这题算是最为基础的联通块问题,重点就在于打标记。

import sys

sys.setrecursionlimit(100000)
# 联通块问题
n, m = map(int, input().split())
Map = [list(map(int, input().split())) for _ in range(n)]

vis = [[0] * m for _ in range(n)]


def dfs(x, y):
    # 打标记,避免被重复遍历
    vis[x][y] = 1
    global ans
    ans += Map[x][y]
    # 扩展:把联通的一个块全部遍历,进行标记
    for x_d, y_d in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
        xx = x_d + x
        yy = y_d + y
        # 特判:是否过界,是否被标记,是否在符合题目条件
        if 0 <= xx < n and 0 <= yy < m and vis[xx][yy] == 0 and Map[xx][yy] != 0:
            dfs(xx, yy)


ant = 0
for i in range(n):
    for j in range(m):
        # 如果没有被标记, 并符合题目中的条件
        if vis[i][j] == 0 and Map[i][j] != 0:
            ans = 0
            dfs(i, j)
            # 找出最大值
            ant = max(ant, ans)
print(ant)

在来一题

题目链接:全球变暖

import sys

# 一定要设置递归深度
sys.setrecursionlimit(100000)
input = sys.stdin.readline


# dfs的目的是判断是否为高地,以及打标记
def dfs(x, y):
    vis[x][y] = 1
    # 题目中描述,岛屿的边缘全是会被淹没的,所以地图的边缘全是。
    # 不用去考虑越界情况

    # 如果Map上下左右全部为# 则说明此处为高地
    if Map[x - 1][y] == '#' and Map[x][y - 1] == '#' and Map[x + 1][y] == '#' and Map[x][y + 1] == '#':
        global ok
        ok = 1

    # 扩展:
    for d_x, d_y in [(1, 0), (0, 1), (0, -1), (-1, 0)]:
        xx = d_x + x
        yy = d_y + y
        if Map[xx][yy] == '#' and vis[xx][yy] == 0:
            dfs(xx, yy)


N = int(input().strip())
Map = [list(input().strip()) for _ in range(N)]
vis = [[0] * N for _ in range(N)]
ans = 0
for i in range(N):
    for j in range(N):
        if vis[i][j] == 0 and Map[i][j] == '#':
            # ok = 0表示不是高地
            # ok = 1表示是高地
            ok = 0
            dfs(i, j)
            if ok == 0:
                ans += 1
print(ans)

四、迷宫染色问题

题目链接:混镜之地2

import sys

input = sys.stdin.readline
sys.setrecursionlimit(1000000)

def dfs(x, y, color, vis):
    vis[x][y] = color

    for d_x, d_y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
        xx = d_x + x
        yy = d_y + y
        if 0 <= xx < n and 0 <= yy < m and Map[xx][yy] == '.' and vis[xx][yy] == 0:
            dfs(xx, yy, color, vis)


def check(x, y):
    ans = 0

    for x_d, y_d in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
        xx = x_d + x
        yy = y_d + y
        if 0 <= xx < n and 0 <= yy < m:
            ans |= vis1[xx][yy]
    if ans == 3:
        print('Yes')
        sys.exit()
    else:
        return 0



n, m = map(int, input().split())
A, B, C, D = map(int, input().split())
A -= 1
B -= 1
C -= 1
D -= 1
Map = [list(input().strip()) for _ in range(n)]
# vis1:起点标记数组
vis1 = [[0] * m for _ in range(n)]

dfs(A, B, 1, vis1)
if vis1[C][D] == 1:
    print('Yes')
    sys.exit()
dfs(C, D, 2, vis1)

for i in range(n):
    for j in range(m):
        if Map[i][j] == '#':
            check(i, j)
print('No')

  • 13
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值