DFS算法及应用

DFS简介

搜索算法:穷举问题解空间所有情况

深度优先搜索:既暴力枚举,尽可能一条路走到底,走不了再回退

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

  • 就需要实现n重循环
  • n重循环=特定的树状结构=DFS搜索

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

f507f752e4c449279341020ce3c42632.png

DFS模板

def dfs(depth):
    
    if depth == n:
        # 递归的出口
        return

    # 每次循环做的枚举操作

例题:

分糖果

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

ans = 0  # 方案数


def dfs(depth, n, m):     # 每一层都有枚举
    # depth 第几个小朋友
    # n,m为剩余量
    if depth == 7:   # 不管还剩多少,dfs都结束了,如果剩余为零则说明ans加一
        if n == 0 and m == 0:
            global ans
            ans += 1
        return
    for i in range(0, 6):
        for j in range(0, 6):   # 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)

# 5067671

在写dfs函数时,要先写出递归的结束深度,以及在此处的判断,然后写出分糖果的一一枚举情况,最后写出下一层(下一个小朋友)的情况。

 买瓜

小蓝正在一个瓜摊上买瓜。瓜摊上共有n个瓜,每个瓜的重量为A;小蓝刀功了得,他可以把任何瓜劈成完全等重的两份,不过每个瓜只能劈一刀。小蓝希望买到的瓜的重量的和恰好为m。请问小蓝至少要劈多少个瓜才能买到重量恰好为m的瓜。如果无论怎样小蓝都无法得到总重恰好为m 的瓜,请输出—1。

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

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

def dfs(depth, weight, cnt):   # 写之前要明确参数,这是第几个瓜,目前买到的质量是多少,目前劈瓜几次
    # 第depth个瓜
    # 当前买到瓜的重量
    # 劈了多少次

    # 剪枝
    if weight > m:    # 剪枝的出口
        return
    if weight == m:
        global ans
        ans = min(ans, cnt)
    if depth == n:   # 正常的出口
        return

    # 不买
    dfs(depth + 1, weight, cnt)
    # 买
    dfs(depth + 1, weight + A[depth], cnt)
    # 卖一半
    dfs(depth + 1, weight + A[depth] // 2, cnt + 1)


n, m = map(int, input().split())   # 买n个瓜,希望买到的总质量为m
m *= 2
A = list(map(int, input().split()))
A = [x * 2 for x in A]
ans = n + 1  # 表示每个挂=瓜都劈一次
dfs(0, 0, 0)
if ans == n + 1:
    ans = -1
print(ans)

  • 为了避免除以二造成的精确度缺失,我们对小蓝所期望的总质量m和每个瓜的质量A乘以二,可以默认劈瓜的次数为n+1,如果递归搜索完成还没有得到m,就让ans等于-1。
  • 同理depth,weight,cnt分别代表了轮到了第几个瓜,当前搜索到的总质量,劈瓜的次数。
  • 我们先写出函数的结束,既depth == n ,然后到了枚举阶段,上个案例是每次分给小朋友的糖果数,这次是根据劈不劈瓜分成三种情况。最后还有剪枝的回退,写入前段部分。

3 10

1 3 13
2

 回溯

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

回溯强调走过的路要打标记,一搬在DFS基础上加一些剪枝策略。

回溯模板求排列

1f051396f1664db9886bdae4d5a89fcb.png

 

  • 排列要求数字不重复,每次选择的数字需要打标记,既vis数组。
  • 每次成功时打印path路径。
  • 打标记,记路径,然后下一层,回到上一层,清除标记。 
def dfs(depth):
    # 第几个数字
    if depth == n:   # 以前还需要判断,但是通过回溯,直接打印就行了
        print(path)
        return

    # 第depth个数字可以选择下边的数
    for i in range(1, n + 1):
        if vis[i]:
            continue
        vis[i] = True
        path.append(i)
        dfs(depth + 1)
        vis[i] = False    # 返回到上一个,即返回到最后一层,因为的depth+1是溢出的一层了
        path.pop(-1)


# 选择的数字必须未标记


n = int(input())
vis = [False] * (n + 1)  # 如果是3个数,就是:[][][][] 0这个数不在排列内(索引代表数字)
path = []
dfs(0)

VIS数组的索引代表这个数字,值代表这个数有没有被选取,每次通过for循环选择数字,如果该数之前没有,则将其标记并append到path路径中,最后在dfs下边写出回退时的操作(去除标记、弹出path)

回溯模板求子集

 573e1d08bbc040d78021f138a0b301db.png

  • 给定n个数字,求子集。
  • 可以针对每个数字,做出选择:选不选。
def dfs(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()))
path = []


dfs(0)

 崇拜圈

班里N个小朋友,每个人都有自己最崇拜的一个小朋友(也可以是自己)。在一个游戏中,需要小朋友坐一个圈,每个小朋友都有自己最崇拜的小朋友在他的右手边。求满足条件的圈最大多少人?

输入第一行,一个整数N(3<N<105) 。接下来一行N个整数,由空格分开。

da1c2ac5504d49b5a4ff5cedad5a3661.png

 

def dfs(x, length):
    # 记录走到x的步长
    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 = [0] + list(map(int, input().split()))
# 标记数组,vis[x]代表点X的步数
vis = [0] * (n + 1)
ans = 0
for i in range(1, n + 1):
    if vis[i] == 0:
        dfs(i, 1)
print(ans)

a列表代表的索引为i的小朋友崇拜的人是几号,同样用vis代表索引为i的小朋友走几步才到。

剪枝

  • 在搜索过程中,如果需要完全遍历所有情况可能需要很多时间。
  • 在搜索到某种状态时,根据当前状态判断出后续无解,则该状态无需继续深入搜索。
  • 例如:给定N个正整数,求出有多少个子集之和小于等于K。在搜索过程中当前选择的数字和已经超过K则不需要继续搜索。

数字排队 

 数字王国开学了,它们也和我们人类一样有开学前的军训,现在一共有n名学生,每个学生有自己的一个名字a(数字王国里的名字就是一个正整数,注意学生们可能出现重名的情况),此时叛逆教官来看了之后感觉十分别扭,决定将学生重新分队。排队规则为:将学生分成若干队,每队里面至少—个学生,且每队里面学生的名字不能出现倍数关系(注意名字相同也算是倍数关系)。现在请你帮忙算算最少可以分成几队?

例:有4名学生(2,3,4,4),最少可以分成(2,3)、(4)、(4)共3队。

输入格式:

第一行包含一个正整数n,表示学生数量。

第二行包含n个由空格隔开的整数,第i个整数表示第i个学生的名字a。

def check(x, group):
    for y in group:
        if x % y == 0 or y % x == 0:
            return False
    return True


def dfs(depth):
    # 最优性剪枝
    global ans
    if len(Groups) > ans:
        return
    if depth == n:
        global ans
        ans = min(ans, len(Groups))
        return

    # 对于每个学生,枚举该学生放在哪一组
    # 看看当前学生能否加到这一组
    for every_group in Groups:
        if check(a[depth], every_group):
            every_group.append(a[depth])
            dfs(depth + 1)
            every_group.pop()

    # 对于每个学生也可以单独作为一组
    Groups.append([a[depth]])
    dfs(depth + 1)
    Groups.pop()


n = int(input())
a = list(map(int, input().split()))
# Groups是二维数组,每个组的情况
Groups = []  # 构建一个初始的数组和初始的答案
ans = n
dfs(0)
print(ans)

 N边形

假设一个n边形n条边为a1,a2,a3,…- ,an,定义该n边形的值u=ax ag x a3x··× an。 

定义两个n边形不同是指至少有—条边的长度在一个n边形中有使用而另一个n边形没有用到,如n边形(3,4,5,6)和(3,5,4,6)是两个相同的n边形,(3,4,5,6)和(4,5,6,7)是两个不相同的n边形。现在有t和n,表示t个询问并且询问的是n边形,每个询问给定一个区间[l,7],问有多少个n边形(要求该n边形自己的n条边的长度互不相同)的值在该区间范围内。

输入格式:

第—行包含两个正整数t、n,表示有t个询问,询问的是n边形。

接下来t行,每行有两个空格隔开的正整数l、r,表示询问区间[l,r]。

# 利用DFS求所有的N边形
# N边形:最小的N-1条边之和大于第N边  等价于  N边之和 > 2 * 第N边
def dfs(depth, last_val, tot, mul):
    # depth表示第几条边
    # last_val是上一条边长
    # 当前所有边长之和
    if depth == n:
        if tot > 2 * path[-1]:  # 因为每条边递增
            ans[mul] += 1
        return

    # 枚举第depth条边的边长
    for i in range(last_val + 1, 100000):
        # 剪枝保证当前乘积不超过100000
        if mul * (i ** (n - depth)) <= 100000:
            path.append(i)
            dfs(depth + 1, i + 1, tot + i, mul * i)
            path.pop()
        else:
            break


ans = [] * 1000001
t, n = map(int, input().split())
path = []
dfs(0, 0, 0, 1)
# 每次询问一个区间[l,r],输出有多少个N边形的价值在区间中
# 等价于ans[l]+...+ans[r]
for i in range(100001):
    ans[i] += ans[i - 1]  # 求前缀和
for _ in range(t):
    l, r = map(int, input().split())
    print(ans[r] - ans[l-1])

 

 

 

  • 46
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小森( ﹡ˆoˆ﹡ )

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值