《算法题讲解指南:动态规划算法--子数组系列》--25.单词拆分,26.环绕字符串中唯一的子字符串

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

25.单词拆分

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

C++算法代码(解法一:不借助辅助结点):

C++算法代码(解法二:借助辅助结点):

算法总结及流程解析:

​编辑

26.环绕字符串中唯一的子字符串

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

C++算法代码:

算法总结及流程解析:

结束语


25.单词拆分

题目链接:

139. 单词拆分 - 力扣(LeetCode)

题目描述:

题目示例:

解法(动态规划):

算法思路:

1.状态表示:

对于线性dp,我们可以用「经验+ 题目要求」来定义状态表示:

i.以某个位置为结尾,巴拉巴拉;

ii.以某个位置为起点,巴拉巴拉。

这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:

dp[i]表示:[0,i]区间内的字符串,能否被字典中的单词拼接而成。

2.状态转移方程:

对于dp[i],为了确定当前的字符串能否由字典里面的单词构成,根据最后一个单词的起始位置j,我们可以将其分解为前后两部分:

i.前面一部分[0,j-1]区间的字符串;

ii.后面一部分[j,i]区间的字符串。

其中前面部分我们可以在dp[j-1]中找到答案,后面部分的子串可以在字典里面找到。

因此,我们得出一个结论:当我们在从o~i枚举j的时候,只要dp[j-1]=true并且后面部分的子串s.substr(j,i-j+1)能够在字典中找到,那么dp[i]=true。

3.初始化:

可以在最前面加上一个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:

i.辅助结点里面的值要「保证后续填表是正确的」;

ii.「下标的映射关系」。

在本题中,最前面加上一个格子,并且让dp[0]=true ,可以理解为空串能够拼接而成。

其中为了方便处理下标的映射关系,我们可以将字符串前面加上一个占位符s=' '+s ,这样就没有下标的映射关系的问题了,同时还能处理「空串」的情况。

4.填表顺序:

显而易见,填表顺序「从左往右」。

5.返回值:

由「状态表示」可得:返回dp[n]位置的布尔值。

哈希表优化的小细节:

在状态转移中,我们需要判断后面部分的子串「是否在字典」之中,因此会「频繁的用到查询操作」。为了节省效率,我们可以提前把「字典中的单词」存入到「哈希表」中。

C++算法代码(解法一:不借助辅助结点):

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) 
    {
        //为了后续获取子字符串能快速判断是否在字典中
        //我们可以通过哈希表将字典中的字符串进行存放
        unordered_map<string, int> hash;
        for(int i = 0; i < wordDict.size(); i++)
        {
            hash[wordDict[i]]++;
        }    

        //解法一:不借助辅助结点
        int n = s.size();
        vector<bool> dp(n);
        dp[0] = hash.count(s.substr(0, 1));

        for(int i = 1; i < n; i++)
        {
            for(int j = i; j >= 1; j--)
            {
                //dp[j - 1]存在的情况
                if(dp[j - 1] && hash.count(s.substr(j, i - j + 1)))
                {
                    dp[i] = true;
                    break;
                }
            }
            //dp[j - 1]不存在的情况,即整个[0, i]的字符串是否存在于字典中
            if(hash.count(s.substr(0, i + 1)))
            {
                dp[i] = true;
            }
        }
        return dp[n - 1];
    }
};

C++算法代码(解法二:借助辅助结点):

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) 
    {
        //为了后续获取子字符串能快速判断是否在字典中
        //我们可以通过哈希表将字典中的字符串进行存放
        unordered_map<string, int> hash;
        for(int i = 0; i < wordDict.size(); i++)
        {
            hash[wordDict[i]]++;
        }    

        //解法二:借助辅助结点
        int n = s.size();
        vector<bool> dp(n + 1);
        dp[0] = true; //保证后续填表是正确的
        s = ' ' + s; //在字符串开头位置加上空格,使原始字符串下标统一+1
                     //为了和dp数组下标进行对应
        for(int i = 1; i < n + 1; i++)
        {
            for(int j = i; j >= 1; j--)
            {
                if(dp[j - 1] && hash.count(s.substr(j, i - j + 1)))
                {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

算法总结及流程解析:

26.环绕字符串中唯一的子字符串

题目链接:

467. 环绕字符串中唯一的子字符串 - 力扣(LeetCode)

题目描述:

题目示例:

解法(动态规划):

算法思路:

1.状态表示:

对于线性dp,我们可以用「经验+题目要求」来定义状态表示:

i.以某个位置为结尾,巴拉巴拉;

ii.以某个位置为起点,巴拉巴拉。

这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:

dp[i]表示:以i位置的元素为结尾的所有子串里面,有多少个在base中出现过。

2.状态转移方程:

对于dp[i],我们可以根据子串的「长度」划分为两类:

i.子串的长度等于1:此时这一个字符会出现在base中;

ii.子串的长度大于1:如果i位置的字符和i-1 位置上的字符组合后,出现在 base中的话,那么dp[i-1]里面的所有子串后面填上一个s[i]依旧在base中出现。因此dp[i]=dp[i-1]。

综上,dp[i]= 1 + dp[i-1],其中dp[i-1]是否加上需要先做一下判断。

3.初始化:

可以根据「实际情况」,将表里面的值都初始化为1 。

4.填表顺序:

显而易见,填表顺序「从左往右」。

5.返回值:

这里不能直接返回dp表里面的和,因为会有重复的结果。在返回之前,我们需要先「去重」:

i.相同字符结尾的dp值,我们仅需保留「最大」的即可,其余dp值对应的子串都可以在最大的里面找到;

ii.可以创建一个大小为26的数组,统计所有字符结尾的最大dp值。

最后返回「数组中所有元素的和」即可。

C++算法代码:

cpp 复制代码
class Solution {
public:
    int findSubstringInWraproundString(string s) 
    {
        int n = s.size();
        vector<int> dp(n);
        int hash[26] = { 0 };
        //hash数组存放的值表示为:相应字符为结点的最大的dp值(作用:去除重复子字符串)
        dp[0] = 1;
        hash[s[0] - 'a'] = 1;

        for(int i = 1; i < n; i++)
        {
            if(s[i - 1] + 1 == s[i] || (s[i - 1] == 'z' && s[i] == 'a'))
            {
                dp[i] = dp[i - 1] + 1;
            }
            else
            {
                dp[i] = 1;
            }
            if(dp[i] > hash[s[i] - 'a'])
            {
                //如果当前位置字符为结尾的dp值大于hash对应字符所在位置的值
                //说明hash中存放的dp值(以该字符为结尾的所有子字符串情况)对于这一次而言是全部重复的
                hash[s[i] - 'a'] = dp[i];
            }
        }   
        int ret = 0;
        for(int i = 0; i < 26; i++)
        {
            ret += hash[i];
        }
        return ret;
    }
};

算法总结及流程解析:

结束语

到此,25.单词拆分,26.环绕字符串中唯一的子字符串 这两道算法题就讲解完了。**单词拆分问题:通过定义dp[i]表示前i个字符能否由字典单词组成,利用哈希表存储字典,通过状态转移判断子串是否存在于字典中。给出了两种实现方案:不使用辅助节点和使用辅助节点优化下标处理;环绕字符串子串问题:定义dp[i]为以i结尾且在base中出现过的子串数量,通过字符连续性判断进行状态转移,并使用哈希数组去重,最终统计所有字符结尾的最大dp值之和。**希望大家能有所收获!

相关推荐
CappuccinoRose18 分钟前
回溯法 - 软考备战(四十三)
算法·排列组合·路径·n皇后·子集·解数独·岛屿
AC赳赳老秦22 分钟前
OpenClaw进阶技巧:批量修改文件内容、替换关键词,解放双手
java·linux·人工智能·python·算法·测试用例·openclaw
CoderCodingNo1 小时前
【信奥业余科普】C++ 的奇妙之旅 | 12:程序的交互与加工——数据的输入与算术运算
开发语言·c++
yx868xy1 小时前
Cuda加速直线拟合
c++·cuda
Robot_Nav1 小时前
Shape-Aware MPPI(SA MPPI)算法:基于RC-ESDF的任意形状机器人实时轨迹优化
算法·机器人·sa-mppi
蜗牛在听雨1 小时前
基于 C++ 的 UG/NX 二次开发环境配置
c++·二次开发·ug
SimpleLearingAI2 小时前
C++虚函数详解
开发语言·c++
踩坑记录2 小时前
leetcode hot100 118. 杨辉三角 easy 动态规划
leetcode·动态规划
小O的算法实验室2 小时前
2026年ESWA,自适应基于排序的协同进化学习粒子群算法+边缘计算服务器部署,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
小柯博客2 小时前
STM32MP2安全启动技术深度解析
c语言·c++·stm32·嵌入式硬件·安全·开源·github