算法刷题指北-基础篇-背包问题+图(BFS、DFS)+回溯算法

本文介绍了图算法中的邻接矩阵和邻接链表,以及深度优先搜索(DFS)和广度优先搜索(BFS)的原理和Java实现。同时,探讨了回溯算法在解决排列组合、棋盘问题和背包问题中的应用,并举例说明了不同类型的背包问题解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图算法

  • 邻接链表和邻接矩阵
    • 邻接矩阵:一般有两种形式:
      • 无权的图(x,y)
      • 带权图:(x,y,z)
    • 邻接链表:一般头结点是一个数组,后面为其直接连通的节点
      • Node[]:节点数组

深度优先搜索(DFS)

DFS:一般用堆数据结构来辅助实现DFS算法。其过程简要来说是**对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。**借助堆栈数据结构暂时保存还有下一未入栈节点的节点数据,如果没有将该节点这出栈,直到堆栈为空(如果连通图此时图所有节点均已经遍历),如果不是连通图则选择另一个图的该节点进行类似操作;

例如如下的无向图

img

​ 从1开始进行深度优先遍历,借助栈堆结构,那么进行深度优先搜索的时候一种可能的情况是:1,2,3,4,5先后入栈,此时5没有下一没有遍历的节点,所以对于5进行出栈,然后由于4有还未遍历的节点6所以对于6进入入栈,同理7也会进行一次进出栈,然后8、9先后入栈,最后全部出栈;

//java实现
public void dfs() {
    if(xx) {
        if(xx) {
            //收集
        }
        return;
    }
	for() {
        //剪枝
        if(xxx){
            continue;//break
        }
        //添加
        dfs();//dfs
        //删除
    }
}

广度优先搜索(BFS)

  • BFS广度优先搜索是按层来处理顶点,距离开始点最近(在无权或者权值相等的图中)的那些顶点首先被访问,而最远的那些顶点则最后被访问,这个和树的层序变量很像,BFS的代码借助了一个队列实现,每次将该节点所有连通的未遍历的下一节点入队,然后再对队列中下一节点进行类似操作
    • BFS本质是求最值问题(例如最短路径);
      • 例如:求有A到B,最短路径,直接BFS 搜索即可
    • 有些题目不能直接搜索
      • 例如:求有A到B,最少操作多少次(某些位置本来不能通过,操作后可通过)可以到达B;显然这时有俩种思路
        • 分组:显然对于任意节点如果连通可以认为他们是一个节点,这样题目就变回了一般的BFS
          • 实现思路就是直接连通的通过DFS遍历确定,非直接连通的通过扩散进行(BFS模板即可)
        • 0-1BFS:一般BFS 是通过单向队列(先进先出),0-1BFS则是将直接连通的视为权重0,非直接连通的视为权重1,这时,权重0的放入队头,权重1的放入队尾,所以权重0 的总是先被遍历到
          • 显然其本质还是 将直接连通的作为一组,只不过其通过BFS的方式进行分组
      • 例题:

例子使用上次的图片:

img
  • 首先,将1入队,然后出队将与1连通还未入队的2,8入队;
  • 然后2出队,将3,5入队;
  • 8出队,将6、9入队;
  • 3出队,4入队;
  • 5出队、然后6出队,7入队、最后队列所有节点相继出队;

实现

//java实现
public RES bfs(xxx) {
    Queue<XXX> queue = new LinkedList<>();
    if(xxx) {
        return xx;
    }
    queue.add(xxx);
    int size = 1;
    for(queue.size()!=0&&xxx) {
        for(int i=0;i<size;i++) {
            XXX xx = queue.poll();
            //收集结果
            resxxxxxx
            if(xx) {
                queue.add(xx.xxxx);
            }
        }
        size = queue.size();
    }
    return res;
}

回溯算法

理论基础

  • 回溯算法:类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

  • 回溯法思想简单描述:把问题的解空间转化为图或者树的结构表示,然后使用dfs进行遍历,遍历过程中记录和寻找可行解或者最优解。

    • 这里无论是树还是图,其要点无非就是三个:
      • 先序遍历的剪枝
      • 中序遍历的选择最优解
      • 后续遍历的记忆化(如果需要记忆)
    • 算法退出一般使用自然条件即可,对于图可能需要借助访问矩阵;

回溯算法解决问题的一般步骤

  • 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
  • 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
  • 深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索

适用类型

  • 排列组合问题
    • 排列问题:需要考虑顺序:比如子序列(给定不相同的n个不相同的数,这些数能组合成的所有情况);
    • 组合问题:即没有顺序:比如子集合(给定一个集合{abcd……},组成的所有含有k个元素的子集)
  • 棋盘问题
    • 八皇后问题
  • 字符串切割 和 数组子集
    • 同前面

解题

一个经典的回溯算法解题格式为:

public Object solution(……) {
    Object res = new Object();
    dfs(res,……);
    return res;
}
//深度优先遍历
public void dfs(Object res,……) {
    //终止条件
    if(xxxx) {
        //收集结果
        resXXX;
        //结束回退
        return;
    }
    // 深度优先遍历
    for() {
        //条件选择
        choose;
        //递归
        dfs(res,……);
        //回溯
        reChoose;
    }
}

例题:https://leetcode-cn.com/problems/word-break/

背包问题

  • 背包问题:

0-1背包

  • 物品的状态:每个选择只能做一次;

    • f(x,y) = {f(x-1,y)|w[x]>targer} | {MAX(f(x-1,y),f(x-1,y-w[x])+v[x]) | w[x]<=target}

    • 既有

      • int[] dp = new int[价值+1];//这里的价值是承受能力的意思,为了方便理解
        for(物品)
            for(价值范围【逆序,避免覆盖】)
                if(w[x]>target) {
                    dp[价值] = dp[价值]//即上一价值
                } else {
                    dp[价值] = MAX(dp[价值],dp[价值-w[x]]+v[x])}
        
  • 状态机的角度:例如最大的容量为x,则:状态机包含x中状态,每个物品可以使得状态机由某一状态转为下一状态;

完全背包

  • 物品的状态:可以在该选择无限重复选择;

    • 可以转化为多个0-1背包问题(即相当于所有价值多一层循环,直到不满足位置)

      • f(x,y) = {f(x-1,y)|w[x]>targer} | {MAX(f(x-1,y),f(x,y-k*w[x]+k*v[x])) | w[x]<=target}
    • 可以直接借用当前层数组进行滚动(由于当前无穷多可选,显然当前层就具有无后性,直接使用当前层状态)

      • f(x,y) = {f(x-1,y)|w[x]>targer} | {MAX(f(x-1,y),f(x,y-w[x]+v[x])) | w[x]*k<=target}
    • int[] dp = new int[物品数+1];
      for(物品)
          for(价值范围【正序,利用当前层无后性】)
              if(w[x]>target) {
                  dp[价值] = dp[价值-1]} else {
                  dp[价值] = MAX(dp[价值-1],dp[价值-w[x]+v[x]])}
      
  • 状态机的角度:例如最大的容量为x,则:状态机包含x中状态,每个物品可以使得状态机由某一状态转为下一状态;

多重背包

  • 物品的状态:可以在该选择有限(指定次数)重复选择;

  • 参考前面:可以转化为多个0-1背包问题(即相当于所有价值多一层循环,直到不满足位置)

    • f(x,y) = {f(x-1,y)|w[x]>targer} | {MAX(f(x-1,y),f(x,y-k*w[x]+k*v[x])) | w[x]*k<=target&&k<=给定数量}
  • 二进制替换:还没有研究。。。。

  • int[] dp = new int[物品数+1];
    for(物品)
       for(本重可选的次数)
           for(价值范围【逆序,避免覆盖】)
                if(w[x]>target) {
                    dp[价值] = dp[价值-1]} else {
                    dp[价值] = MAX(dp[价值-1],dp[价值-w[x]+v[x]])}
    

分组背包

  • 物品的状态:分组内的物品必须选择,且只能选择一次,分组内如果选择空集就是不选择该分组任意元素或者分组作为一个节点构成一个0-1背包问题

    • 对于某些题目不允许选择空,则为必选的分组背包;
  • int[] dp = new int[分组+1];
    for(分组)
        for(价值范围【正序,利用当前层无后性】)
            for(可选分组元素)
                //具体操作
    

依赖背包

  • 依赖背包是指:如果选择某一物品必须选择其依赖的物品,依赖背包可以转化为分组背包:即将依赖的划分为一组,例如:选择1必须选择2,选择2必须选择3,选择5必选选择3;则可以将{1,2,3,5,6,7,8}划分为作为整体,将相交的整体划分到一个分组;既有:

    • [[{},{3},{3,5},{3,2},{3,2,5},{3,2,1},{3,2,1,5}],[6],[7],[8]]
  • int[] dp = new int[分组+1];
    for(分组)
        for(价值范围【正序,利用当前层无后性】)
            //dfs求可选的分组
            for(可选分组元素)
                //具体操作
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

舔猫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值