算法训练第五十七天 | 647. 回文子串、516.最长回文子序列、动态规划总结篇

news2024/12/23 17:03:52

动态规划part17

  • 647. 回文子串
    • 题目描述
    • 思路
      • 暴力解法
      • 动态规划
      • 双指针法
  • 516.最长回文子序列
    • 题目描述
    • 思路
  • 动态规划总结篇
    • 动划基础
    • 背包问题系列
    • 打家劫舍系列
    • 股票系列
    • 子序列系列
    • 总结

647. 回文子串

题目链接:647. 回文子串
参考:https://programmercarl.com/0647.%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2.html

题目描述

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:“abc”
  • 输出:3
  • 解释:三个回文子串: “a”, “b”, “c”

示例 2:

  • 输入:“aaa”
  • 输出:6
  • 解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”

提示:输入的字符串长度不会超过 1000 。

思路

暴力解法

两层for循环,遍历区间起始位置和终止位置,然后还需要一层遍历判断这个区间是不是回文。所以时间复杂度:O(n^3)

动态规划

动规五部曲:

  1. 确定dp数组(dp table)以及下标的含义

如果大家做了很多这种子序列相关的题目,在定义dp数组的时候 很自然就会想题目求什么,我们就如何定义dp数组。

绝大多数题目确实是这样,不过本题如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系。

dp[i] 和 dp[i-1] ,dp[i + 1] 看上去都没啥关系。

所以我们要看回文串的性质。 如图:
在这里插入图片描述
我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。

那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。

所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。

布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

  1. 确定递推公式

在确定递推公式时,就要分析如下几种情况。

整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。

当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。

当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况

  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

以上三种情况分析完了,那么递归公式如下:

if (s[i] == s[j]) {
    if (j - i <= 1) { // 情况一 和 情况二
        result++;
        dp[i][j] = true;
    } else if (dp[i + 1][j - 1]) { // 情况三
        result++;
        dp[i][j] = true;
    }
}

result就是统计回文子串的数量。

注意这里我没有列出当s[i]与s[j]不相等的时候,因为在下面dp[i][j]初始化的时候,就初始为false。

  1. dp数组如何初始化

dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。

所以dp[i][j]初始化为false。

  1. 确定遍历顺序

遍历顺序可有有点讲究了。

首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
在这里插入图片描述
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。

有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。

代码如下:

for (int i = s.size() - 1; i >= 0; i--) {  // 注意遍历顺序
    for (int j = i; j < s.size(); j++) {
        if (s[i] == s[j]) {
            if (j - i <= 1) { // 情况一 和 情况二
                result++;
                dp[i][j] = true;
            } else if (dp[i + 1][j - 1]) { // 情况三
                result++;
                dp[i][j] = true;
            }
        }
    }
}
  1. 举例推导dp数组

举例,输入:“aaa”,dp[i][j]状态如下:
在这里插入图片描述
图中有6个true,所以就是有6个回文子串。

注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分。

以上分析完毕,C++代码如下:

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {  // 注意遍历顺序
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) { // 情况一 和 情况二
                        result++;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) { // 情况三
                        result++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return result;
    }
};

以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下:

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
                    result++;
                    dp[i][j] = true;
                }
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

双指针法

动态规划的空间复杂度是偏高的,我们再看一下双指针法。

首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。

在遍历中心点的时候,要注意中心点有两种情况

一个元素可以作为中心点,两个元素也可以作为中心点。

那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。

所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。

这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算,代码如下:

class Solution {
public:
    int countSubstrings(string s) {
        int result = 0;
        for (int i = 0; i < s.size(); i++) {
            result += extend(s, i, i, s.size()); // 以i为中心
            result += extend(s, i, i + 1, s.size()); // 以i和i+1为中心
        }
        return result;
    }
    int extend(const string& s, int i, int j, int n) {
        int res = 0;
        while (i >= 0 && j < n && s[i] == s[j]) {
            i--;
            j++;
            res++;
        }
        return res;
    }
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

516.最长回文子序列

题目链接:516.最长回文子序列
参考:https://programmercarl.com/0516.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E5%BA%8F%E5%88%97.html

题目描述

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: “bbbab” 输出: 4 一个可能的最长回文子序列为 “bbbb”。

示例 2: 输入:“cbbd” 输出: 2 一个可能的最长回文子序列为 “bb”。

提示:

  • 1 <= s.length <= 1000
  • s 只包含小写英文字母

思路

我们刚刚做过了 动态规划:回文子串 ,求的是回文子串,而本题要求的是回文子序列, 要搞清楚这两者之间的区别。

回文子串是要连续的,回文子序列可不是连续的! 回文子串,回文子序列都是动态规划经典题目。

回文子串,可以做这两题:

647.回文子串
5.最长回文子串

思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点。

动规五部曲分析如下:

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。

  1. 确定递推公式
    在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。

如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;

如图:
在这里插入图片描述
(如果这里看不懂,回忆一下dp[i][j]的定义)

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

加入s[j]的回文子序列长度为dp[i + 1][j]。

加入s[i]的回文子序列长度为dp[i][j - 1]。

那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
在这里插入图片描述
代码如下:

if (s[i] == s[j]) {
    dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
  1. dp数组如何初始化

首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。

所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。

其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。

vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
  1. 确定遍历顺序

从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图:
在这里插入图片描述
所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的。

j的话,可以正常从左向右遍历。

代码如下:

for (int i = s.size() - 1; i >= 0; i--) {
    for (int j = i + 1; j < s.size(); j++) {
        if (s[i] == s[j]) {
            dp[i][j] = dp[i + 1][j - 1] + 2;
        } else {
            dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
        }
    }
}
  1. 举例推导dp数组

输入s:“cbbd” 为例,dp数组状态如图:
在这里插入图片描述
红色框即:dp[0][s.size() - 1]; 为最终结果。

以上分析完毕,C++代码如下:

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][s.size() - 1];
    }
};

动态规划总结篇

参考:https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%80%BB%E7%BB%93%E7%AF%87.html
动规五部曲分别为:

  • 确定dp数组(dp table)以及下标的含义
  • 确定递推公式
  • dp数组如何初始化
  • 确定遍历顺序
  • 举例推导dp数组

动划基础

背包问题系列

在这里插入图片描述

打家劫舍系列

股票系列

在这里插入图片描述

子序列系列

在这里插入图片描述

总结

在这里插入图片描述

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

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

相关文章

hot100:数组——31、33

31. 下一个排列 思路&#xff1a;其实这道题的意思就是&#xff0c;简单地说&#xff0c;就是找到一个比现有的给出的数组代表的值大的最小的数 比如给出的数组是[1,2,3]&#xff0c;它代表的数值是123&#xff0c;现有的元素组成的数值中&#xff0c;比123大的有很多&#xf…

3.6 n维随机变量

学习目标&#xff1a; 学习n维随机变量需要掌握一定的数学知识&#xff0c;包括多元微积分、线性代数和概率论等。要学习n维随机变量&#xff0c;我会采取以下步骤&#xff1a; 复习相关的数学知识&#xff1a;首先&#xff0c;我会复习多元微积分、线性代数和概率论的基本知…

OpenCV介绍与GUI特征(一)

目录0.1 OpenCV-Python教程简介OpenCVOpenCV-PythonOpenCV-Python教程OpenCV需要你!!!贡献者0.2 在Windows中安装OpenCV-Python目标从预制的二进制文件中安装OpenCV从源代码构建OpenCV练习0.3 在Ubuntu中安装OpenCV-Python目标从预制的二进制文件中安装OpenCV-Python从源码构建…

Revit怎么绘制结构梁?一键生成梁?

绘制结构梁是Revit基础的功能&#xff0c;对于不少刚接触Revit的小伙伴来说似乎还无从下手&#xff0c;今天就让小编来告诉大家在Revit中绘制结构梁的方法。 一、Revit中结构梁图文绘制过程 首先&#xff0c;我们选择“结构”选项卡中的“梁”工具&#xff0c;点击选择梁的类…

android12 displayArea学习

一&#xff1a;数据结构分析 1&#xff1a;android 12 WindowContainer 的类继承关系如下 下图为 WindowContainer 简要的对象图。 下图是 Aosp默认的display层次结构对象图。 Aosp定义的feature有如下 FEATURE_ROOT 0; FEATURE_DEFAULT_TASK_CONTAINER 1; FEATURE_WINDOW_…

DNS服务器 - 理论

DNS服务器1. 概念2. DNS域名结构3. 域名的分级4. 域名服务器5. 域名解析过程5.1 递归查询与迭代查询5.2 解析流程1. 迭代查询2. 递归查询6. 高速缓存&#xff1a;7. 加上主机缓存后的DNS解析流程8. 常见的域名解析记录9. DNS正向解析和反向解析1. 概念 DNS服务器&#xff08;D…

C++指针与其它复合类型

目录 前言&#xff1a; 1.指针与字符串 1.1cout接收char类型的地址的反应 1.2字符串字面值 1.3字符串备份 2.使用new创建动态结构 3.使用new和delete搭配存储键盘输入的字符串 前言&#xff1a; 指针我们已经知道如何使用了&#xff0c;也知道指针和数组配合起来使用&am…

DHCP及中继(UOS)

DHCP服务器 中继器 客户端 服务器 安装DHCP apt install isc-dhcp-server -y 编辑配置文件 vim /etc/dhcp/dhcpd.conf 重启服务 systemctl restart isc-dhcp-server 配置监听网卡 vim /etc/default/isc-dhcp-server 中继器 安装dhcp yum install dhcp -y nmtui 修改…

【LeetCode每日一题: 1042. 不邻接植花 | 图论 | 染色问题】

&#x1f34e;作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域新星创作者&#x1f3c6;&#xff0c;保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享&#x1f48e;&#x1f48e;&#x1f48e; &#x1f34e;座右…

Qt中调用C#制作的com组件

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 这里记录一下在 Qt 64位程序中调用 C# 制作的 com 组件的流程&#xff0c;方便后期自己回顾。 1. 了解 TLB 格式 拿到的依赖库最…

hbase进阶操作——读流程与写流程介绍

系列文章目录 centos7虚拟机下hbase的使用案例讲解 文章目录 系列文章目录 一、hbase架构原理 1.1、StoreFile 1.2、MemStore 1.3、WAL 二、hbase的写流程 2.1、写流程的流程图 2.2、写流程的流程图说明 三、hbase读流程 3.1、读流程的流程图 3.2、读流程的流程图解…

C/C++|物联网开发入门+项目实战|指针|嵌入式C语言高级|C语言内存空间的使用-学习笔记(9)

文章目录2-3 : C语言内存空间的使用指针概述示例&#xff1a;指针修饰符指针运算符示例示例1多级指针例子省略argc&#xff08;个数&#xff09;的常用写法&#xff1a;参考&#xff1a; 麦子学院-嵌入式C语言高级-内存空间2-3 : C语言内存空间的使用 指针概述 内存类型资源地…

CPU工作原理

CPU&#xff08;中央处理器&#xff09;是计算机中的重要组件&#xff0c;它负责执行计算机程序中的指令。在了解CPU的工作原理之前&#xff0c;我们需要先了解一些基本概念。 指令和指令集 指令是计算机程序中的基本单位&#xff0c;它指示计算机执行某个操作。指令集是一组…

我在“Now In Android”中学到的 9 件事

我在“Now In Android”中学到的 9 件事 Now in Android是一款功能齐全的 Android 应用程序&#xff0c;完全使用 Kotlin 和 Jetpack Compose 构建。它遵循 Android 设计和开发最佳实践&#xff0c;旨在为开发人员提供有用的参考。 https://github.com/android/nowinandroid UI…

淘宝悄悄内测“店号一体”的新模式

4月17日消息&#xff0c;淘宝近日已开始小规模测试“店号一体”新模式。新模式下&#xff0c;淘宝店铺将与逛逛、直播等账号完全打通&#xff1b;此前针对达人及内容型商家的新店铺模式“视频内容店”也同步升级。 内测商家透露&#xff0c;目前在淘宝发布的图文、短视频、直播…

腾讯学长分享的这份Java面试八股文手册,让我GitHub下载量破百万!!!

一些不满现状&#xff0c;被外界的“高薪”“好福利”吸引的人&#xff0c;一般就在这时候毅然决然地跳槽了。 跳槽是为了寻求更好的发展&#xff0c;但在跳槽前我们也不能确定下家就是更好的归宿&#xff0c;这就更加需要我们审慎地去对待&#xff0c;不能盲目跳槽。 其次&a…

知识图谱专栏简介:数据增强,智能标注,文本信息抽取(实体关系事件抽取)、知识融合算法方案、知识推理、模型优化、模型压缩技术等

知识图谱专栏简介&#xff1a;数据增强&#xff0c;智能标注&#xff0c;文本信息抽取&#xff08;实体关系事件抽取&#xff09;、知识融合算法方案、知识推理、模型优化、模型压缩技术等 专栏链接&#xff1a;NLP知识图谱相关技术业务落地方案和码源 NLP知识图谱相关技术业…

程序员跳槽薪水涨了一倍,谈谈java工程师找新工作的八大技巧

大家好&#xff0c;这几天发生了一些事情&#xff0c;我找到了一份新工作&#xff0c;明天是第一天上班。我想先谈一下我的新工作待遇&#xff0c;因为我觉得相对来说还算比较满意。接下来我想谈一下我的个人经历&#xff0c;从毕业到现在的工作经历。第三个话题是我最近半个月…

k-d Tree算法

1.概述 本文介绍一种用于高维空间中的快速最近邻和近似最近邻查找技术——Kd- Tree(Kd树)。Kd-Tree&#xff0c;即K-dimensional tree&#xff0c;是一种高维索引树形数据结构&#xff0c;常用于在大规模的高维数据空间进行最近邻查找(Nearest Neighbor)和近似最近邻查找(Appro…

Java工程行业管理系统源码-专业的工程管理软件-提供一站式服务

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示1…