图论10-哈密尔顿回路和哈密尔顿路径+状态压缩+记忆化搜索

文章目录

  • 1 哈密尔顿回路
  • 2 哈密尔顿回路算法实现
    • 2.1 常规回溯算法
    • 2.2 引入变量记录剩余未访问的节点数量
  • 3 哈密尔顿路径问题
  • 4 状态压缩
    • 4.1 查看第i位是否为1
    • 4.2 设置第i位是为1或者0
    • 4.3 小结
    • 4.4 状态压缩在哈密尔顿问题中的应用
  • 5 记忆化搜索
    • 5.1 记忆化搜索与递推区别
    • 5.2 记忆化搜索的实现 - 力扣980

1 哈密尔顿回路

求解哈密尔顿回路

如何求解一个图是否存在哈密尔顿回路呢?

一个最直观的想法就是暴力求解。暴力求解的思路也很简单:我们遍历图的每一个顶点 v,然后从顶点 v 出发,看是否能够找到一条哈密尔顿回路。

暴力求解的代价同求解全排列问题是等价的,其时间复杂度为 O ( N ! ) O(N!) O(N!),N 为图的顶点的个数。

那么除了暴力求解哈密尔顿回路问题,是否存在更好的算法?

很遗憾我们只能在暴力破解的基础上,尽量去做到更多的优化,譬如回溯剪枝,记忆化搜索等,但是,还没有找到一种多项式级别的算法来解决哈密尔顿问题。

通常,这类问题也被称为 NP(Non-deterministic Polynomial)难问题。

综上所述,求解哈密尔顿回路,我们可以采用回溯+剪枝的思想来进行求解。

在这里插入图片描述

2 哈密尔顿回路算法实现

2.1 常规回溯算法

package Chapter07_Hamilton_Loop_And_Path.Hamilton_Loop;

import java.util.ArrayList;
import java.util.Collections;

public class HamiltonLoop {

    private Graph G;
    private boolean[] visited;
    private int[] pre;
    private int end; //用来表示最后一个被遍历的顶点

    public HamiltonLoop(Graph G){

        this.G = G;
        visited = new boolean[G.V()];
        pre = new int[G.V()];
        end = -1;
        dfs(0, 0);
    }

    private boolean dfs(int v, int parent){

        visited[v] = true;
        pre[v] = parent;

        for(int w: G.adj(v))
            if(!visited[w]){
                if(dfs(w, v))
                    return true;
            }
            else if(w == 0 && allVisited()){ //如果回到起始点0并且所有的点都被访问过了,则找到了哈密尔回路
                end = v;
                return true;
            }

        // 回溯
        visited[v] = false;
        return false;
    }

    public ArrayList<Integer> result(){

        ArrayList<Integer> res = new ArrayList<>();
        if(end == -1) return res;

        int cur = end;
        while(cur != 0){
            res.add(cur);
            cur = pre[cur];//上一个节点
        }
        res.add(0);

        Collections.reverse(res);
        return res;
    }

    private boolean allVisited(){
        for(int v = 0; v < G.V(); v ++)
            if(!visited[v]) return false;
        return true;
    }

    public static void main(String[] args){

        Graph g = new Graph("g9.txt");
        HamiltonLoop hl = new HamiltonLoop(g);
        System.out.println(hl.result());

        Graph g2 = new Graph("g10.txt");
        HamiltonLoop hl2 = new HamiltonLoop(g2);
        System.out.println(hl2.result());
    }
}

在这里插入图片描述

2.2 引入变量记录剩余未访问的节点数量

package Chapter07_Hamilton_Loop_And_Path.Hamilton_Loop;

import java.util.ArrayList;
import java.util.Collections;

public class HamiltonLoop_Optimization {

    private Graph G;
    private boolean[] visited;
    private int[] pre;
    private int end;

    public HamiltonLoop_Optimization(Graph G){

        this.G = G;
        visited = new boolean[G.V()];
        pre = new int[G.V()];
        end = -1;

        //dfs的过程记录剩余未访问的节点的数量
        dfs(0, 0, G.V());
    }

    private boolean dfs(int v, int parent, int left){
        visited[v] = true;
        pre[v] = parent;
        left --;

        //出口:如果未访问的节点是0,并且当前节点和初始节点有边
        if(left == 0 && G.hasEdge(v, 0)){
            end = v;
            return true;
        }

        for(int w: G.adj(v))
            if(!visited[w]){
                if(dfs(w, v, left)) return true;
            }
//            else if(w == 0 && left == 0){
//                end = v;
//                return true;
//            }

        visited[v] = false;
        return false;
    }

    public ArrayList<Integer> result(){

        ArrayList<Integer> res = new ArrayList<>();
        if(end == -1) return res;

        int cur = end;
        while(cur != 0){
            res.add(cur);
            cur = pre[cur];
        }
        res.add(0);

        Collections.reverse(res);
        return res;
    }

    public static void main(String[] args){

        Graph g = new Graph("g9.txt");
        HamiltonLoop hl = new HamiltonLoop(g);
        System.out.println(hl.result());

        Graph g2 = new Graph("g10.txt");
        HamiltonLoop hl2 = new HamiltonLoop(g2);
        System.out.println(hl2.result());
    }
}

3 哈密尔顿路径问题

根据出发位置的不同,路径也会不一样。但是算法实现还是一致的,只需要修改构造函数,传入起点位置。

package Chapter07_Hamilton_Loop_And_Path.Hamilton_Loop;

import java.util.ArrayList;
import java.util.Collections;

public class HamiltonPath {

    private Graph G;
    private int s;
    private boolean[] visited;
    private int[] pre;
    private int end;

    public HamiltonPath(Graph G, int s){

        this.G = G;
        this.s = s;
        visited = new boolean[G.V()];
        pre = new int[G.V()];
        end = -1;
        dfs(s, s, G.V());
    }

    private boolean dfs(int v, int parent, int left){

        visited[v] = true;
        pre[v] = parent;
        left --;
        if(left == 0){
            end = v;
            return true;
        }

        for(int w: G.adj(v))
            if(!visited[w]){
                if(dfs(w, v, left)) return true;
            }

        visited[v] = false;
        return false;
    }

    public ArrayList<Integer> result(){

        ArrayList<Integer> res = new ArrayList<>();
        if(end == -1) return res;

        int cur = end;
        while(cur != s){
            res.add(cur);
            cur = pre[cur];
        }
        res.add(s);

        Collections.reverse(res);
        return res;
    }

    public static void main(String[] args){

        Graph g = new Graph("g10.txt");
        HamiltonPath hp = new HamiltonPath(g, 0);
        System.out.println(hp.result());

        HamiltonPath hp2 = new HamiltonPath(g, 1);
        System.out.println(hp2.result());
    }
}

在这里插入图片描述

4 状态压缩

在这里插入图片描述

4.1 查看第i位是否为1

在这里插入图片描述

4.2 设置第i位是为1或者0

在这里插入图片描述

4.3 小结

在这里插入图片描述

4.4 状态压缩在哈密尔顿问题中的应用

在我们的代码中,一直都使用布尔型的 visited 数组来记录图中的每一个顶点是否有被遍历过。

在算法面试中,对于像哈密尔顿回路/路径这样的 NP 难问题,通常都会有输入限制,一般情况下,求解问题中给定的图不会超过 30 个顶点。

这样,在算法笔试/面试中,我们就可以对 visited 数组进行状态压缩来优化算法类执行的效率。我们知道一个 int 型的数字有 32 位,每一位不是 1 就是 0,这正好对应了布尔型的 true 和 false。

所以,我们可以将 visited 数组简化成一个数字,该数字的每一个比特位用来表示 visited 数组的每一个 true 或 false 值。

来看一下我们的 HamiltonLoop 中 dfs 的代码:

    public HamiltonLoop_StateCompression(Graph G){

        this.G = G;
        pre = new int[G.V()];
        end = -1;

        int visited = 0; //用一个数的比特位来表示是否被访问过
        dfs(visited, 0, 0, G.V());
    }

    private boolean dfs(int visited, int v, int parent, int left){

        visited += (1 << v); //第v位置设置为1
        pre[v] = parent;
        left --;
        if(left == 0 && G.hasEdge(v, 0)){
            end = v;
            return true;
        }

        for(int w: G.adj(v))
            if((visited & (1 << w)) == 0){ //看数字的第w个位置是否为 0
                if(dfs(visited, w, v, left)) return true;
            }

        visited -= (1 << v); //回溯,第v位置恢复为0
        return false;
    }

5 记忆化搜索

记忆化搜索是动态规划的一种实现方式。

在记忆化搜索中,当算法需要计算某个子问题的结果时,它首先检查是否已经计算过该问题。如果已经计算过,则直接返回已经存储的结果;否则,计算该问题,并将结果存储下来以备将来使用。

举个例子,比如「斐波那契数列」的定义是: f ( 0 ) = 0 , f ( 1 ) = 1 , f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(0) = 0, f(1) = 1, f(n) = f(n - 1) + f(n - 2) f(0)=0,f(1)=1,f(n)=f(n1)+f(n2)。如果我们使用递归算法求解第 n n n 个斐波那契数,则对应的递推过程如下:

在这里插入图片描述
从图中可以看出:如果使用普通递归算法,想要计算 f ( 5 ) f(5) f(5),需要先计算 f ( 3 ) f(3) f(3) f ( 4 ) f(4) f(4),而在计算 f ( 4 ) f(4) f(4) 时还需要计算 f ( 3 ) f(3) f(3)。这样 f ( 3 ) f(3) f(3) 就进行了多次计算,同理 f ( 0 ) f(0) f(0) f ( 1 ) f(1) f(1) f ( 2 ) f(2) f(2) 都进行了多次计算,从而导致了重复计算问题。

为了避免重复计算,在递归的同时,我们可以使用一个缓存(数组或哈希表)来保存已经求解过的 f ( k ) f(k) f(k) 的结果。如上图所示,当递归调用用到 f ( k ) f(k) f(k) 时,先查看一下之前是否已经计算过结果,如果已经计算过,则直接从缓存中取值返回,而不用再递推下去,这样就避免了重复计算问题。

5.1 记忆化搜索与递推区别

  • 记忆化搜索:「自顶向下」的解决问题,采用自然的递归方式编写过程,在过程中会保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。

  • 优点:代码清晰易懂,可以有效的处理一些复杂的状态转移方程。有些状态转移方程是非常复杂的,使用记忆化搜索可以将复杂的状态转移方程拆分成多个子问题,通过递归调用来解决。

  • 缺点:可能会因为递归深度过大而导致栈溢出问题。

  • 递推:「自底向上」的解决问题,采用循环的方式编写过程,在过程中通过保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。

  • 优点:避免了深度过大问题,不存在栈溢出问题。计算顺序比较明确,易于实现。

  • 缺点:无法处理一些复杂的状态转移方程。有些状态转移方程非常复杂,如果使用递推方法来计算,就会导致代码实现变得非常困难。

  • 记忆化搜索解题步骤

我们在使用记忆化搜索解决问题的时候,其基本步骤如下:

  1. 写出问题的动态规划「状态」和「状态转移方程」。 定义一个缓存(数组或哈希表),用于保存子问题的解。
  2. 定义一个递归函数,用于解决问题。在递归函数中,首先检查缓存中是否已经存在需要计算的结果,如果存在则直接返回结果,否则进行计算,并将结果存储到缓存中,再返回结果。
  3. 在主函数中,调用递归函数并返回结果。

5.2 记忆化搜索的实现 - 力扣980

 memo = new int[1 << (R * C)][R * C];
  • 第一个维度是顶点数量的2倍,因为一个位置有两种可能——访问过/未访问过。

    • 假设顶点数量为1,则第一个维度未2: 0, 1.
      假设顶点数量为2,则第一个维度未4: 00, 01, 10, 11.
  • 第二个维度表示顶点数量,记录当前状态对应顶点是否被访问过。

在这里插入图片描述

package Chapter07_HamiltonLoop_And_StateCompression_And_MemoizationSearch.Memoization_Search;

import java.util.Arrays;

// Leetcode 980
class Solution {

    private int[][] dirs = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    private int R, C;
    private int[][] grid;
    private int start, end;
    private int[][] memo;

    public int uniquePathsIII(int[][] grid) {

        this.grid = grid;
        R = grid.length;
        C = grid[0].length;
        int left = R * C;
        memo = new int[1 << (R * C)][R * C];
        for(int i = 0; i < memo.length; i ++)
            Arrays.fill(memo[i], -1);

        for(int i = 0; i < R; i ++)
            for(int j = 0; j < C; j ++)
                if(grid[i][j] == 1){
                    start = i * C + j;
                    grid[i][j] = 0;
                }
                else if(grid[i][j] == 2){
                    end = i * C + j;
                    grid[i][j] = 0;
                }
                else if(grid[i][j] == -1)
                    left --;

        int visited = 0;
        return dfs(visited, start, left);
    }

    private int dfs(int visited, int v, int left){

        if(memo[visited][v] != -1) return memo[visited][v];

        visited += (1 << v);
        left --;
        if(v == end && left == 0){
            visited -= (1 << v);
            memo[visited][v] = 1;
            return 1;
        }

        int x = v / C, y = v % C;
        int res = 0;
        for(int d = 0; d < 4; d ++){
            int nextx = x + dirs[d][0], nexty = y + dirs[d][1];
            int next = nextx * C + nexty;
            if(inArea(nextx, nexty) && grid[nextx][nexty] == 0 && (visited & (1 << next)) == 0)
                res += dfs(visited, next, left);
        }

        visited -= (1 << v);
        memo[visited][v] = res;
        return res;
    }

    private boolean inArea(int x, int y){
        return x >= 0 && x < R && y >= 0 && y < C;
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Unity细节】Failed importing package???Unity导包失败?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…

阿里云服务器怎么样?阿里云服务器优势、价格及常见问题介绍

阿里云&#xff08;Alibaba Cloud&#xff09;是阿里巴巴集团旗下的云计算服务提供商&#xff0c;其提供的云服务器&#xff08;ECS&#xff09;是其核心服务之一。在云计算市场中&#xff0c;阿里云服务器备受用户的青睐&#xff0c;那么&#xff0c;阿里云服务器究竟怎么样呢…

现在个人想上架微信小游戏已经这么难了吗...

点击上方亿元程序员关注和★星标 引言 大家好&#xff0c;最近我突然想起来我还有一款微信小游戏还没有上架&#xff0c;于是捣鼓了一天把游戏完善了一下&#xff0c;然后准备提交审核&#xff0c;却发现异常的艰难… 1.为什么难&#xff1f; 相信大家都大概知道&#xff0c…

基于SSM的数据结构课程网络学习平台

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

基于SpringBoot的SSMP整合案例(实体类开发与数据层开发)

实体类开发 导入依赖 Lombok&#xff0c;一个Java类库&#xff0c;提供了一组注解&#xff0c;简化POJO实体类开发<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId> </dependency>lombok版本由SpringB…

02MyBatisPlus条件构造器,自定义SQL,Service接口

一、条件构造器 1.MyBatis支持各种复杂的where条件&#xff0c;满足开发的需求 Wrapper是条件构造器&#xff0c;构建复杂的where查询 AbstractWrapper有构造where条件的所有方法&#xff0c;QueryWrapper继承后并有自己的select指定查询字段。UpdateWrapper有指定更新的字段的…

c: CLion 2023.1.1

/** # encoding: utf-8 # 版权所有 2023 涂聚文有限公司 # 许可信息查看&#xff1a;https://www.learnc.net/c-data-structures/c-linked-list/ # 描述&#xff1a;https://blog.jetbrains.com/clion/2016/05/keep-your-code-documented/ # Author : geovindu,Geovin Du 涂…

将VS工程转为pro工程及VS安装Qt插件后没有create basic .pro file菜单问题解决

目录 1. 前言 2. VS工程转为pro工程 3. 没有create basic .pro file菜单 1. 前言 很多小伙伴包括本人&#xff0c;如果是在Windows下开发Qt程序&#xff0c;偏好用Visual Studio外加装个Qt插件进行Qt开发&#xff0c;毕竟Visual Studio确实是功能强大的IDE&#xff0c;但有时…

【Truffle】四、通过Ganache部署连接

目录 一、下载安装 Ganache&#xff1a; 二、在本地部署truffle 三、配置ganache连接truffle 四、交易发送 除了用Truffle Develop&#xff0c;还可以选择使用 Ganache, 这是一个桌面应用&#xff0c;他同样会创建一个个人模拟的区块链。 对于刚接触以太坊的同学来说&#x…

LeetCode16的最接近的三数之和

目录 优化解法暴力搜索 优化解法 看了题解之后的根据题解的意思编写的优化解法,感觉还行,代码算是比较简短了,没有复杂的逻辑,就是写的时候总是只记得记录那个sum,忘记要记录最小的差值,更新min_minus. class Solution {public int threeSumClosest(int[] nums, int target) {…

NLP领域的突破催生大模型范式的形成与发展

当前的大模型领域的发展&#xff0c;只是范式转变的开始&#xff0c;基础大模型才刚刚开始改变人工智能系统在世界上的构建和部署方式。 1、大模型范式 1.1 传统思路&#xff08;2019年以前&#xff09; NLP领域历来专注于为具有挑战性的语言任务定义和设计系统&#xff0c…

【算法与数据结构】93、LeetCode复原 IP 地址

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;参照【算法与数据结构】131、LeetCode分割回文串的思路&#xff0c;需要将IP字符串进行分割&#xff0…

“基于RflySim平台飞控底层算法开发”系列专题培训 (第三期)

>> RflySim平台系列专题培训 RflySim平台是一个生态系统或工具链&#xff08;官网&#xff1a;https://doc.rflysim.com&#xff09;&#xff0c;发起于北航可靠飞行控制研究组&#xff0c;主要用于遵循基于模型设计的思想进行无人系统的控制和安全测试。本平台选择MATL…

使用MVS-GaN HEMT紧凑模型促进基于GaN的射频和高电压电路设计

标题&#xff1a;Facilitation of GaN-Based RF- and HV-Circuit Designs Using MVS-GaN HEMT Compact Model 来源&#xff1a;IEEE TRANSACTIONS ON ELECTRON DEVICES&#xff08;19年&#xff09; 摘要—本文阐述了基于物理的紧凑器件模型在研究器件行为细微差异对电路和系统…

目标检测最新创新点: EMS-YOLO:首个用于目标检测的直接训练脉冲神经网络

EMS-YOLO&#xff1a;第一个用于目标检测的深度直接训练脉冲神经网络&#xff0c;首次使用代理梯度训练深度 SNN 进行检测&#xff0c;并设计全脉冲残差块EMS-ResNet&#xff0c;代码刚刚开源&#xff01;单位&#xff1a;国科大, 西安交大, 清华, 北大, 华为 脉冲神经网络 (S…

151. 反转字符串中的单词

151. 反转字符串中的单词 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a;错误经验吸取 原题链接&#xff1a; 151. 反转字符串中的单词 https://leetcode.cn/problems/reverse-words-in-a-string/description/ 完成情况&#xff1a; 解…

音频——解析 PCM 数据

文章目录 生成 PCM 数据16bit16bit mono16bit stereo16bit 4 channel16bit 8 channel24bit解析 PCM 数据解析 24bit 数据程序源码生成 PCM 源码解析 PCM 源码生成 PCM 数据 16bit 16bit mono int 48k_16bit_modo[] = {0, 4276, 8480, 12539, 16383, 19947, 23169, 25995, 28…

华为防火墙vrrp+hrp双机热备主备备份(两端为交换机)

默认上下来全两个vrrp主都是左边 工作原理&#xff1a; vrrp刚开机都是先initialize状态&#xff0c;然后切成active或standb状态。 hrp使用18514端口&#xff0c;且用的单播&#xff0c;要策略放行&#xff0c;由主设备发hrp心跳报文 如果设备为acitve状态时自动优先级为65…

CS224W5.1——消息传递和节点分类

从之前的文中&#xff0c;学习了如何使用图表示学习进行节点分类。在这节中&#xff0c;将讨论另一种方法&#xff0c;消息传递。将引入半监督学习&#xff0c;利用网络中存在的相关性来预测节点标签。其中一个关键概念是集体分类&#xff0c;包括分配初始标签的局部分类器、捕…

strerror函数详解之【错误码探秘】

目录 一&#xff0c;strerror函数简介 二&#xff0c;strerror函数的基本用法 三&#xff0c;errno变量 一&#xff0c;strerror函数简介 当程序出现错误时&#xff0c;了解错误的具体信息对于调试和修复问题至关重要。在C语言中&#xff0c;我们可以使用strerror函数来获取…