python实现:深度优先搜索(DFS)的原理和应用

深度优先搜索(DFS)的原理和应用

深度优先搜索是图论里面的知识,我们今天来一起讨论一下什么是深度优先搜索。

图论基础

图,分为有向图和无向图。同时,有向图中,有分强连通图和不连通图。无向图中分连通图和非连通图。

什么是连通图呢?

就是说,图中的每个顶点都可以有路径到达图中的除它以外的任何顶点,这个就是连通图。

拿有向图来讲,由于每一条边都是有方向的,所以只要满足连通图的定义,它就是强连通图。同时,如果每个顶点都有出度和入度,那么这个有向图就是完全图。

所以对于无向图来说,如果无向图是连通的,我们却不能说它是完全的。因为,完全图是指图的任意两个顶点之间有边连接,也就是说,就算两个顶点之间连通,我们也不能说无向图就一定是完全图。

这里我们补充三个概念:

  1. 入度:是指指向该顶点的入边
  2. 出度:是指从该节点指出的出边
  3. 度:是指出边和入边的总和

上面三个概念是对有向图来讲的,对于无向图来讲,我们不讨论它的度,因为它的出边和入边都是一条边。

剪枝思想

在遍历的过程中,肯定会有很多是没有必要再去遍历的,如果一昧的穷举,会导致程序的时间复杂度越来越大。所以我们应该将这些不必要的情况全部排出。这种思想就叫做剪枝思想。

深度优先搜索

深度优先搜索中,我们用到的是无向图。

深度优先搜索的介绍

首先,我们要知道什么是深度优先搜索。

现在我们来看一个例子:
在这里插入图片描述

例子开始前,我们假设这个图都是简单路径,不会有循环的情况。(简单路径,就是除了起始顶点和终止顶点之外,访问的路径上的每个顶点都是唯一的,不会有重复的情况)

我们顺时针的规定,从最上面的顶点开始,为1,然后一次是2,3,4,5;并且,我们规定每个顶点有三个颜色,白色为没有访问过,灰色为访问过但是访问可能不会只有一次,黑色代表访问结束。现在小明想从1出发去寻找在4号位的小张。我们能找出多少条路径呢?这个我们就可以用到深度优先搜索,首先,我们任意选择一个颜色是白色的顶点,然后将该顶点标记为灰色,接下来,我们继续任意选择这个灰色顶点的子顶点,将子顶点标记为灰色后继续遍历,直到找到了目的地。这个时候,我们将目的地标灰然后返回到上一个顶点,去寻找下一个可行的路径,如果上一个顶点确实没有另一个可行的路径的时候,我们会将该祖先顶点标黑,并返回这个祖先顶点的祖先,重复之前的操作。

我们发现,在遍历的时候,我们会一条一条的路径生成。

这个就是深度优先,通俗点来讲,就是程序不撞南墙不回头,撞了南墙才会换一个路径去尝试。

应用:

蓝桥杯之棍子的切割问题(Sticks)
问题描述

问题描述

George took sticks of the same length and cut them randomly until all parts became at most 50 units long. Now he wants to return sticks to the original state, but he forgot how many sticks he had originally and how long they were originally. Please help him and design a program which computes the smallest possible original length of those sticks. All lengths expressed in units are integers greater than zero.

输入格式

The input contains blocks of 2 lines. The first line contains the number of sticks parts after cutting, there are at most 64 sticks. The second line contains the lengths of those parts separated by the space. The last line of the file contains zero.

输出格式

The output should contains the smallest possible length of original sticks, one per line.

中文意思就是说:George这个人非常的无聊,喜欢把等长的棍子切割,而且切割成任意长度,切割的棍子数量总和不超过64根,且每根切割后的棍子的最大长度不会超过50,而且长度一定是不小于零的正整数。但是他的记性还很差,切了就忘了原来有几根,长度是多少。

现在问题来了,我们如何制作出一个算法,通过他切割后的棍子的总数量,以及每根棍子的长度来计算出原来每根棍子的最小长度。

这个就是典型的深度优先搜索的应用了:

​ 思路就是,我们先将子棍子排序,按降序排列。为什么要排序呢?这个是因为我们如果从最大的开始搜索,那么可以避免很多没有必要的搜索,减少时间复杂度。这里我说一下我的理解:由于木棒的长度已经从大到小的排列好了,从大的开始,递归的话可以在一开始的时候就迅速的发现不符合条件的路径,从而直接跳过;如果棍子试从小的开始匹配,那么每次我们可能都要枚举相同的子长度然后才能够在后面发现这个长度到底符不符合规范,这样的时间复杂度就很大了。

然后就是关于每根棍子的长度问题。由于长度一定是正整数,并且,长度必须比切片的最大长度还要大或者刚好相等(应为可能存在有一根恰好不切割的情况。),所以,我们可以将子棍子的长度全部相加,然后,除数从最大的子长度开始,由于每根棍子的长度一定是正整数,所以,余数不为零的除数直接舍去,然后除数加一,继续判断,如果余数为零,那么就开始递归,判断这个除数是否符合条件,能作为棍子的长度。

还有一种情况就是如果有一根棍子是不符合条件的话,那么和它等长的棍子也一定不符合条件,我们直接跳过对这类棍子的讨论。

这里我们在编程的时候还需要注意一点,就是说,我们会定义子棍子类,这个类包含两个参数,一个是它的长度(length),还有一个就是它是否被访问过(is_visited)。其中,我们规定,is_visited如果是0,那么就是没有被访问过;如果是1,就是被访问过。由于递归会自动回溯,所以,我们取消了第三种颜色,不需要用2来标识这个元素是否还能继续被访问。

当代码中,tag被置为1后,说明我们已经找到了一条符合规范的路径,所以我们也就不用再继续往下搜索了。直接返回。

代码
class Vex:

    def __init__(self,length,is_visited):
        """
        :param length:
        :param is_visited:
        """
        self.length = length
        self.is_visited = is_visited


def dfs(getStick,nowLen,nowStick) -> None:
    """
    :param getStick: 已经处理的小棒数
    :param nowLen: 当前的长度
    :param nowStick: 当前正在处理的小棒下标
    :param tag: 全局变量,用来申明是否找到了最小值。
    :return: 如果是1的话,说明函数结束,同时tag也应为1。
    """
    global tag  # 全局变量的申明
    if nowStick > lenNum: return
    if tag: return
    if nowLen > beginNum: return # 如果当前的长度已经超过了我们要匹配的长度的话,我们也直接结束。
    # 父顶点的选择
    if nowLen == 0: # 当前长度为0的时候,我们进入循环,看一下是否64根小棒全部用完了。
        for eachStick in chilStick:
            if tag == 1:
                return
            if eachStick.is_visited == 0:
                if sum(num_list[nowStick:]) < beginNum:
                    return
                eachStick.is_visited = 1
                dfs(getStick+1,eachStick.length,nowStick+1)
                eachStick.is_visited = 0
                return
    # 如果当前长度和我们要匹配的长度相等,那么我们进入下一步的判断,看循环是否能结束
    if nowLen == beginNum:
        # 如果在当前长度和我们要的长度相等的情况下,所用的小棒的数量和我们输入的数量是相等的情况下,就说明是正确的。
        # 第一次成功的一定是最小的单位,因为我们这个是深度优先搜索。剩下的也不用循环了,所以直接返回
        if getStick == lenNum:
            tag = 1
            return
        else:
            tag = 0
            # 这里是说,匹配到了之后,由于棍子没有用完,所以要继续,同时由于棍子是被标记过的,所以不用担心重复匹配的问题。
            dfs(getStick,0,0)
            # 如果没有匹配到,就结束函数。
            return
    # 开始深搜子顶点
    for i in range(nowStick,lenNum):
        if chilStick[i].is_visited == 0 and nowLen+chilStick[i].length <= beginNum:
            # 如果tag被设置成了1,那么说明一条路径已经完成,也就是最小长度已经找到了,这样就没必要继续往下找了

            if tag == 1:
                return
            chilStick[i].is_visited = 1
            # 从这边开始继续深搜,一旦深搜成功,那么直接结束,如果不成功,那么将顶点的访问取消,继续下一次的深搜。
            dfs(getStick+1,nowLen+chilStick[i].length,i+1)
            chilStick[i].is_visited = 0

            # 这句代码的意思是,如果传入这里的时候,第一根被废弃了,那么总数肯定是达不到的,这种情况要直接剪掉。
            if nowLen == 0: return

            # 这个是个过滤,当上一根小棒被访问过,同时,后面的小棒又和上一根相同的时候,说明都不能匹配,直接就跳过循环。
            # 因为,相同长度的小棒,加了一次之后已经不满足了,那么后面的也就不用加了。
            while i+1 < lenNum and chilStick[i].length == chilStick[i+1].length:
                i += 1
                continue
    return
if __name__ == '__main__':
    tag = 0
    lenNum = eval(input())
    stickList = input().split()
    num_list = [int(i) for i in stickList]
    num_list.sort()
    num_list.reverse()

    # 初始化顶点
    chilStick = [None]*len(num_list)
    count = 0
    for i in num_list:
        chilStick[count] = Vex(i,0)
        count += 1
    beginNum = chilStick[0].length
    sumStick = sum(num_list)

    # 生成长度
    while beginNum <= sumStick:
        if sumStick % beginNum != 0:
            beginNum += 1
            continue

        else:
            g = int(sumStick / beginNum)
            dfs(0,0,0)
            if tag == 1:
                break
            beginNum += 1
    print(beginNum)
代码中存在的一个问题

我的代码是借鉴别的大佬的思路改过来的,但是有一个数据会存在一点点超时:

64
24 9 24 38 40 34 8 29 8 15 22 30 1 10 19 39 26 39 25 34 8 32 44 9 21 50 18 40 20 14 23 45 24 48 32 13 33 41 43 42 8 16 22 8 25 42 48 2 32 22 38 41 5 31 49 26 32 17 17 3 32 40 47 5

就是这个。目前,我猜想是由于剪枝的不够充分,当然,剪枝的标准我还在研究,争取让python写的这个不超时。

参考博客
  1. https://www.cnblogs.com/wwj321/p/12326398.html?from=singlemessage
  2. https://blog.csdn.net/sj2050/article/details/80645121
  3. https://blog.csdn.net/w745241408/article/details/7517418
  4. https://blog.csdn.net/Raymond_YP/article/details/104450936
    tml?from=singlemessage
  5. https://blog.csdn.net/sj2050/article/details/80645121
  6. https://blog.csdn.net/w745241408/article/details/7517418
  7. https://blog.csdn.net/Raymond_YP/article/details/104450936
  8. https://blog.csdn.net/yi_qing_z/article/details/88084875
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值