当前位置: 首页 > article >正文

动态规划算法:字符串类问题(2)公共串

0 前言

上节课我们已经讲述了使用动态规划求取回文串长度与数量的方法(和本节课关系不大,感兴趣可以去看字符串类问题(1)回文串),这节课我们继续探索字符串问题中的动态规划问题。

进入本篇文章前,需要给大家普及两个概念:
(1)子串/子数组:从原字符串上截取的一段连续的字符串
(2)子序列:可以删除掉中间某些元素,不改变剩余元素相对位置的子集合

1、公共子串

1.1 题目描述

最长重复子数组 - 力扣
给两个整数数组 nums1nums2 ,返回两个数组中公共的、长度最长的子数组的长度 。

1.2 题目分析

读题,发现这道题要求我们求公共子数组,或者说是公共子串(后面我会混着说,大家别混淆),要求连续。
说实话,这道题即使用暴力解法,代码量也不小,思路是把nums1的所有可能的子数组放入unordered_set中,再把nums2所有可能的子数组找出来在哈希表中查询。如果题目要求我们输出最长子串内的每个元素,那我们只能这么做。但是这道题只要求我们输出长度。
这道题属于双字符串动态规划的基础问题,没做过很难想出来,除非你是天才,因此我把dp数组的思考过程直接给大家。

1.2.1 dp数组的定义:

一般来说,涉及到两个字符串的递归,我们需要设计二维dp数组。
这里定义dp[i][j]为:在nums1的索引[0, i]nums2的索引[0, j]内寻找公共子串,公共子串的最后一个元素必须与nums1[i]nums2[j]相等,子串的长度为dp[i][j]
就是把nums1nums2先分割开,只看前半部分,寻找内部可能的最大公共子串。

假设

nums1 = {0, 1, 2, 3};
nums2 = {1, 2, 3};

dp[2][1]就表示在nums1[0:2] = {0, 1, 2}nums2[0:1] = {1, 2}中找公共子串,同时要求子串的最后一个元素必须与nums1[2] = 2nums2[1] = 2相等,肉眼观察可以得到 dp[2][1] = 2;

dp[1][2]表示在nums1[0:1] = {0, 1}nums2[0:2] = {1, 2, 3}中找公共子串,同时要求子串的最后一个元素必须与nums1[1] = 1nums2[2] = 3相等。这是一个矛盾的命题,不可能满足,因此dp[1][2] = 0;

dp数组的递推公式:

if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 0;

else {...}中的情况刚刚解释过,而当nums1[i] == nums2[j]时证明我们最长的公共子串长度可以在之前的基础上再加1,请大家结合下面的图片仔细理解一下这句话。
在这里插入图片描述

1.2.2 dp数组初始化:

可以看到递推方向是左上到右下,因此我们需要初始化的元素有:
在这里插入图片描述
对于dp[0][j]来说,当nums2[j] = nums1[0]时,对应的dp[0][j]需要置1。
对于dp[i][0]来说,当nums1[i] = nums2[0]时,对应的dp[i][0]需要置1。
这里其实很好理解,假设初始化dp[0][j]时,结合dp定义这个时候nums1只有一个元素nums1[0]在区间内,因此当出现nums2[j] = nums1[0],就证明最大子数组长度为1。

1.3 示例代码

1.3.1 标准方法

下面就是参考代码。

class Solution {
public:int findLength(vector<int>& nums1, vector<int>& nums2) {// dp初始化vector<vector<int>> dp(nums1.size(), vector<int>(nums2.size(), 0));int ret = 0;	// 定义一个计数器,初始时 ret=0for(int j = 0; j < nums2.size(); j++) {if(nums1[0] == nums2[j]) { dp[0][j] = 1;ret = 1;	// 至少有一个元素相等}}for(int i = 0; i < nums1.size(); i++) {if(nums2[0] == nums1[i]) {dp[i][0] = 1;ret = 1;	// // 至少有一个元素相等}}// 递推for(int i = 1; i<nums1.size(); i++) {for(int j = 1; j < nums2.size(); j++) {if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1] + 1;ret = std::max(ret, dp[i][j]);}}return ret;}
};

注意:
(1)这道题返回值看似不是dp,但是实际上ret就是在记录dp中的最大值,因此这个题目中dp与结果是强相关的。
(2)这道题在初始化dp时就把所有元素默认为0,因此在递推时不需要写else分支。

1.3.2 简化初始化流程

在上面的代码中我们发现初始化是一个比较麻烦的事情,对于做过动态规划的朋友们都会有一个经验,就是把dp数组的大小设计的比数据量多一个,暨n个数据,dp数组则设计为n+1。通过这种方式可以简化dp初始化步骤。、
看下面的代码:

class Solution {
public:int findLength(vector<int>& nums1, vector<int>& nums2) {vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));int ret = 0;for(int i = 1; i<=nums1.size(); i++) {for(int j = 1; j <= nums2.size(); j++) {if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;ret = std::max(ret, dp[i][j]);}}return ret;}
};

这里我们给dp数组多留出来了一个位置,保证dp数组左上方向全部为0,这样我们就不需要对边界初始化了。
在这里插入图片描述
同步修改一下dp的定义:
这里定义dp[i][j]为:在nums1的索引[0, i-1]nums2的索引[0, j-1]内寻找公共子串,公共子串的最后一个元素必须与nums1[i-1]nums2[j-1]相等,子串的长度为dp[i][j]

其余的推导全部一致,大家一定要自己写一遍,后面我都会用这种简化的方法。

2、公共子序列

2.1 题目描述

最长公共子序列 - 力扣
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0 。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列,因为c与e的相对顺序发生改变。
两个字符串的公共子序是这两个字符串所共同拥有的子序列。

2.2 题目分析

读题,发现这道题要求我们求公共子序列,不要求连续。
这道题即使用暴力解法,思路是通过回溯把text1的所有可能的子序列放入unordered_set中,再把text2所有可能的子序列找出来在哈希表中查询。如果题目要求我们输出最长子串内的每个元素,那我们只能这么做。但是这道题只要求我们输出长度。

2.2.1 dp数组的定义

我刚刚说过,两个字符串的动规,需要二维dp数组。
定义dp[i][j]为:text1在[0, i-1]范围内,text2在[0, j-1]范围内的最大公共子序列长度。

dp的递推关系为:

if(text1[i-1] == text2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = std::max(dp[i-1][j], dp[i][j-1];

看过最长回文子序列那篇文章的同学对这个应该不陌生。这里解释一下:
(1)当text1[i-1] == text2[j-1]时,公共子序列长度应该在之前的记录的值的基础上加1,之前的值是什么呢,当然是dp[i-1][j-1]了。
(2)当text1[i-1] != text2[j-1]时,证明text1[i-1] 与text2[j-1]元素不同,公共子序列的长度应该为之前记录的最大值,但是这种情况下这里之前记录的最大值是什么呢?
有同学直接默认为dp[i-1][j-1],这是表示text1[i-2] == text2[j-2],但是我们不妨是靠一下,虽然text1[i-1] != text2[j-1],但是有没有可能存在text1[i-2] == text2[j-1]或者text1[i-1] == text2[j-2]的情况呢?所以说我们寻找的最大值应该是dp[i-1][j]与dp[i][j-1]中更大的那一个。

**注意:**对于字符串问题的dp数组定义方式有两种,这两种定义方式的区别是:是否明确要求以对应索引元素结尾。结题时很难直接确定使用哪一种方式,还是要靠经验,大家不妨也按照另一种方式设计一下:
定义dp[i][j]为:text1在[0, i-1]范围内,text2在[0, j-1]范围内的最大公共子序列长度,且最长公共子序列必须以text1[i-1]与text2[j-1]所标识的共同元素结尾
这种定义下,dp的递推公式为:

if(text1[i-1] == text2[j-1]) dp[i][j] = func(i-1, j-1) + 1;
else dp[i][j] = 0;

我们需要一个dp数组在[0 … i-1] and [0 … j-1]范围内的最大值,用函数func来返回。
这么一看这种方法更加困难。

2.2.2 dp数组的初始化

如何初始化我在上文的公共子串中讲述的很明白了,我们扩充了dp数组的大小,实现了简化的初始化步骤。这里我就不再赘述了。

1.3 示例代码

class Solution {
public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>> dp(text2.size()+1, vector<int>(text1.size()+1, 0));// int ret = 0; 这种dp定义方式下不需要记录最大值,dp本身就是最大值for(int i = 1; i <= text2.size(); i++) {for(int j = 1; j <= text1.size(); j++) {if(text1[j-1] == text2[i-1]) dp[i][j] = dp[i-1][j-1] + 1;else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);// ret = std::max(ret, dp[i][j]);}}return dp.back().back();}
};

碎碎念:
我刚刚给大家说了dp数组的定义问题,我们在最终结果输出时,可以明确看出两种方式的区别,在当前我们定义的模式下,最右下角的元素就是我们要的结果。
而在另一种定义方式下(强制要求尾部元素),我们需要在dp数组内寻找最大值。
这里没有什么好办法来进行很好的区分,需要大家结合经验来判断。

3 拓展

3.1 回文串长度的另一种解法

题目链接:最长回文子序列 - 力扣

不知道这道题的可以去看我之前的文章:动态规划算法:字符串类问题(1)回文串

我在这篇文章里说到,设计到两个字符串的动态规划问题都需要二维dp数组。对于回文串,我们仅在一个字符串上进行判断,却仍然使用了二维dp数组,不知道大家有没有一些思考。
回文串的定义是正着读与反着读都一样的字符串,所以我们是在两个方向上判断一个字符串,所以其实还是两个字符串,所以dp数组也要用二维。

结合上面的文字描述,大家很有可能想出一个诡异的做法。回文串的定义是正着读与反着读都一样的字符串
我们将字符串逆序去找公共子序列,是不是可以从另一个方向上解决这个问题呢?

直接给代码:

class Solution {int findLength(string& nums1, string& nums2) {vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));int ret = 0;for(int i = 1; i<=nums1.size(); i++) {for(int j = 1; j <= nums2.size(); j++) {if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);ret = std::max(ret, dp[i][j]);}}return ret;}public:int longestPalindromeSubseq(string& s) {string s2 = s;reverse(s2.begin(), s2.end());return findLength(s, s2);}
};

这里直接逆序字符串,并去找最长公共子序列,上面的私有化方法就是我们这篇文章第二部分讲的公共子序列的函数接口。

4 小结

这篇文章里我们继续拓展了字符串类的动态规划问题。
我们进一步加深了二维dp数组在字符串问题中的使用,并总结了一些规律。
我们对回文子序列长度提出了另一种解法。
这类问题会涉及到一些变种,由于篇幅原因我会在下一篇文章中进行讲解。
感谢大家的阅读,希望大家有所收获。

相关文章:

动态规划算法:字符串类问题(2)公共串

0 前言 上节课我们已经讲述了使用动态规划求取回文串长度与数量的方法&#xff08;和本节课关系不大&#xff0c;感兴趣可以去看字符串类问题&#xff08;1&#xff09;回文串&#xff09;&#xff0c;这节课我们继续探索字符串问题中的动态规划问题。 进入本篇文章前&#x…...

uni-app(5):Vue3语法基础上

Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统&#xff0c;只关注视图层&#xff0c;…...

深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化,全面掌握性能优化核心技巧

深度解析Vue项目Webpack打包分包策略 从基础配置到高级优化&#xff0c;全面掌握性能优化核心技巧 一、分包核心价值与基本原理 1.1 为什么需要分包 首屏加载优化&#xff1a;减少主包体积&#xff0c;提升TTI&#xff08;Time to Interactive&#xff09;缓存利用率提升&am…...

ubuntu下docker安装mongodb-支持单副本集

1.mogodb支持事务的前提 1) MongoDB 版本&#xff1a;确保 MongoDB 版本大于或等于 4.0&#xff0c;因为事务支持是在 4.0 版本中引入的。 2) 副本集配置&#xff1a;MongoDB 必须以副本集&#xff08;Replica Set&#xff09;模式运行&#xff0c;即使是单节点副本集&#x…...

spring-boot-starter-data-redis应用详解

一、依赖引入与基础配置 添加依赖 在 pom.xml 中引入 Spring Data Redis 的 Starter 依赖&#xff0c;默认使用 Lettuce 客户端&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis<…...

5060显卡驱动PyCUDA开发环境搭建

5060显卡驱动PyCUDA开发环境搭建 本文手把手讲解了RTX5060ti显卡从上手尝试折腾&#xff0c;到在最新Ubuntu LTS版本上CUDA开发环境搭建成功的详细流程。 1.1 开机后Ubuntu2404LTS不识别显卡 1.1.1 显卡硬件规格要求 笔者下单的铭瑄电竞之心&#xff0c;安装规格是PCIe …...

redis搭建最小的集群,3主3从

create.sh脚本用于快速部署一个Docker化的Redis集群。首先&#xff0c;脚本创建了一个自定义的Docker网络redis-net&#xff0c;并指定了子网以防止IP变动。接着&#xff0c;脚本设置了宿主机的公网IP&#xff0c;并生成了六个Redis节点的配置文件&#xff0c;每个配置文件都启…...

《帝国时代1》游戏秘籍

资源类 PEPPERONI PIZZA&#xff1a;获得 1000 食物。COINAGE&#xff1a;获得 1000 金。WOODSTOCK&#xff1a;获得 1000 木头。QUARRY&#xff1a;获得 1000 石头。 建筑与生产类 STEROIDS&#xff1a;快速建筑。 地图类 REVEAL MAP&#xff1a;显示所有地图。NO FOG&#xf…...

【sylar-webserver】10 HTTP模块

HTTP 解析 这里使用 nodejs/http-parser 提供的 HTTP 解析器。 HTTP 常量定义 HttpMethod HttpStatus /* Request Methods */ #define HTTP_METHOD_MAP(XX) \XX(0, DELETE, DELETE) \XX(1, GET, GET) \XX(2, HEAD, HEAD) …...

攻略生成模块

攻略生成模块 这个模块实现了一个旅行行程规划服务&#xff0c;主要流程如下&#xff1a; 核心思路是通过前端传来的城市和出游天数信息&#xff0c;先在本地数据库中查找是否已存有相应的旅游数据&#xff08;例如景点、美食等&#xff09;&#xff0c;如果没有就自动检索和…...

海康NVR录像回放SDK原始流转FLV视频流:基于Java的流媒体转码(无需安装第三方插件ffmpeg)

wlinker-video-monitor 代码地址&#xff1a;https://gitee.com/wlinker/wlinker-video-monitor 背景与需求 在安防监控、智能楼宇等场景中&#xff0c;海康威视设备作为行业主流硬件&#xff0c;常需要将录像回放功能集成到Web系统中。然而&#xff0c;海康设备的原始视频流…...

深入理解设计模式:工厂模式、单例模式

深入理解设计模式&#xff1a;工厂模式、单例模式 设计模式是软件开发中解决常见问题的可复用方案。本文将详细介绍两种种重要的创建型设计模式&#xff1a;工厂模式、单例模式&#xff0c;并提供Java实现示例。 一、工厂模式 工厂模式是一种创建对象的设计模式&#xff0c;…...

运维Linux之Ansible详解学习(更新中)

什么是Ansible Ansible 是一款新出现的自动化运维工具&#xff0c;基于 Python 开发。以下是对它的详细介绍&#xff1a; 功能特点&#xff1a;集合了众多运维工具的优点&#xff0c;能实现批量系统配置、批量程序部署、批量运行命令等功能。它是基于模块工作的&#xff0c;本…...

深入浅出IIC协议 - 从总线原理到FPGA实战开发 -- 第三篇:Verilog实现I2C Master核

第三篇&#xff1a;Verilog实现I2C Master核 副标题 &#xff1a;从零构建工业级I2C控制器——代码逐行解析与仿真实战 1. 架构设计 1.1 模块分层设计 三层架构 &#xff1a; 层级功能描述关键信号PHY层物理信号驱动与采样sda_oe, scl_oe控制层协议状态机与数据流控制state…...

网络世界的“变色龙“:动态IP如何重构你的数据旅程?

在深秋的下午调试代码时&#xff0c;我偶然发现服务器日志中出现异常登录记录——IP地址显示为某个境外数据中心。更有趣的是&#xff0c;当我切换到公司VPN后&#xff0c;这个"可疑IP"竟自动消失在了防火墙监控列表中。这个瞬间让我意识到&#xff1a;现代网络架构中…...

进阶-自定义类型(结构体、位段、枚举、联合)

自定义类型&#xff1a;结构体&#xff0c;枚举&#xff0c;联合 结构体 结构体类型的声明 结构的自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 结构体实现位段(位段的填充&可移植性) 枚举 枚举类型的定义 枚举的优点 枚举的使用 联合 联合类型的定义 联…...

5G 网络全场景注册方式深度解析:从信令交互到报文分析

摘要 本文全面梳理 5G 网络包含的初始注册、移动性注册更新、紧急注册、周期性注册更新、服务请求触发注册、切换触发注册、基于策略的注册更新等多种注册方式。详细阐述每种注册方式的触发条件、信令流程、关键报文结构,结合对比分析与实际案例,助力读者深入理解 5G 网络接…...

ARM笔记-嵌入式系统基础

第一章 嵌入式系统基础 1.1嵌入式系统简介 1.1.1嵌入式系统定义 嵌入式系统定义&#xff1a; 嵌入式系统是以应用为中心&#xff0c;以计算机技术为基础&#xff0c;软硬件可剪裁&#xff0c;对功能、可靠性、成本、体积、功耗等有严格要求的专用计算机系统 ------Any devic…...

一文讲透golang channel 的特点、原理及使用场景

在 Go 语言中&#xff0c;通道&#xff08;Channel&#xff09; 是实现并发编程的核心机制之一&#xff0c;基于 CSP&#xff08;Communicating Sequential Processes&#xff09; 模型设计。它不仅用于协程&#xff08;Goroutine&#xff09;之间的数据传递&#xff0c;还通过…...

upload-labs通关笔记-第19关文件上传之条件竞争

系列目录 upload-labs通关笔记-第1关 文件上传之前端绕过&#xff08;3种渗透方法&#xff09; upload-labs通关笔记-第2关 文件上传之MIME绕过-CSDN博客 upload-labs通关笔记-第3关 文件上传之黑名单绕过-CSDN博客 upload-labs通关笔记-第4关 文件上传之.htacess绕过-CSDN…...

第5章:任务间通信机制(IPC)全解析

💬 在多线程开发中,线程之间如何协作?如何让一个线程产生数据,另一个线程消费数据?本章聚焦 Zephyr 提供的多种任务间通信机制(IPC)及实战使用技巧。 📚 本章导读 你将学到: Zephyr 提供的常用 IPC 接口:FIFO、消息队列、邮箱、信号量 每种机制适用场景和用法对比…...

CAPL自动化-诊断Demo工程

文章目录 前言一、诊断控制面板二、诊断定义三、发送诊断通过类.方法的方式req.SetParameterdiagSetParameter四、SendRequestAndWaitForResponse前言 本文将介绍CANoe的诊断自动化测试,工程可以从CANoe的 Sample Configruration 界面打开,也可以参考下面的路径中打开(以实…...

SVN被锁定解决svn is already locked

今天遇到一个问题&#xff0c;svn 在提交代码的时候出现了svn is already locked&#xff0c;解决方案...

【深度学习】1. 感知器,MLP, 梯度下降,激活函数,反向传播,链式法则

一、感知机 对于分类问题&#xff0c;我们设定一个映射&#xff0c;将x通过函数f(x)映射到y 1. 感知机的基本结构 感知机&#xff08;Perceptron&#xff09;是最早期的神经网络模型&#xff0c;由 Rosenblatt 在 1958 年提出&#xff0c;是现代神经网络和深度学习模型的雏形…...

云原生安全:网络协议TCP详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 &#xff08;注&#xff1a;文末附可视化流程图与专有名词说明表&#xff09; 1. 基础概念 TCP&#xff08;Transmission Control Protocol&#xff09;是…...

使用CentOS部署本地DeekSeek

一、查看服务器的操作系统版本 cat /etc/centos-release二、下载并安装ollama 1、ollama下载地址&#xff1a; Releases ollama/ollama GitHubGet up and running with Llama 3.3, DeepSeek-R1, Phi-4, Gemma 3, Mistral Small 3.1 and other large language models. - Re…...

Spring Boot与Eventuate Tram整合:构建可靠的事件驱动型分布式事务

精心整理了最新的面试资料和简历模板&#xff0c;有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 一、引言 在现代微服务架构中&#xff0c;分布式事务管理一直是复杂系统中的核心挑战之一。传统的两阶段提交&#xff08;2PC&#xff09;方案存在性能瓶颈&…...

Python:从脚本语言到工业级应用的传奇进化

一、Python的诞生:一场喜剧与编程的奇妙相遇 1989年的冬天,荷兰程序员Guido van Rossum在阿姆斯特丹的CWI研究所里,用一段独特的代码开启了编程语言的新纪元。这个被命名为"Python"的项目,灵感并非源自冷血的蟒蛇,而是源于Guido对英国喜剧团体Monty Python的痴…...

【排序算法】典型排序算法 Java实现

以下是典型的排序算法分类及对应的 Java 实现&#xff0c;包含时间复杂度、稳定性说明和核心代码示例&#xff1a; 一、比较类排序&#xff08;通过元素比较&#xff09; 1. 交换排序 ① 冒泡排序 时间复杂度&#xff1a;O(n)&#xff08;优化后最优O(n)&#xff09; 稳定性&…...

node.js如何实现双 Token + Cookie 存储 + 无感刷新机制

node.js如何实现双 Token Cookie 存储 无感刷新机制 为什么要实施双token机制&#xff1f; 优点描述安全性Access Token 短期有效&#xff0c;降低泄露风险&#xff1b;Refresh Token 权限受限&#xff0c;仅用于获取新 Token用户体验用户无需频繁重新登录&#xff0c;Toke…...