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

🔥小叶-duck个人主页

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

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


目录

25.单词拆分

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

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

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

算法总结及流程解析:

​编辑

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

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

C++算法代码:

算法总结及流程解析:

结束语


25.单词拆分

题目链接:

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

题目描述:

题目示例:

解法(动态规划):

算法思路:

1.状态表示:

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

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

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

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

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

2.状态转移方程:

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

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

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

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

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

3.初始化:

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

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

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

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

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

4.填表顺序:

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

5.返回值:

由「状态表示」可得:返回dpn位置的布尔值。

哈希表优化的小细节:

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

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.以某个位置为起点,巴拉巴拉。

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

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

2.状态转移方程:

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

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

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

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

相关推荐
CodeSheep程序羊几秒前
宇树科技,即将上市!
java·c语言·c++·人工智能·python·科技·硬件工程
无限码力9 分钟前
阿里算法岗 0530笔试真题 - 寻找满足条件的最优子序列
算法·阿里笔试真题·阿里机试真题·阿里算法岗笔试真题·阿里算法题
@小阿宝15 分钟前
机器人正向逆向运动学
算法·机器人
小雨下雨的雨17 分钟前
数独算法与求解器鸿蒙PC Electron框架完成深度解析
javascript·人工智能·算法·游戏·华为·electron·鸿蒙系统
HZ·湘怡20 分钟前
数据结构之排序算法 (1)--插入排序
c语言·数据结构·算法·排序算法
ouliten20 分钟前
[Triton笔记7]融合注意力 (Fused Attention)
人工智能·笔记·算法
开源Z20 分钟前
LeetCode 238 · 除自身以外数组的乘积:左右两遍扫描,不用除法
算法·leetcode
雪落漂泊28 分钟前
C++ 继承与多态(下)
开发语言·c++
charlie11451419130 分钟前
通用GUI编程技术——图形渲染实战(四十九)——完全自绘控件架构:状态机与动画
c++·windows·架构·图形渲染
BAGAE31 分钟前
FEC-RS前向纠错编码理论及工程实施研究
c语言·c++·qt·算法·决策树·链表