惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
小众软件
小众软件
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
AI
AI
美团技术团队
人人都是产品经理
人人都是产品经理
S
Secure Thoughts
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
宝玉的分享
宝玉的分享
Security Latest
Security Latest
P
Privacy & Cybersecurity Law Blog
C
Cisco Blogs
大猫的无限游戏
大猫的无限游戏
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 最新话题
罗磊的独立博客
Recent Announcements
Recent Announcements
H
Hacker News: Front Page
博客园 - 【当耐特】
K
Kaspersky official blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
Schneier on Security
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
Google DeepMind News
Google DeepMind News
V
V2EX
博客园 - 聂微东
量子位
云风的 BLOG
云风的 BLOG
C
Check Point Blog
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
V2EX - 技术
V2EX - 技术
Vercel News
Vercel News
L
LINUX DO - 热门话题
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
The GitHub Blog
The GitHub Blog

潮汐朝夕

Ad-Hoc问题:在字符串中删改字符 leetcode第一题,两数之和 频繁查询子串是否回文:Manacher预处理后$O(1)$响应 状态转移方程描述最优子结构 | 优化状态表示 | 单调队列优化DP 树形 DP | 剪枝优化 | 移位操作 滑动窗口 | 按位单独处理 | 字符计数 通过模拟解决算法问题:导论 Python的生成器对象与 filter 对象 按位单独处理的技巧 滑动窗口 | 满足条件的最短的子串 滑动窗口字符计数的优化:增加维护聚合信息应对高频查询 滑动窗口 | 满足条件的子串数目 最少硬币组合问题2:贪心算法【附算法导论习题答案下载】 力扣2274-不含特殊楼层的最大连续楼层数 最少硬币组合问题1:完全背包 带余除法 | 算法式证明 | 进制转换 2025年的内容与栏目建设 概率方法 | Cr不等式 | r阶绝对矩 Weierstrass不等式的另一边 | Bernoulli不等式的推广
频繁查询子串是否回文:区间DP
Cheng Zhaoxi · 2025-03-01 · via 潮汐朝夕

字数统计: 1.2k字   |   阅读时长: 4分

摘要: 频繁查询子串是否回文,用区间DP预处理


【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings

在上一篇文章中,我们解决了分割回文串的问题,给定字符串 s,将其分为若干个回文子串,由于需要返回所有可能的分割方案,因此只能用回溯法。

本文我们解决分割回文串的第二个问题,还是将字符串 s 分割为若干回文子串,但只需要返回最少分割次数,这是一个优化的问题,可以考虑动态规划。

频繁查询子串是否回文:Manacher预处理后O(1)响应 中的情况一样,本题也是需要频繁查询子串是否是回文,处理方法也一样,先预处理一个二维数组 cc[i][j] 表示子串 s[i..j] 是否是回文。

如果用 Manacher 算法预处理,用其中间结果的 p 数组来相应查询,可以 $O(n)$ 时间完成预处理,更直观的预处理方法是区间 DP,这也是在涉及到回文的问题中重用的算法,可以在 $O(N^{2})$ 的时间完成预处理。

如果算法在预处理外的部分的时间复杂度低于 $O(N^{2})$,将预处理的算法改为 Manacher 算法比较有意义,否则用区间 DP 处理也行,不影响整体的时间复杂度。

题目

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的 最少分割次数 。

提示:

1
2
1 <= s.length <= 2000
s 仅由小写英文字母组成

示例 1:
输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,”b”] 这样两个回文子串。

示例 2:
输入:s = “a”
输出:0

示例 3:
输入:s = “ab”
输出:1

题解

算法:动态规划

定义 $dp[i]$ 表示前缀 s[0..i] 分割为回文子串最少可分成的部分数,比如如果前缀 s[0..2] = aab 最少可以分成 aab 2 个部分,于是 dp[2] = 2。这样定义的话 dp[n-1] + 1 就是最终答案。

初始值是前缀本身是回文的情况,即如果 s[0..i] 是回文,则 dp[i] = 1。比如单个字符的前缀 s[0] 本身就是回文,因此最少可分为 1 个部分,所以 dp[0] = 1

接下来考虑状态转移方程。假设现在正在计算 dp[i],如果 s[0..i] 是回文,则为初始值,直接取 dp[i] = 1 即可。否则进行以下推导。

考虑 $j = 1,\cdots,i$,若 s[j..i] 为回文,则可以将 s[j..i] 分割取出形成 1 个部分,之后要考虑剩下的 s[0..j-1] 可以分为几部分,这是重复子问题,其答案为 dp[j-1],于是状态转移方程如下:

其中集合 $S$ 表示满足 $1 \leq j \leq i$ 且 s[j..i] 是回文的 $j$ 的集合。

这里我们需要频繁查询子串是否为回文,如果这步查询是 $O(1)$,则整体的时间复杂度为 $O(N^{2})$。

预处理:区间DP

为了响应频繁的子串是否为回文的查询,预处理一个二维数组 cc[i][j] 表示子串 s[i..j] 是否为回文,若为回文则为 true,若不为回文则为 false。

由于单个字符是回文,因此 c[i][i] = true,空串也是回文,因此 c[i+1][i] = true

如果子串 s[i..j] 长度大于 1,则看子串两个端点是否相等,若不相等则子串肯定不为回文;若相等,则继续看 s[i+1..j-1] 是否为回文。

上述过程就是一个区间 DP,也就是区间的两个端点构成了动态规划的阶段。

代码 (C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int minCut(string s) {
if(s.empty())
return 0;
int n = s.size();
vector<vector<bool> > c(n, vector<bool>(n, false));
c[n - 1][n - 1] = true;
for(int i = 0; i < n - 1; ++i)
{
c[i][i] = true;
c[i + 1][i] = true;
}
for(int j = 1; j < n; ++j)
for(int i = j - 1; i >= 0; --i)
if(s[i] == s[j])
c[i][j] = c[i + 1][j - 1];

vector<int> dp(n, 0);
for(int i = 0; i < n; ++i)
{
if(c[0][i])
{
dp[i] = 1;
continue;
}
int minx = INT_MAX;
for(int j = 1; j <= i; ++j)
{
if(c[j][i])
minx = min(minx, dp[j - 1]);
}
dp[i] = minx + 1;
}
return dp[n-1] - 1;
}
};