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

news2024/11/29 13:42:37

文章目录

  • 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

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;
    }
}

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

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

相关文章

洛谷 Equalize the Remainders

洛谷没提供中文题面&#xff0c;这里大致翻译一下&#xff1a; 可以进行的操作&#xff1a;任选一个数加一。 一共有n个整数&#xff0c;还有一个约数m&#xff0c;n个数都对m进行求余&#xff0c;累计余数的数量&#xff0c;要求每个余数都有n/m个。 对于样例1的输入&#xff…

Windows系统下本地MQTT服务器搭建(保姆级教程)

Windows系统下本地MQTT服务器搭建 1.下载并安装emqx服务器 1. 访问Eqmx官网 2. 选中合适的MQTT服务器版本 由于我们使用的是本地部署MQTT服务器&#xff0c;而且只使用基础功能的MQTT服务器功能&#xff0c;所以选中“大规模分布式MQTT消息服务器”即可&#xff0c;如下如图…

构建全面预算体系,加强企业风险管理

全面预算管理体系是帮助企业实现其战略目标的重要手段。随着预算管理理念备受重视&#xff0c;这种新型的企业管理模式通过高效科学的方式和工具&#xff0c;在我国新时代背景下&#xff0c;逐渐成为了企业经营运作过程中针对挑战的有效措施。通常情况下&#xff0c;企业将全面…

docker搭建mysql主从复制

1. 基础环境 环境 名称描述CentOS 7.6Linux操作系统版本docker 20.10.5docker版本mysql 8.0.29mysql镜像版本 节点 节点名称读写/主从地址端口master读节点/主节点192.168.1.6:3306slave1写节点/从节点192.168.1.6:3307slave2写节点/从节点192.168.1.6:3308 2. 主节点 使…

Lightroom Classic 2021 v10.4

Lightroom Classic 2021是一款一体化照片管理和编辑解决方案。 它面向专业人士和高端用户&#xff0c;支持各种不同相机的原始图像编辑&#xff0c;包括Canon、Apple、Casio、Contax、DxO、Epson等品牌。这样可以将原图像快速导入进行编辑&#xff0c;轻松满足不同用户的需求。…

将 Ordinals 与比特币智能合约集成:第 4 部分

控制 BSV-20 代币的分配 在上一篇文章中&#xff0c;我们展示了智能合约可以在铸造后控制 BSV-20 代币的转移。 今天&#xff0c;我们演示如何控制此类代币的分发/发行。 无Tick模式 BSV-20 在 V2 中引入了无Tick模式&#xff0c;并采用了与 V1 不同的方法。 部署 (Deploy) …

大厂面试题-MySQL为什么使用B+Tree作为索引结构

从几个方面来回答&#xff1a; 首先&#xff0c;常规的数据库存储引擎&#xff0c;一般都是采用B树或者B树来实现索引的存储。 (如图)因为B树是一种多路平衡树&#xff0c;用这种存储结构来存储大量数据&#xff0c;它的整个高度会相比二叉树来说&#xff0c;会矮很多。 而对…

【论文阅读】NeROIC:在线图像集合中对象的神经渲染

论文连接&#xff1a; NeROIC: Neural Rendering of Objects from Online Image Collections introduction 从在线图像集合中获取对象表示的新颖方法&#xff0c;从具有不同相机、照明和背景的照片中捕获任意对象的高质量几何形状和材料属性。这使得各种以对象为中心的渲染应…

AI:78-基于深度学习的食物识别与营养分析

🚀 本文选自专栏:人工智能领域200例教程专栏 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的代码,详细讲解供大家学习,希望可以帮到大家。欢迎订阅支持,正在不断更新中,…

ssm+vue的疫情防控管理系统设计与实现(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的疫情防控管理系统设计与实现&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网…

【AI】生成模型变得简单:了解它们的工作原理和不同类型

什么是生成模型&#xff1f; 在不断发展的人工智能领域&#xff0c;生成模型已成为人工智能技术最具吸引力和创造力的方面之一。这些模型是创意人工智能的核心&#xff0c;它们有能力生成各种内容&#xff0c;从栩栩如生的图像和引人入胜的文本到令人着迷的音乐和创新的艺术作…

webpack babel

构建工具 简介 当我们习惯了在node中编写代码的方式后&#xff0c;在回到前端编写html、css、js这些东西会感觉到各种的不便。比如&#xff1a;不能放心的使用模块化规范&#xff08;浏览器兼容性问题&#xff09;、即使可以使用模块化规范也会面临模块过多时的加载问题。我们…

MySQL字符串需要注意的事项

char(N)&#xff0c;N在0-255间 varchar(N)&#xff0c;N在0-65536间 需要注意N是字符&#xff0c;不是字节&#xff0c;英文字母一个字符一个字节&#xff0c;阿拉伯字母一个字符两个字节&#xff0c;中文日文一个字符三个字节&#xff0c;emoji是一个字符四个字节 当今移动端…

跨境电商:自养买家账号测评,你需要了解的细节

在跨境电商的浪潮中&#xff0c;自养买家账号测评已经成为了一种趋势。近期&#xff0c;不少跨境卖家咨询自养买家账号测评的相关问题&#xff0c;看来有必要再讲解一下卖家测评的一些细节。今天将着重介绍自养号测评的相关内容。 首先&#xff0c;什么叫做自养号测评呢&#x…

创建云端服务器

1.申请云端服务器 每个账户有三个月的免费试用 我的服务器选择是centos7 &#xff0c;别选成win了。 2.创建实例 创建实例的步骤&#xff0c;阿里云有文档 介绍 大致就是 左边点实例 -》 顶部选你申请服务器时的地区-》下面就出现一条实例-》点更多 -》要重置实例密码 -》同一…

海康工业相机如何提高相机帧率

影响帧率的因素 相机参数 帧率限制使能 像素格式 曝光时间 数据包大小&#xff08;网口&#xff09; 相机默认参数 ADC位深 系统环境设置

React进阶之路(四)-- React-router-v6、Mobx

文章目录 ReactRouter前置基本使用核心内置组件说明编程式导航路由传参嵌套路由默认二级路由404路由配置集中式路由配置 Mobx什么是Mobx环境配置基础使用计算属性&#xff08;衍生状态&#xff09;异步数据处理模块化多组件数据共享 ReactRouter 前置 在一开始前端开发都是单…

HBuilderX 运行Android App项目至雷电模拟器

一、下载安装HBuilderX HBuildeX官网 安装最新的正式版&#xff0c;或者点击历史版本查看更多版本&#xff1b;【ps&#xff1a;Alpha版本为开发版&#xff0c;功能更多&#xff0c;但是也不稳定&#xff0c;属于测试版本】 直接将压缩包解压&#xff0c;运行HBuildeX即可。 二…

凯美瑞 vs 太空船:Web3 游戏生长的两条路径

撰文&#xff1a;Teng Yan&#xff08;0xPrismatic&#xff09;&#xff0c;Delphi Digital 研究员 编译&#xff1a;TinTinLand 来源&#xff1a;https://0xprismatic.substack.com/p/my-short-web3-gaming-thesis 经常有人问我关于 Web3 游戏的看法&#xff0c;所以我想以这…

文本生成高精准3D模型,北京智源AI研究院等出品—3D-GPT

北京智源AI研究院、牛津大学、澳大利亚国立大学联合发布了一项研究—3D-GPT&#xff0c;通过文本问答方式就能创建高精准3D模型。 据悉&#xff0c;3D-GPT使用了大语言模型的多任务推理能力,通过任务调度代理、概念化代理和建模代理三大模块&#xff0c;简化了3D建模的开发流程…