动态规划——子序列问题

文章目录

目录

文章目录

前言

一、动态规划思路简介

二、具体实现

1. 字符串问题

1.1 最长公共字符串

1.2.0 最长回文子串

1.2.1 最长回文子序列

2.编辑距离问题

2.1 判断子序列

2.2 编辑距离

总结


前言

上文提到动态规划的背包问题,本文继续介绍动态规划的另一种应用:子序列问题。子序列分为间断子序列与连续子序列,其中诸多问题例如最大递增子序列、最长回文串等等,都可以用动态规划的思路解决


一、动态规划思路简介

动态规划解题的关键有两步。

第一步是找出题目中量的递推关系式。动态规划的代码实现是依据递推关系展开的,找出递推关系是最关键,也是最先要处理的一件事情。

第二步是依据递推关系明确dp数组的下标含义,并进行正确的初始化。

接下来我们从一些具体问题看一下如何使用动态规划思路解决问题。

二、具体实现

1. 字符串问题

1.1 最长公共字符串

请编写一个 C 语言程序来查找两个字符串的最长公共子串(由字符串中连续的一段字符组成的字符串)

输入两个字符串str1和str2, 字符串长度范围均为[0, 200)。

输出两个字符串的最长公共子串,如果不存在公共子串,则输出“没有公共子串”;如果有多个最长公共子串,则输出str1中从左至右第一个最长公共子串。

示例输入

abbbcc
accd

示例输出

cc

解题思路:先寻找有没有递推关系式。观察发现,此类最长问题,一般有 “当前位”的最长公共字符串长度等于“前一位”的最长公共字符串长度加一。

由递推关系可知,遍历数组的方向应该从前往后遍历,并有dp[i]=dp[i-1]+1,此处i表示在字符串中的位置,dp[i]表示以第i位为结尾的公共字符串长度。

具体代码

#include<stdio.h>
#include<string.h>
int main(){
	char a[300]={0},b[300]={0};
	scanf("%s%s",a+1,b+1);
//	dp[i][j]表示a的i位,b的j位相同的情况下 从前向后数公共字串的长度
	int dp[300][300]={0};
//初始化为0的原因在于默认字符不相等,在遍历时检测是否相等,若相等则加一
	int alen=strlen(a+1),blen=strlen(b+1);
	int maxlen=0,maxLocation=1;
	for(int i=1;i<=alen;i++){
		for(int j=1;j<=blen;j++){
			if(a[i]==b[j]){
				dp[i][j]=dp[i-1][j-1]+1;
				if(maxlen<dp[i][j]){
					maxlen=dp[i][j];
					maxLocation=i-maxlen+1;//找起始位置 
				}
			}
		}
	}
	for(int i=0;i<maxlen;i++){
		printf("%c",a[maxLocation+i]);
	}
	return 0;
}

1.2.0 最长回文子串

5. 最长回文子串 - 力扣(LeetCode)

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

解题思路:观察回文串规律,发现回文串有如下特征:若回文串字符数为奇数,则以第i位为中心向左右检索,如果左右边缘的字符相等,则回文串字符数加二。若回文串字符为偶数,则以第i位与第 i+1 位为中心向左右检索。

设dp1[i]与dp2[i],前者表示在第i位向前数有多少个回文字符,dp2同理。

代码样例

char* longestPalindrome(char* s){
    int dp1[1000]={0},dp2[1000]={0};
	int len=strlen(s);
    for(int i=0;i<len;i++){
        //dp1[i]表示对i加上中心后一半的长度
        while(i-dp1[i]>=0&&i+dp1[i]<len){
        	if(s[i-dp1[i]]!=s[i+dp1[i]])
            	break;
            dp1[i]++;
		}
        if(s[i]==s[i+1]&&i+1<len){
            while(i-dp2[i]>=0&&i+1+dp2[i]<len){
            	if(s[i-dp2[i]]!=s[i+1+dp2[i]])
            		break;
				dp2[i]++;
			}
        }
    }
    int max=2*dp1[1]-1,maxlocation=0;
    for(int i=0;i<len;i++){
    	dp1[i]=2*dp1[i]-1;
    	dp2[i]=2*dp2[i];
        if(max<(dp1[i]>dp2[i]?dp1[i]:dp2[i])){
        	max=dp1[i]>dp2[i]?dp1[i]:dp2[i];
        	maxlocation=i+1-(max+1)/2;
		}
	}
    char*a=(char*)malloc(1010);
	strncpy(a,s+maxlocation,max);
    a[max]=0; 
    return a;
}

1.2.1 最长回文子序列

516. 最长回文子序列 - 力扣(LeetCode)

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

解题思路:回文数组规律一般可由中间向两边推知。故知,对于 i 到 j 的子序列而言,若s [ i ] = s [ j ] 。其最大回文子序列数必定是dp[ i +1] [ j -1 ]+2。若s [ i ]不等于s [ j ],那么考虑加入左侧元素或右侧元素后能否使子序列数增加,即比较放入左侧元素与放入右侧元素后的最大子序列数。

if(s[left]==s[right]){
                dp[left][right]=dp[left+1][right-1]+2;
            }else{
                int max=dp[left+1][right]>dp[left][right-1]?dp[left+1][right]:dp[left][right-1];
                dp[left][right]=dp[left+1][right-1];
            }

在动态规划解题过程中,我们只考虑按遍历顺序下的当前步未知,前面已遍历过的dp应该已知,并不再考虑它们是怎么计算得出的。因此对遍历顺序有较强的要求。

对于dp[ i ][ j ],i 表示左边界,j 表示右边界,dp[ i ][ j ]表示最大序列数。因为计算dp[ i ][ j ]需要

dp[ i+1][ j ]   dp[ i+1 ][ j-1]   dp[ i ][ j-1],所以遍历顺序应该i递减,j递增,从而保持完备性。

示例代码:

int longestPalindromeSubseq(char* s) {
    int dp[1010][1010]={0};
    //表示从i到j的最长回文序列
    int len=strlen(s);
    for(int i=0;i<len;i++)
        dp[i][i]=1;
    for(int left=len-1;left>=0;left--){
        for(int right=left+1;right<len;right++){
            if(s[left]==s[right]){
                dp[left][right]=dp[left+1][right-1]+2;
            }else{
                int max=dp[left+1][right]>dp[left][right-1]?dp[left+1][right]:dp[left][right-1];
                dp[left][right]=max;
            }
        }
    }
    return dp[0][len-1];
}

2.编辑距离问题

2.1 判断子序列

392. 判断子序列 - 力扣(LeetCode)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

示例1

输入:s = "abc", t = "ahbgdc"
输出:true

示例2

输入:s = "axc", t = "ahbgdc"
输出:false

解题思路f[i][j] 存储了从位置 i 开始,字符 j + 'a' 在字符串 t 中的下一个出现位置。首先初始化 f 数组并填充,之后遍历 s 中的每个字符,在 f 数组中找到对应字符的下一个出现位置并更新。如果在任何时刻无法找到字符,返回 false,否则返回 true

具体代码

bool isSubsequence(char* s, char* t) {
    int n = strlen(s), m = strlen(t);

    int f[m + 1][26];
    memset(f, 0, sizeof(f));
    for (int i = 0; i < 26; i++) {
        f[m][i] = m;
    }

    for (int i = m - 1; i >= 0; i--) {
        for (int j = 0; j < 26; j++) {
            if (t[i] == j + 'a')
                f[i][j] = i;
            else
                f[i][j] = f[i + 1][j];
        }
    }
    int add = 0;
    for (int i = 0; i < n; i++) {
        if (f[add][s[i] - 'a'] == m) {
            return false;
        }
        add = f[add][s[i] - 'a'] + 1;
    }
    return true;
}

2.2 编辑距离

72. 编辑距离 - 力扣(LeetCode)

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

示例1

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例2 

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

解题思路

定义一个 dp 数组,dp[i][j] 表示将 word1[0..i-1] 转换为 word2[0..j-1] 的最小编辑距离。然后初始化 dp 数组的第一行和第一列,分别代表从空字符串到其他字符串的编辑距离。

对每一对字符 (word1[i-1], word2[j-1]),如果相等,dp[i][j] = dp[i-1][j-1],否则考虑三种操作(删除、插入、替换),选择最小值。

最终结果在 dp[n][m],其中 nm 分别是 word1word2 的长度。

具体代码

int minDistance(char* word1, char* word2) {
    int n = strlen(word1), m = strlen(word2);
    int dp[n+1][m+1];
    // 初始化边界条件
    for (int i = 0; i <= n; i++) dp[i][0] = i;
    for (int j = 0; j <= m; j++) dp[0][j] = j;
    // 填充 dp 数组
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            int cost = (word1[i-1] == word2[j-1]) ? 0 : 1;
            dp[i][j] = fmin(fmin(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1] + cost);
        }
    }
    return dp[n][m];
}


总结

动态规划在子序列问题中应用广泛,关键是找出递推关系并设计合适的 dp 数组。通过精妙的状态转移,可以高效解决如最长公共子串、回文子序列及编辑距离等问题。

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

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

相关文章

群控系统服务端开发模式-应用开发-短信工厂七牛云短信开发

一、七牛云短信工厂开发 1、添加框架对应的SDK composer require qiniu/php-sdk 2、添加七牛云工厂 在根目录下extend文件夹下Sms文件夹下channel文件夹下&#xff0c;创建七牛云短信发送工厂并命名为QiniuyunSmsSender。记住&#xff0c;一定要在七牛云短信发送工厂类名后面去…

机器学习概述,特征工程简述2.1——2.3

机器学习概述&#xff1a; 1.1人工智能概述 达特茅斯会议—人工智能的起点 机器学习是人工智能的一个实现途径 深度学习是机器学习的一个方法发展而来 1.1.2 机器学习和深度学习能做什么 传统预测 图像识别 自然语言处理 1.2什么是机器学习 数据 模型 预测 从历史数…

基于vite6+ vue3 + electron@33 实现的 局域网内互传文件的桌面软件

目录 项目介绍项目部分截图介绍下基础项目搭建先搭建一个vite 前端项目 再安装 electron 相关依赖依赖安装失败解决方案修改 vite配置文件和 ts 配置文件修改packjsonts相关配置项目结构介绍 项目介绍 前端 基于 vue3 ts windicss 后端 就是node 层 项目地址&#xff1a; h…

Linux 内核系统架构

Linux 内核是一个复杂且高度模块化的系统&#xff0c;负责操作硬件资源、管理进程和内存、提供网络服务、执行文件系统操作、进行设备驱动程序的管理等。它为用户空间提供了一个抽象层&#xff0c;并为应用程序提供了底层服务。本文将深入探讨 Linux 内核的系统架构&#xff0c…

TYUT设计模式精华版

七大原则 单一职责原则 职责要单一不能将太多的职责放在一个类中 开闭原则 软件实体对扩展是开放的&#xff0c;但对修改是关闭的 里氏代换原则 一个可以接受基类对象的地方必然可以接受子类 依赖倒转原则 要针对抽象层编程&#xff0c;而不要针对具体类编程 接口隔离原则 …

计算机网络——不同版本的 HTTP 协议

介绍 HTTP&#xff0c;即超文本传输协议&#xff08;HyperText Transfer Protocol&#xff09;&#xff0c;是应用层的一个简单的请求-响应协议&#xff0c;它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。本文将介绍 HTTP 协议各个版本。 HTTP/1.0 HTTP/1…

Fastapi + vue3 自动化测试平台---移动端App自动化篇

概述 好久写文章了&#xff0c;专注于新框架&#xff0c;新UI界面的实践&#xff0c;废话不多说&#xff0c;开搞 技术架构 后端&#xff1a; Fastapi Airtest multiprocessing 前端&#xff1a; 基于 Vue3、Vite、TypeScript、Pinia、Pinia持久化插件、Unocss 和 Elemen…

FreeRTOS之ARM CR5栈结构操作示意图

FreeRTOS之ARM CR5栈结构操作示意图 1 FreeRTOS源码下载地址2 ARM CR5栈结构操作宏和接口2.1 portSAVE_CONTEXT宏2.1.1 portSAVE_CONTEXT源码2.1.2 portSAVE_CONTEXT宏操作栈结构变化示意图 2.2 portRESTORE_CONTEXT宏2.2.1 portRESTORE_CONTEXT源码2.2.2 portRESTORE_CONTEXT宏…

警惕开源信息成为泄密源头

文章目录 前言一、信息公开需谨慎1、警惕采购招标泄密。2、警惕信息公开泄密。3、警惕社交媒体泄密。 二、泄密风险需严防1、健全制度&#xff0c;明确责任。2、加强管控&#xff0c;严格审查。3、提高意识&#xff0c;谨言慎行。 前言 大数据时代&#xff0c;信息在网络空间发…

指针(上)

目录 内存和地址 指针变量和地址 取地址&#xff08;&&#xff09; 解引用&#xff08;*&#xff09; 大小 类型 意义 const修饰 修饰变量 修饰指针 指针运算 指针- 整数 指针-指针 指针的关系运算 野指针 概念 成因 避免 assert断言 指针的使用 strl…

常见的数据结构---队列、树与堆的深入剖析

目录 一、队列 二、树 三、堆 在现代计算机科学与工程领域&#xff0c;队列、树和堆是三种极其重要的基础数据结构&#xff0c;它们各自具有独特的特点和应用。在日常开发中&#xff0c;合理选择和使用这些数据结构可以显著提高程序的效率和可维护性。它们不仅奠定了算法设计…

Java 整合图片处理相关一套:传输、保存、重命名、删除、AI图片文字识别、图片映射、vue3裁剪、设置黑白色、设置负片、提高照片品质

目录 一、vue3 axios spring boot文件传输 二、Java图片保存到本地 三、Java 本地图片重命名 四、Java 删除本地图片 五、 JavaAI图片文字识别 六、Java映射图片地址&#xff0c;前端直接访问图片 七、vue3 图片裁剪 八、Java 设置图片黑白色 九、Java 设置图片负片 …

web三、 window对象,延时器,定时器,时间戳,location对象(地址),本地存储-localStorage,数组去重new Set

一、 window对象 window对象 是一个全局对象&#xff0c;也可以说是JavaScript中的 顶级对象 像document、alert()、console.log()这些都是window的属性&#xff0c;基本BOM的属性和方法都是window的 所有通过 var定义 在全局作用域中的 变量 、 函数 都会变成window对象的属…

【AI系统】昇腾异构计算架构 CANN

昇腾异构计算架构 CANN 本文将介绍昇腾 AI 异构计算架构 CANN&#xff08;Compute Architecture for Neural Networks&#xff09;&#xff0c;这是一套为高性能神经网络计算需求专门设计和优化的架构。CANN 包括硬件层面的达芬奇架构和软件层面的全栈支持&#xff0c;旨在提供…

Spark和MapReduce场景应用和区别

文章目录 Spark和MapReduce场景应用和区别一、引言二、MapReduce和Spark的应用场景1. MapReduce的应用场景2. Spark的应用场景 三、MapReduce和Spark的区别1. 内存使用和性能2. 编程模型和易用性3. 实时计算支持 四、使用示例1. MapReduce代码示例2. Spark代码示例 五、总结 Sp…

CSS函数

目录 一、背景 二、函数的概念 1. var()函数 2、calc()函数 三、总结 一、背景 今天我们就来说一说&#xff0c;常用的两个css自定义属性&#xff0c;也称为css函数。本文中就成为css函数。先来看一下官方对其的定义。 自定义属性&#xff08;有时候也被称作CSS 变量或者级…

【C语言】递归的内存占用过程

递归 递归是函数调用自身的一种编程技术。在C语言中&#xff0c;递归的实现会占用内存栈&#xff08;Call Stack&#xff09;&#xff0c;每次递归调用都会在栈上分配一个新的 “栈帧&#xff08;Stack Frame&#xff09;”&#xff0c;用于存储本次调用的函数局部变量、返回地…

大数据新视界 -- 大数据大厂之 Hive 数据压缩算法对比与选择(下)(20 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

【golang】单元测试,以及出现undefined时的解决方案

单元测试 要对某一方法进行测试时&#xff0c;例如如下这一简单减法函数&#xff0c;选中函数名后右键->转到->测试 1&#xff09;Empty test file 就是一个空文件&#xff0c;我们可以自己写测试的逻辑 但是直接点绿色箭头运行会出问题&#xff1a; 找不到包。我们要在…

ETL工具观察:ETLCloud与MDM是什么关系?

一、什么是ETLCloud ETLCloud数据中台是一款高时效的数据集成平台&#xff0c;专注于解决大数据量和高合规要求环境下的数据集成需求。 工具特点 1.离线与实时集成&#xff1a;支持离线数据集成&#xff08;ETL、ELT&#xff09;和变更数据捕获&#xff08;CDC&#xff09;实…