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

相关推荐
To_OC6 小时前
LC 131 分割回文串:刚学回溯时,我连怎么切字符串都想不明白
javascript·算法·leetcode
炸膛坦客7 小时前
单片机/C/C++八股:(二十六)IIC 专题(I²C)---- 上集
c语言·c++·单片机
旖-旎7 小时前
LeetCode 518:零钱兑换||(完全背包)—— 题解
c++·算法·leetcode·动态规划·背包问题
To_OC7 小时前
LC 42 接雨水:暴力超时卡半天?前后缀数组一用就通了
javascript·算法·leetcode
Henry Zhu1237 小时前
C++中的特殊成员函数与智能指针
c++
delishcomcn8 小时前
AI视觉识别+分切算法:电化铝缺陷检测与裁切一体化解锁
人工智能·算法
触底反弹8 小时前
深入理解大模型采样:Temperature、Top-K、Top-P 的原理与实战
人工智能·算法·面试
雪碧聊技术9 小时前
力扣 LCR 091. 粉刷房子 —— 动态规划入门详解
算法·动态规划
_wyt00110 小时前
多重背包问题详解
c++·背包dp