动态规划之布尔运算(回文子串 和 单词拆分)
- 前言
动态规划的有两大特征最优子结构和重叠子问题,最优子结构经常与最大值、最小值或求和相关联,还有一类最优子结构和布尔运算或布尔值强相关,对于这类问题,急需另辟蹊跷,采用布尔dp进行计算。本文将对回文串和单词分割问题展开分析,这两类问题都是布尔运算的典型代表。
动态规划时常涉及决策和选择,实际上动态规划就是从多决策模型发展起来的。对于最大值、最小值而言,题目会非常容易得知选择的代价,它是一个整数值;相对而言,对于布尔运算,它选择的代价就是某个问题的进行比较,比较的结果可以作为选择的代价,这个代价需要和之前的选择代价进行布尔运算,进行&&或||运算。
- 问题描述
- 回文子串
回文子串问题描述,题目来源于Leetcode, 647. 回文子串 - 力扣(Leetcode)
给你一个字符串
s
,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
如若采用动态规划解决问题,首先需要创建合适的DP数组,建立问题之间的状态转移方程,一般状态转移数组的维度和可变量的数量一致,对于上述问题,需要跟踪两个状态,子字符串的起始和结束位置,为了方便,表示为dp[i][j],显而易见,状态转移方程立即可以建立起来。
d
p
[
i
]
[
j
]
=
(
s
[
i
]
=
=
s
[
j
]
)
&
&
d
p
[
i
+
1
]
[
j
−
1
]
dp[i][j]=(s[i]==s[j]) \&\& dp[i+1][j-1]
dp[i][j]=(s[i]==s[j])&&dp[i+1][j−1]
上面状态转移返程意思是,字符子串s[i…j-1,j]满足回文子串取决于两个条件,两端当前的字符是否相同,也即s[i]是否等于s[j],如果两端字符相同,紧接着需要判断s[i+1…j-1] 是否是回文串。当且仅当两个条件都为真,dp[i][j]才能判定为回文子串。
index | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
value(s[i]) | a | b | c | c | b | a |
如果要判断s[0][5]是否为回文子串,需要同时满足两个条件,显而易见s[0]等于s[5],都等于字符’a’;通过目视观察,发现s[i][4]恰好为回文字符串。套用上面的状态转移方程,可以得到下面的关系式:
d
p
[
0
]
[
5
]
=
(
s
[
0
]
=
=
s
[
5
]
)
&
&
d
p
[
1
]
[
4
]
=
t
r
u
e
dp[0][5]=(s[0]==s[5])\ \&\& \ dp[1][4]=true
dp[0][5]=(s[0]==s[5]) && dp[1][4]=true
- 单词拆分
接下来让我们把讨论转移到单词分割问题上来,单词分割问题同样来自Leetcode, 问题可以描述为,
给你一个字符串
s
和一个字符串列表wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出s
**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
如果以实例1中的字典为基础,就可以形成一个三叉树,树的分支数量取决于字典里面的单词数量,通常情况下,分支数目比单词数量多1。用通俗语言表述,如果字典中含有n个单词,那么树的分支数量N就等于(n+1)。这个问题有点类似硬币问题,规定有无限的重复资源,求解特定条件下面的解(最小硬币数量,兑换的所有方式或某个特定解等)。
对于给点的单词s, 需要在不同的地方予以拆分,拆分过程中,需要记录的状态是,在某个位置上,是否成功拆分,定义dp[length of s+1]为状态数组。
数组dp[i]的涵义为,i之前的所有字符能否拆分为字典中已有的单词,如果能拆分成字典中对应的某个单词,那么dp[i]的值就为true. 这就要求对长度为i的字符进行分割,一旦找到满足条件的解,就停止搜索。
定义状态转移方程为,
d
p
[
i
]
=
d
p
[
j
]
&
&
s
[
j
+
1...
i
]
∈
d
i
c
t
i
o
n
a
r
y
;
l
e
t
t
e
r
a
t
j
+
1
m
e
a
n
s
s
[
j
]
(
0
<
j
<
i
)
;
dp[i]=dp[j] \&\& s[j+1...i]∈ dictionary; \ letter\ at \ j+1 \ means \ s[j]\ (0<j<i);
dp[i]=dp[j]&&s[j+1...i]∈dictionary; letter at j+1 means s[j] (0<j<i);
首先dp数组属于布尔类型,在 i之前所有字符要满足单词拆分的条件是,在某个位置j处,可以拆分为两部分,前面的部分dp[j]如果满足拆分条件(单个,两个或多个单词),再加上s[j+1…i]的字符串如果从字典中能找到,那么dp[i]=true。
- 代码实现
子回文串的代码实现,
/**
* @file count_substring.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-19
*
* @copyright Copyright (c) 2023
*
*/
#ifndef COUNT_SUBSTRING_C
#define COUNT_SUBSTRING_C
#include "count_substring.h"
int count_substring(char *s)
{
int i;
int j;
int count=0;
int n=strlen(s);
bool dp[n][n];
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++)
{
dp[i][i]=true;
}
for(i=0;i<n;i++)
{
for(j=0;j<i;j++)
{
dp[j][i]=((s[j]==s[i]) && dp[j+1][i-1]);
}
}
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
if(dp[i][j])
{
count ++;
}
}
}
return count;
}
#endif
拆分单词的代码实现
/**
* @file word_break.c
* @author your name (you@domain.com)
* @brief https://leetcode.cn/problems/word-break/description/
* @version 0.1
* @date 2023-03-18
*
* @copyright Copyright (c) 2023
*
*/
#ifndef WORD_BREAK_C
#define WORD_BREAK_C
#include "word_break.h"
bool break_word(char *target, char **word_dic, int word_dic_size)
{
int len_target=strlen(target);
int dp[len_target+1];
int rec[word_dic_size];
int i;
int j;
for(i=0;i<word_dic_size;i++)
{
*(rec + i) = HASH(word_dic[i], 0, strlen(word_dic[i]));
}
dp[0]=true;
for(i=1;i<=len_target;i++)
{
for(j=0;j<i;j++)
{
if(dp[j]&&query(rec,word_dic_size,HASH(target,j,i)))
{
dp[i]=true;
break;
}
}
}
return dp[len_target];
}
//start j measning j+1
int HASH(char *s, int l, int r)
{
int value=0;
for(int i=l;i<r;i++)
{
value+=(value*23);
value+=(s[i]-'a'+1);
}
return value;
}
bool query(int *rec, int len_rec, int to_be_searched)
{
int i;
for(i=0;i<len_rec;i++)
{
if(rec[i]==to_be_searched)
{
return true;
}
}
return false;
}
#endif
- 小结
本文回顾了回文子串和单词分拆的动态规划实现方式,二者都采用了bool类型的DP数组对状态进行追踪,布尔DP数组的状态转移方程涉及到对象之间的比较以及前面相关的已知结果。可以选择布尔运算的各类逻辑运算符,最终求得结果。
参考资料:
- LeetCode 习题集