记忆化搜索与状态压缩:优化递归与动态规划的利器

记忆化搜索是解决递归和动态规划问题的一种高效优化技术。它结合了递归的灵活性和动态规划的缓存思想,通过保存已经计算过的子问题结果,避免了重复计算,大幅提升了算法的效率。当问题状态复杂时,状态压缩技术可以进一步优化空间使用,尤其在处理大规模搜索问题时表现突出。本文将深入解析记忆化搜索的原理、应用,并结合状态压缩技术展示其在面试和算法竞赛中的常见应用。

1. 记忆化搜索的基本概念

1.1 什么是记忆化搜索?

记忆化搜索是一种递归与动态规划相结合的优化方法。它的核心思想是通过递归来解决问题,同时将已计算过的子问题结果保存起来(通常存储在数组或哈希表中),以便在后续调用时直接返回结果,避免重复计算。

1.2 记忆化搜索的应用场景

记忆化搜索常用于解决具有重叠子问题的递归问题。常见的应用场景包括:

  • 斐波那契数列:通过记忆化避免重复计算同一层次的结果。

  • 背包问题:在递归中缓存不同容量和物品选择的结果。

  • 图论中的最短路径问题:如 TSP 问题,通过记忆化减少不必要的重复计算。

1.3 示例:记忆化斐波那契数列

斐波那契数列的递归形式会重复计算很多相同的子问题,例如 fib(4) 会递归计算两次 fib(3)fib(2)。通过记忆化搜索,我们可以将中间结果存储起来,从而避免冗余计算。

import java.util.HashMap;
import java.util.Map;
​
public class Fibonacci {private Map<Integer, Integer> memo = new HashMap<>();
​public int fib(int n) {if (n <= 1) return n;if (memo.containsKey(n)) return memo.get(n);  // 从缓存中获取int result = fib(n - 1) + fib(n - 2);memo.put(n, result);  // 将计算结果存入缓存return result;}
​public static void main(String[] args) {Fibonacci fib = new Fibonacci();System.out.println(fib.fib(10));  // 输出 55}
}

这种记忆化搜索通过保存已经计算过的斐波那契值,避免了指数级递归的时间开销,优化后的时间复杂度为 O(n)

2. 记忆化搜索与动态规划的关系

记忆化搜索可以看作是递归版本的动态规划,它们的核心思想都是缓存中间状态的结果,但在实现方式上有所不同:

  • 动态规划(DP):自底向上,通过填表方式迭代求解。

  • 记忆化搜索:自顶向下,通过递归求解的同时缓存结果。

2.1 选择记忆化搜索的场景

  • 递归结构清晰的场景:如果问题本质上是递归求解的,且递归结构容易表达,记忆化搜索往往是更直接的解决方法。

  • 状态空间较大且需要缓存中间结果:记忆化搜索常用于那些状态多、空间大的问题,尤其适合结合状态压缩技术。

3. 状态压缩与记忆化搜索的结合

3.1 什么是状态压缩?

状态压缩的核心思想是将多个状态变量组合成一个数值或位掩码,以减少存储空间。例如,在图论问题中,可以用一个整数的二进制形式记录多个顶点的访问情况。这种方法通过紧凑的状态表示优化了存储效率,常用于处理复杂的动态规划问题。

3.2 状态压缩的应用场景

状态压缩与记忆化搜索的结合,能够解决很多复杂的图论和动态规划问题:

  • TSP 问题(旅行商问题):通过状态压缩记录已访问的城市,减少重复计算。

  • 棋盘覆盖问题:通过压缩棋盘状态,记录当前状态下的覆盖情况。

3.3 示例:TSP 问题中的状态压缩与记忆化搜索

旅行商问题要求找到一个最短路径,使得从起点经过每个城市恰好一次并回到起点。为了优化计算,我们使用状态压缩记录哪些城市已经访问过,并结合记忆化搜索来减少重复计算。

import java.util.Arrays;
​
public class TSP {private int n;private int[][] dist;private int[][] dp;  // dp[state][i] 表示从起点经过 state 状态下到达 i 的最短路径
​public TSP(int n, int[][] dist) {this.n = n;this.dist = dist;this.dp = new int[1 << n][n];  // 状态压缩for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE);dp[1][0] = 0;  // 起点到自身的距离为 0}
​public int solve() {for (int state = 1; state < (1 << n); state++) {for (int last = 0; last < n; last++) {if ((state & (1 << last)) == 0) continue;  // last 必须在当前状态中for (int prev = 0; prev < n; prev++) {if ((state & (1 << prev)) == 0) continue;  // prev 必须在当前状态中dp[state][last] = Math.min(dp[state][last], dp[state ^ (1 << last)][prev] + dist[prev][last]);}}}// 返回经过所有城市并回到起点的最短路径return Arrays.stream(dp[(1 << n) - 1]).min().getAsInt();}
​public static void main(String[] args) {int[][] dist = {{0, 10, 15, 20},{10, 0, 35, 25},{15, 35, 0, 30},{20, 25, 30, 0}};TSP tsp = new TSP(4, dist);System.out.println(tsp.solve());  // 输出 80}
}

在此代码中,我们使用位运算表示哪些城市已经被访问,并结合记忆化搜索记录每个状态下的最短路径,从而避免重复计算。

4. 实战案例:棋盘覆盖问题

4.1 问题描述

给定一个 N×M 的棋盘,要求将其分割成若干个 1×2 的长方形,问有多少种合法的分割方案。

例如当 N=2,M=4N=2,M=4 时,共有 55 种方案。当 N=2,M=3N=2,M=3 时,共有 33 种方案。

如下图所示:

4.2 记忆化搜索与状态压缩的结合

在棋盘覆盖问题中,我们使用位运算来表示每一列的状态,通过记忆化搜索来缓存中间状态,避免重复计算。状态转移则通过位运算来完成。

import java.util.*;public class Main{public static void main(String[] args) {Scanner sca = new Scanner(System.in);int N = 12, M = 1 << N;long[][] f = new long[N][M];int[][] state = new int[M][M];boolean[] st = new boolean[M];while (true) {int n = sca.nextInt();int m = sca.nextInt();//当 n 和 m 同时为 0 结束循环if (n == 0 && m == 0) {break;}for (int i = 0; i < 1<< n; i ++ ) {int cnt = 0;    //  表示的是当前 前面0的个数boolean flag = true;for (int j = 0; j < n; j ++ ) { // 从上倒下判断有多少个0// 判断现在这位是不是 1if ((i >> j & 1) == 1 ) {//如果是1,判断1前面0的个数是不是偶数,奇数的话就结束if ((cnt & 1) == 1) { // & 1  等于 1 就是奇数,反之是偶数flag = false;break;}cnt = 0;} else {cnt++; // 如果当前不是1 ,则 0 的个数 +1}}// 最后还要判断一下最后一层0的个数是不是奇数if ((cnt & 1) == 1) flag = false;// 最后将这一种状态存入st数组,表示true 合法 或者false非法st[i] = flag;}// 这是 i- 1 到 i列的方块for (int i = 0; i < 1 << n; i ++ ) {// 将所有的状态清零,因为多组数据防止上一组的影响Arrays.fill(state[i], 0); for(int j = 0; j < 1 << n; j ++ ) {// 当满足 1. i 和 j没有相交(同一行的两列不能连续放置方块会重叠)//        2. i - 1 列的空格数是不是偶数if ((i & j) == 0 && st[i | j]) {state[i][j] = 1;}}}for (int i = 0; i < N; i ++ ) {// 因为有多组数据,防止上一组数据的干扰Arrays.fill(f[i], 0);}// 边界,横着在第一列方只有一种方案就是 什么也不放 f[0][0] = 1;// 最后的 DP部分//为什么从1开始呢,因为从0开始的话,我们定义的f[m][j]就是前i - 1列已经摆好//如果是0开始,就会从-1个开始摆好,因为我们没有-1列,所以从1开始for (int i = 1; i <= m ; i ++ ) {// 枚举 i - 1 到 i 的所有方案啊for (int j = 0; j < 1 << n; j ++) {//枚举 i- 2 到 i- 1 的所有方案啊for (int k = 0; k < 1 << n; k ++ ) {// 现在的方案等于前面每种k方案的总和if (state[j][k] == 1 ) {f[i][j] += f[i - 1][k];}}}}System.out.println(f[m][0]);}}
}

详细题解请移步:蒙德里安的梦想

通过状态压缩,我们减少了空间消耗,并利用记忆化搜索提升了算法效率。

5. 性能分析与优化策略

5.1 时间与空间复杂度分析

记忆化搜索通过缓存结果,将递归问题的时间复杂度从指数级降低到线性级别;结合状态压缩后,还可以进一步减少空间消耗。在 TSP 问题中,时间复杂度为 O(n^2 * 2^n),空间复杂度也被压缩到 O(n * 2^n)

5.2 常见的优化策略

  • 剪枝:通过提前判断某些路径不可能达到最优解,避免无效计算。

  • 缓存状态:在动态规划中合理设计状态表示,确保状态能够准确地反映问题的当前进展,避免遗漏和重复。

6. 常见的位运算技巧

6.1 设置与检查位

  • 设置位state |= (1 << i) 将第 i 位设为 1

  • 检查位(state >> i) & 1 检查第 i 位是否为 1

  • 清除位state &= ~(1 << i) 将第 i 位设为 0

6.2 示例:动态规划中的位运算

位运算在很多图论问题和动态规划问题中都非常高效。通过对状态进行位操作,可以快速地进行状态转换和检查。

6.2.1问题描述

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

import java.util.*;
public class Main{public static void main(String[] args){Scanner scan = new Scanner(System.in);int N = 20, M = 1 << N;int[][] f = new int[M][N]; // f[state][j]: state状态下最后到达点j的最短路径int[][] w = new int[N][N]; // 权重矩阵 w[i][j]: 点i到点j的距离int n = scan.nextInt(); // 点的个数// 读取图的邻接矩阵for(int i = 0 ; i < n ; i ++ )for(int j = 0 ; j < n ; j ++ )w[i][j] = scan.nextInt(); // 初始化动态规划数组,设置为正无穷for(int i = 0 ; i < 1 << n ; i ++ )Arrays.fill(f[i],0x3f3f3f); f[1][0] = 0; // 起点是顶点0,状态为只访问了顶点0// 枚举所有的状态for(int state = 0 ; state < 1 << n ; state ++ ){for(int j = 0 ; j < n ; j ++ ){// 判断当前状态下是否访问过顶点 jif((state >> j & 1) == 1){ for(int k = 0 ; k < n ; k ++ ){// 判断倒数第二步是否访问过顶点 kif((state - (1 << j) >> k & 1) == 1){// 状态转移f[state][j] = Math.min(f[state][j], f[state - (1 << j)][k] + w[k][j]);}}}}}// 输出最终结果,表示从顶点0开始访问所有顶点到终点n-1的最短路径System.out.println(f[(1 << n) - 1][n - 1]);}
}

 详细题解请移步:最短Hamilton路径

7. 总结与扩展

记忆化搜索结合状态压缩是一种极为高效的优化技术,特别是在解决具有重叠子问题和复杂状态空间的问题时,能够显著提升算法的时间和空间效率。在实际应用中,合理地设计状态表示并结合位运算,可以进一步优化问题的求解过程。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java数组05:Arrays类

本节内容视频链接&#xff1a;Java数组07&#xff1a;Arrays类讲解_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p57&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 Java中的‌Array类是一个针对数组进行操作的工具类&#xff0c;‌提供了排序、‌…

算法_字符串专题---持续更新

文章目录 前言最长公共前缀题目要求题目解析代码如下 最长回文子串题目要求题目解析代码如下 二进制求和题目要求题目解析 字符串相乘题目要求题目解析代码如下 前言 本文将会向你介绍有关字符串的相关题目&#xff1a;最长公共前缀、最长回文子串、二进制求和、字符串相乘。本…

GDB的基本使用(1)

我有话说 因为时间和精力原因&#xff0c;本文写的虎头蛇尾了&#xff0c;除了启动调试与程序执行以外只有少量截图演示&#xff0c;只是简单的说明。如果有需要可以联系我&#xff0c;我有时间的话会把演示补上&#xff0c;谢谢理解。 启动调试与程序执行 启动调试并传递参数…

linux环境下通过源码编译的方式安装mysql8.0.16版本

文章目录 前言一、资源准备1.源码下载2.依赖命令安装2.1 安装依赖包2.2 安装高版本gcc2.3 安装高版本cmake 二、编译安装mysql1.解压mysql-boost-8.0.16安装包2.执行编译命令3.执行make命令4.执行make install 命令5.编译安装完成后结果检查 三、初始化mysql1. 创建数据相关目录…

在ubuntu16.04下使用词典工具GoldenDict

前言 本来要装有道词典&#xff0c;结果发现各种问题&#xff0c;放弃。 网上看大家对GoldenDict评价比较高&#xff0c;决定安装GoldenDict 。 安装 启动 添加词库 GoldenDict本身并不带词库&#xff0c;需要查词的话&#xff0c;必须先下载离线词库或者配置在线翻译网址才…

SQL每日一练-0816

今日SQL题&#xff1a;计算每个项目的年度收入增长率 难度系数&#xff1a;&#x1f31f;☆☆☆☆☆☆☆☆☆ 1、题目要求 计算每个项目每年的收入总额&#xff0c;并计算项目收入环比增长率。找出每年收入增长率最高的项目。输出结果显示年份、项目ID、项目名称、项…

OD C卷 - 查找一个有向网络的头节点和尾节点

查找一个有向网络的头节点和尾节点 &#xff08;200&#xff09; 在一个有向图中&#xff0c;有向边用两个整数表示&#xff0c;第一个整数表示起始节点&#xff0c;第二个整数表示终止节点&#xff1b;图中只有一个头节点&#xff0c;一个或者多个尾节点&#xff1b;图可能存…

RTX 40全系10款显卡《黑神化:悟空》测试:打开DLSS3帧生成 性能直翻4倍

一、前言&#xff1a;《黑神话&#xff1a;悟空》临近发布 RTX 40系显卡表现如何&#xff1f; 2020年8月20日&#xff0c;游戏科学发布了《黑神话&#xff1a;悟空》的首个实机演示预告&#xff0c;惊艳了整个游戏行业&#xff01; 以往&#xff0c;很多人认为国产开发商做不出…

华为数通方向HCIP-DataCom H12-821题库(更新单选真题:1-10)

第1题 1、下面是一台路由器的部分配置,关于该配置描述正确的是? [HUAWEllact number 2001 [HUAWEl-acl-basic-2001]rule 0 permit source 1.1.1.1 0 [HUAWEl-acl-basic-2001]rule 1 deny source 1.1.1.0 0 [HUAWEl-acl-basic-2001]rule

Java实现STL中的全排列函数next_permutation()

目录 一、引言 二、全排列函数next_permutation() 三、next_permutation()的使用 四、Java实现next_permutation() 五、使用next_permutation()实现全排列 一、引言 相信很多小伙伴们都做过全排列的算法题&#xff0c;输入一个n&#xff0c;输出1~n的全排列。对于这个问题…

jemeter压力测试入门

1. 安装jemeter的压缩包并且解压 点击运行 2. 添加线程组 3. 线程组的参数设置 4. 添加http请求 5. 填写请求信息 添加监听器——结果树&#xff08;结果&#xff09;&#xff0c;聚合报告&#xff08;吞吐量报告&#xff09; 6. 通过cvs数据文件设置&#xff0c;配置元件&…

ARM 裸机与 Linux 驱动对比及 Linux 内核入门

目录 ARM裸机代码和驱动的区别 Linux系统组成 内核五大功能 设备驱动分类 内核类型 驱动模块 驱动模块示例 Makefile配置 命令 编码辅助工具 内核中的打印函数 printk 函数 修改打印级别 ​编辑 打印级别含义 驱动多文件编译 示例 模块传递参数 命令行传递参数…

jmeter简单发送接口

一、安装jmeter 拥有java环境&#xff0c;再下载jmeter 安装之后解压到本地&#xff0c;jmeter中的bin目录配置到环境变量中 之后可以通过cmd中 jmeter.bat命令运行 二、利用jmeter发送接口请求 1、添加线程组 添加->线程->线程组 2、添加http请求 添加->取样器-&g…

利用Matlab求解高阶微分方程(ode45)

1、高阶微分方程的基本概念 二阶以及二阶以上的微分方程称之为高阶微分方程&#xff0c;一般来说&#xff0c;微分方程的阶数越高&#xff0c;求解的难度也就越大。求高阶方程的一个常用方法就是降低阶数。对二阶方程 &#xff0c;如果能用变量代换把它化成一阶方程&#xff0c…

学习记录——day33 HTTP

目录 一、HTTP相关概念 二、客服端请求 1、请求首部 2、 响应首部 三、线程实现HTTP并发服务器 一、HTTP相关概念 1、HTTP&#xff0c;全称Hyper Text Transfer Protocol&#xff0c;用于万维网&#xff08;world wide web&#xff09;进行超文本学习的传输协议 2、HTTP属…

计算xpclr

1.conda安装xpclr 首先安装流程很轻松 conda create -n xpclr -c bioconda xpclr conda activate xpclr xpclr -h 2.按照要求准备文件 XPCLR - 简书 (jianshu.com) 根据教程准备文件&#xff0c;vcf&#xff0c;计算好的map&#xff0c;以及样本文件txt 其实官网也有介绍…

Docker基础概述、Docker安装、Docker镜像加速、Docker镜像指令

1.为什么学docker 开发环境与测试环境不同&#xff0c;导致错误 因此docker提供解决方法———系统平滑移植&#xff0c;容器虚拟化技术 将代码与软件与配置文件 打包成一个镜像 2.docker的历练 创建一个开发环境内成为镜像文件再用docker使用镜像 3.什么是docker Docke…

MySQL5.7数据库---入门教程(小白教程)

一、MySQL安装 本文以MySQL5.7安装为例。在设置完root密码和添加一个用户后&#xff0c;一路默认。 1、 2、通过点击红圈里的箭头选择对应的版本。 3、 4、端口&#xff08;Port&#xff09;一般默认不需要更改。 5、 二、配置环境变量 配置环境变量可以方便在win系统中cmd…

流媒体服务器二 3学习 librtmp 库的配置使用

librtmp 库是个啥&#xff1f; librtmp是一个开源的基于C语言的库&#xff0c;提供了一个连接RTMP服务器&#xff0c;发送和接收RTMP流的API。 它可以用来开发流媒体播放器&#xff0c;网络直播等应用。它的主要特点是快速、稳定和低延迟。 librtmp支持RTMP&#xff0c;RTMPS…

Qt第十七章 多线程

文章目录 多线程1. 线程概念的起源2. 三种方式创建线程3. 启动线程前的准备工作4. 启动线程/退出线程5. 操作运行中的线程6. 为每个线程提供独立数据7.子线程不能操作ui解决方案 多线程 1. 线程概念的起源 单核CPU 早期还没有线程的概念&#xff0c;如何保证2个进程同时进行呢…