《算法题讲解指南:动态规划算法--子数组系列》--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值之和。**希望大家能有所收获!

相关推荐
Fcy6488 小时前
算法基础详解(二)枚举算法——普通枚举与二进制枚举
算法·枚举算法
承渊政道8 小时前
【优选算法】(实战:栈、队列、优先级队列高频考题通关全解)
数据结构·c++·笔记·学习·算法·leetcode·宽度优先
py有趣8 小时前
力扣热门100题之将有序数组转为二叉搜索树
算法·leetcode
天若有情6738 小时前
Python精神折磨系列(完整11集·无断层版)
数据库·python·算法
凌波粒8 小时前
LeetCode--383.赎金信(哈希表)
java·算法·leetcode·散列表
liulilittle8 小时前
OPENPPP2 1.0.0.26145 正式版发布:内核态 SYSNAT 性能飞跃 + Windows 平台避坑指南
开发语言·网络·c++·windows·通信·vrrp
AIminminHu8 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(2):当你的CAD代码变得“又大又乱”:从手动编译到CMake,从随性编码到单元测试))
c++·单元测试·cmake·cad·cad开发
xiaoye-duck8 小时前
《算法题讲解指南:动态规划算法--子数组系列》--23.等差数列划分,24.最长湍流子数组
c++·算法·动态规划
消失的旧时光-19438 小时前
C++ 网络服务端主线:从线程池到 Reactor 的完整路线图
开发语言·网络·c++·线程池·并发