Python - 深夜数据结构与算法之 Divide Conquer Backtrack

news2024/11/17 11:28:26

目录

一.引言

二.分治与回溯简介

1.Divide & Conquer 分治

2.BackTrack 回溯

三.经典算法实战

1.Combination-Of-Phone [17]

2.Permutations [46]

3.Permutations-2 [47]

4.Pow-X [50]

5.N-Queen [51]

6.Combinations [78]

7.Sub-Sets [78]

8.Majority-Element [169]

四.总结


一.引言

分治与回溯本质上也是递归的一种,其相对传统递归稍微复杂一些,涉及到最后一步状态的恢复,下面我们学习下二者的特性与题目。

二.分治与回溯简介

1.Divide & Conquer 分治

分治的思路整体和递归是一样的,我们需要先将 Problem 转化为子问题 Sub-Problem,然后针对每个 Sub-Problem 进行解决,最后将多个 Sub-Solution 合并得到最终结果,下面的代码模版就是按照上面的思路来实现。

2.BackTrack 回溯

基于 base 情况,不断向前试探,试探成功找到结果,试探失败回撤,并且恢复上一步的状态。 

三.经典算法实战

1.Combination-Of-Phone [17]

电话号码组合: https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/

◆ 题目分析

分别获取数字及其对应的字符,逐层遍历即可。

◆ 回溯实现

class Solution(object):
    def letterCombinations(self, digits):
        """
        :type digits: str
        :rtype: List[str]
        """
        if not digits:
            return []
        
        phone_map = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }

        combination = []
        res = []

        def backtrack(position):
            if position == len(digits):
                res.append("".join(combination))
                return 
            
            # 遍历当前数字的多个字母
            digit = digits[position]
            for letter in phone_map[digit]:
                combination.append(letter)
                backtrack(position + 1)
                combination.pop()
        
        backtrack(0)
        return res

人肉递归的方式可以参考这个图理解。 

2.Permutations [46]

全排列: https://leetcode-cn.com/problems/permutations/

◆ 题目分析

按照回溯思路实现,从 0 到 len(nums) 固定每个位置,将该元素与其后方元素依次调换位置,直至最后一个元素即可。

◆ 回溯实现

class Solution(object):
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """

        res = []

        def backtrack(position, end):
            if position == end:
                res.append(nums[:])
                return
            
            for i in range(position, end):
                nums[i], nums[position] = nums[position], nums[i]
                backtrack(position + 1, end)
                nums[i], nums[position] = nums[position], nums[i]
            
        backtrack(0, len(nums))
        return res

第一次循环遍历位置 0,因为 replace 的原因,所以位置 0 上每个元素都会出现一次,在该基础上,固定第一个位置,分别将剩余元素分别替换至位置 1,以此类推。可以理解为第一次循环把位置 0 的所有可能遍历一遍, [0] [1] [2] 这样,第二次基于前面的基础 [0, 1] [0,2]、[1,0] [1, 2] 这样,... 以此类推。 

3.Permutations-2 [47]

全排列2: https://leetcode.cn/problems/permutations-ii/

◆ 题目分析

按照回溯思路实现,从 0 到 len(nums) 固定每个位置,将该元素与其后方元素依次调换位置,直至最后一个元素即可。和上面方法一致。

◆ 回溯实现

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """

        res = []

        # 回溯起始位置
        def backtrack(position, end):
            if position == end:
                res.append(nums[:])
                return
            
            for i in range(position, end):
                # position 位置的 N 种可能
                nums[position],nums[i] = nums[i], nums[position]
                # 固定 position 位置,在此基础上固定 position + 1 的位置
                backtrack(position + 1, end)
                # 回复原始状态供后面 position 从初始状态遍历
                nums[position],nums[i] = nums[i], nums[position]
            
        backtrack(0, len(nums))

        res = list(set(tuple(sub) for sub in res))
        res = [list(sub) for sub in res]

        return res

在上一题的基础上进行去重,set 支持 tuple 不支持 list 去重,所以需要转换,时空复杂度都比较高。

◆ 去重优化

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """

        res = []

        # 回溯起始位置
        def backtrack(position, end):
            if position == end:
                res.append(nums[:])
                return
            
            repeat = set()

            for i in range(position, end):
                if nums[i] in repeat:
                    continue
                repeat.add(nums[i])

                # position 位置的 N 种可能
                nums[position],nums[i] = nums[i], nums[position]
                # 固定 position 位置,在此基础上固定 position + 1 的位置
                backtrack(position + 1, end)
                # 回复原始状态供后面 position 从初始状态遍历
                nums[position],nums[i] = nums[i], nums[position]
            
        backtrack(0, len(nums))

        return res

最后全局去重的时间、空间复杂度都很高,我们修改为递归内判断,在 for 循环之前增加 set,如果 position-end 区间有相同元素则直接 continue 跳过即可。

4.Pow-X [50]

求 x 的 n 次方: https://leetcode.cn/problems/powx-n/description/

◆ 题目分析

Problem =  2^10,sub-problem = 2^5,我们处理的话就是 2^ (n/2)

Problem = 2^5,sub-problem = 2^2,我们处理的话还是 2^ (n/2),但是遗漏一个 2

所以我们还需要区分 n/2 是否整除,整除 x^n = x^(n/2) * x^(n/2) 不整除则再多乘一个 2。

◆ 递归实现

class Solution(object):
    def myPow(self, x, n):
        """
        :type x: float
        :type n: int
        :rtype: float
        """
        # x^0 == 1
        if n == 0:
            return 1.0
        if n == 1:
            return x
        if n == -1:
            return 1 / x
        
        half = self.myPow(x, int(n / 2))
        rest = self.myPow(x, n % 2)
        return half * half * rest

 half 负责将 2^n 减半,rest 负责检查是否需要补充一个 2。

5.N-Queen [51]

N 皇后: https://leetcode.cn/problems/n-queens/description/

◆ 题目分析

给定 n x n 的棋盘放置皇后,要求其上下左右和对角线都不可以放置其他皇后,观察棋盘坐标,我们可以发现是否同一行同一列比较简单,row / col 相等即可,对于左右 45° 的对角线,我们可以通过 row col 组合获取,这里我们称为撇 pie 和捺 na,pie 上的元素 row + col 都相同,na 上的元素 row - col 都相同,这样通过 row、col 我们即可判断所有可行的情况,剩下递归即可。 

◆ 回溯实现 

class Solution(object):
    def solveNQueens(self, n):
        """
        :type n: int
        :rtype: List[List[str]]
        """

        results = []
        # 行 左 右 是否可以放置
        cols = set()
        pie = set()
        na = set()

        def dfs(n, row, cur):
            if row >= n:
                results.append(cur)
            
            for col in range(n):
                if col in cols or (row + col) in pie or (row - col) in na:
                    continue
                
                # 判断有效
                cols.add(col)
                pie.add(row + col)
                na.add(row - col)

                dfs(n, row + 1, cur + [col])

                # 恢复状态
                cols.remove(col)
                pie.remove(row + col)
                na.remove(row - col)
        
        dfs(n, 0, [])
        return self.genResult(n, results)

    def genResult(self, n, results):
        return [[ '.' * i + 'Q' + (n - i - 1) * '.' for i in result] for result in results]

    def genResultV2(self, n, results):
        re = []
        for result in results:
            re.append([ '.' * i + 'Q' + (n - i - 1) * '.' for i in result])
        return re

这里最后获取 results 后还需要给出棋盘的形态,所以我们需要根据索引构建 '.' 和 'Q' 的关系。

6.Combinations [78]

组合: https://leetcode.cn/problems/combinations/description/

◆ 题目分析

固定第一个数字,向后遍历其他结果,待数量达到 k 停止,再回溯,固定下一个数字,向后寻找结果,直到 n-k 时再循环一次结束。

◆ 回溯实现

class Solution(object):
    def combine(self, n, k):
        res = []
        self.get_combine(res, [], n, k, 1)
        return res
 
    def get_combine(self, res, prefix, n, k, start):
        if k == 0:
            # K 个结果找到了
            res.append(list(prefix))
        elif start <= n:
            # 添加当前结果
            prefix.append(start)
            # 添加完 start , 还需要 k-1 个, start + 1 去重
            self.get_combine(res, prefix,
                             n, k - 1, start + 1)
            # 恢复状态,还需要 k 个,从 start + 1 开始
            prefix.pop()
            self.get_combine(res, prefix,
                             n, k, start + 1)

其运行过程可以参考下图,固定 1 之后,start_index 一直向后查找添加 [1, 2]、[1, 3]、[1, 4] 后,start_index 为 5,[1] 结束,pop 得到 [],start_index + 1,再固定 2 从 2 开始 ... 

◆ 树形结合

class Solution(object):
    def combine(self, n, k):
        res = []
        com = []

        def backtracking(n, k, start_index):
            if len(com) == k:
                res.append(com[:])
                return 

            # 因为全排列不包含 0,所以最后 + 1
            for num in range(start_index, n + 1):
                com.append(num)
                backtracking(n, k, num + 1) # 递归
                com.pop() # 回溯
        
        backtracking(n, k, 1)
        return res

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。

图中可以发现 n 相当于树的宽度,k 相当于树的深度,代码遵照下述思路完成。

◆ 剪枝优化

class Solution(object):
    def combine(self, n, k):
        res = []
        com = []

        def backtracking(n, k, start_index):
            if len(com) == k:
                res.append(com[:])
                return 

            last_index = n - (k - len(com)) + 1

            # 因为全排列不包含 0,所以最后 + 1
            for num in range(start_index, last_index + 1):
                com.append(num)
                backtracking(n, k, num + 1) # 递归
                com.pop() # 回溯
        
        backtracking(n, k, 1)
        return res

本题还可以通过剪枝进行优化,对于遍历而言,当 n=4、k=2 时,我们就没有必要再从 4 开始遍历了,因为后面已经不足以拼到 2 个数字了,所以我们优化一下循环的次数 n - (k - len(com)) + 1。

7.Sub-Sets [78]

子集: https://leetcode.cn/problems/subsets/description/ 

◆ 题目分析

遍历多种情况, 假设 [1, 2, 3],我们可以先遍历 [1] 生成所有情况,再遍历 [2] 和之前的情况结合并添加,随后继续,每次结果都会翻倍,因为新的数字会和之前的每个结果生成一个新的结果并添加。

◆ 循环实现

class Solution(object):
    def subsets(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = [[]]
        # 遍历每个数字
        for i in nums:
            res = res + [[i] + re for re in res]
        return res

8.Majority-Element [169]

多数元素: https://leetcode-cn.com/problems/majority-element/description/

◆ 题目分析

第一感觉是 wordcount 直接判断即可,但是既然出在回溯和分治的章节,说明其还有其他方法,我们两种方法尝试下。

◆ 字典计数

class Solution(object):
    def majorityElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        limit = len(nums) / 2
        count = {}

        for i in nums:
            if i not in count:
                count[i] = 0

            # 判断是否超过 n/2
            if count[i] + 1 > limit:
                return i
            else:
                count[i] += 1
        
        return 0

 计数判断即可。

◆ 分治实现

class Solution:
    def majorityElement(self, nums):
        def backtrack(lo, hi):
            # base case; the only element in an array of size 1 is the majority
            # element.
            if lo == hi:
                return nums[lo]

            # recurse on left and right halves of this slice.
            mid = (hi - lo) // 2 + lo
            left = backtrack(lo, mid)
            right = backtrack(mid + 1, hi)

            # if the two halves agree on the majority element, return it.
            if left == right:
                return left

            # otherwise, count each element and return the "winner".
            left_count = sum(1 for i in range(lo, hi + 1) if nums[i] == left)
            right_count = sum(1 for i in range(lo, hi + 1) if nums[i] == right)

            return left if left_count > right_count else right

        return backtrack(0, len(nums) - 1)

如果数 a 是数组 nums 的众数,如果我们将 nums 分成两部分,那么 a 必定是至少一部分的众数,所以题目将数组不断拆分,并获取两个部分的众数,这里不是太推荐使用分治法,因为这个场景复杂度太高。

四.总结

本文介绍了回溯和分治的思想和算法题目,观察上面的算法题目,我们可以发现其在代码上都遵循了简介中的模版而且写起来很相似,所以还是要多花时间去体会题目的要求的实现的方法,多巩固多练习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1338842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

模式识别与机器学习-无监督学习-聚类

无监督学习-聚类 监督学习&无监督学习K-meansK-means聚类的优点&#xff1a;K-means的局限性&#xff1a;解决方案&#xff1a; 高斯混合模型&#xff08;Gaussian Mixture Models&#xff0c;GMM&#xff09;多维高斯分布的概率密度函数&#xff1a;高斯混合模型&#xff…

DBAPI个人版如何升级到企业版

安装好企业版软件&#xff0c;并启动 注意要新建mysql数据库&#xff0c;执行新版本的ddl_mysql.sql脚本 在旧版本系统中分别导出数据源、分组、API&#xff0c;得到3个json文件 注意全选所有的数据导出 在新版本系统中导入数据源 在新版本系统中导入分组 进入分组管理菜单&…

FART12刷机脱壳记录笔记

其他脱壳笔记&#xff1a; https://codeooo.blog.csdn.net/article/details/126891503 fart12 脱壳系统 可以脱邦邦 爱加密 企业壳 等&#xff1b; 寒冰大佬的脱壳王 本文记录下刷机过程&#xff0c;方便以后查看使用。 adb授权和ome锁要开着 代表锁是开着状态 如果出现&a…

鸿蒙原生应用/元服务开发-Serverless账户验证码的问题

在应用/元服务早期使用过程中&#xff0c;-Serverless账户验证码的格式是[AGC][应用/元服务名称]&#xff0c;如下图。 但是&#xff0c;在最近&#xff0c;[应用/元服务]名称直接变成了【default】,用户收到这种验证码后&#xff0c;心里存有疑虑的&#xff0c;这是哪里配置…

实习知识整理11:确认订单并将订单的相关信息插入用户订单表和订单详情表

用户订单表&#xff1a; 订单详情表&#xff1a; 思路分析&#xff1a;首先我们需要知道当点击了确认订单按钮后&#xff0c;需要向后端传递哪些数据&#xff0c;先看用户订单表&#xff1a;ORDER_ID是不需要传的&#xff0c;这个可以在后台生成就行了&#xff1b;USER_ID是需要…

【赠书第13期】边缘计算系统设计与实践

文章目录 前言 1 硬件架构设计 2 软件框架设计 3 网络结构设计 4 安全性、可扩展性和性能优化 5 推荐图书 6 粉丝福利 前言 边缘计算是一种新兴的计算模式&#xff0c;它将计算资源推向网络边缘&#xff0c;以更好地满足实时性、低延迟和大规模设备连接的需求。边缘计算…

腾讯开源AppAgent,手机的大模型智能代理

腾讯研究团队开源了一款基于大语言模型的&#xff0c;用于手机端执行复杂任务的多模态智能代理框架——AppAgent。 据悉&#xff0c;AppAgent的功能与AutoGPT等智能代理类似&#xff0c;不依赖于操作系统的后台访问,而是通过UI界面进行点击、滑动等拟人化操作&#xff0c;与Ap…

继电保护测试仪的这4点用途和6点功能你知道几个?

众所周知&#xff0c;继电保护系统是电力系统中至关重要的组成部分。当电路出现故障时&#xff0c;它可以快速准确地切除故障部分&#xff0c;以确保电力系统的安全稳定运行。然后&#xff0c;为了提前发现和保证继电保护系统的故障和问题&#xff0c;通常需要使用继电保护测试…

嵌入式面试准备

题目都摘于网上 嵌入式系统中经常要用到无限循环&#xff0c;如何用C编写死循环 while(1){}或者for(;&#x1f609; 内存分区 代码区&#xff0c;全局区&#xff08;全局变量&#xff0c;静态变量&#xff0c;以及常量&#xff09;&#xff0c;栈区&#xff0c;堆区 const关键…

企业知识库:成为优秀企业的必备系统

导语&#xff1a; 在当今竞争激烈的商业环境中&#xff0c;企业需要不断创新和提高团队的协作效率&#xff0c;以保持竞争优势。而构建一个优秀的企业知识库系统已经成为众多企业必备的系统之一。本文将介绍企业知识库的定义、作用以及如何搭建一个完整有效的企业知识库。 什么…

基于MATLAB的正态分布与卡方分布(附完整代码与例题)

目录 一. 理论部分 二. MATLAB所使用的函数介绍 2.1 概率密度函数 2.2 概率分布函数 2.3 逆概率分布函数 三. 例题与代码 例题1 例题2 例题3 例题4 一. 理论部分 将连续随机变量的概率密度函数记为&#xff0c;既然跟概率相关&#xff0c;那必然满足两个重要的性质&a…

智能酒精壁炉历史轨迹:文化传统与现代融合的取暖方式

智能酒精壁炉作为一种取暖装置&#xff0c;承载着丰富的历史和文化传统。深入了解其在不同文化中的演变过程&#xff0c;以及如何成为现代家庭的取暖方式&#xff0c;可以了解智能酒精壁炉在历史中的发展轨迹。 古希腊和古罗马时期&#xff0c;人们使用酒精和其他可燃性液体作…

猫冻干什么牌子口碑好?专业人士分享口碑好的五款冻干猫粮牌子

近几年&#xff0c;冻干猫粮在宠物圈内非常流行&#xff0c;许多品牌都推出了冻干猫粮。在所有的猫食品中&#xff0c;冻干无疑是最具营养、动物蛋白含量最高的食品之一。冻干作为现在宠物圈最火的猫食品&#xff0c;受到了众多猫友们的喜爱和追捧。但有些铲屎官在选择冻干猫粮…

手动创建idea SpringBoot 项目

步骤一&#xff1a; 步骤二&#xff1a; 选择Spring initializer -> Project SDK 选择自己的JDK版本 ->Next 步骤三&#xff1a; Maven POM ->Next 步骤四&#xff1a; 根据JDK版本选择Spring Boot版本 11版本及以上JDK建议选用3.2版本&#xff0c;JDK为11版本…

rosdep init/update失败(转载)

sudo rosdep init rosdep update 如果你直接成功了&#xff0c;可直接跳过当我没说&#xff08;不过90%的几率你都会失败的啦&#xff09; 由于http://raw.githubusercontent.com这个网址墙的很厉害&#xff0c;大概率你是成功不了的&#xff0c;这里推荐一种本地化安装的方法…

Docker - 镜像 | 容器 | 数据卷 日常开发常用指令 + 演示(一文通关)

目录 Docker 开发常用指令汇总 辅助命令 docker version docker info docker --help 镜像命令 查看镜像信息 下载镜像 搜索镜像 删除镜像 容器命令 查看运行中的容器 运行容器 停止、启动、重启、暂停、恢复容器 杀死容器 删除容器 查看容器日志 进入容器内部…

理解 Go Mod Init

初始化Go模块和管理依赖的全面指南 go mod init 是Go编程语言&#xff08;通常称为Golang&#xff09;中用于初始化新Go模块的命令。在Go中&#xff0c;一个模块是一组相关的Go包&#xff0c;它们作为一个单元一起进行版本控制。通常&#xff0c;在项目目录的根目录下使用 go m…

Qt之自定义分页(翻页)控件

当数据量较大时,分页显示是个不错的选择。这里用百家姓来演示分页效果,包括首页、上一页、下一页、尾页和跳转。 一.效果 每页15个姓氏。 二.实现 QHPageWidget.h #ifndef QHPAGEWIDGET_H #define QHPAGEWIDGET_H#include <QWidget> #include <QStandardItemMod…

搭建一个高效的Python开发环境

“工欲善其事&#xff0c;必先利其器”&#xff0c;这里我们来搭建一套高效的 Python 开发环境&#xff0c;为后续的数据分析做准备。 关于高效作业&#xff0c;对于需要编写 Python 代码进行数据分析的工作而言&#xff0c;主要涉及两个方面。 1. 一款具备强大的自动完成和错…

Linux多线程服务端编程:线程安全的对象管理

1. 前置知识 1.1 __builtin_expect 1.1.1 使用 __builtin_expect提供给编译器分支预测优化信息&#xff0c;其含义为 exp 大概率为 c&#xff0c;其返回值为 exp 的值&#xff1b; long __builtin_expect(long exp, long c) // 下述表明该分支大概率不会执行 if (__builtin…