给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
dp[i][j] // word1.substr(0, i)与word2.substr(0, j)之间的编辑距离
word1:""
word2:""
dp[i][j] 代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数
所以,
当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。
注意,针对第一行,第一列要单独考虑,我们引入 ‘’ 下图所示:
第一行,是 word1 为空变成 word2 最少步数,就是插入操作
第一列,是 word2 为空,需要的最少步数,就是删除操作
class Solution(object):
def minDistance(self, word1, word2):
n1 = len(word1)
n2 = len(word2)
dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
# 第一行
for j in range(1, n2 + 1):
dp[0][j] = dp[0][j-1] + 1
# 第一列
for i in range(1, n1 + 1):
dp[i][0] = dp[i-1][0] + 1
for i in range(1, n1 + 1):
for j in range(1, n2 + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1
#print(dp)
return dp[-1][-1]
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc”,它的长度为 3。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
子序列和子串的区别:子序列之间可以有间隔,而子串是连续的
动态转移方程:如果s1和s2某个字符相同,则说明已经找到了一个共同的字符,那么test1和test2的下标都要减少一位,因为第i个字符和第j个字符已经被用,否则不同的话,要不就是i删掉一个字符,或者j删掉一个字符,注意这里不能同时删掉,因为两者是不相同的
dp[i][j] = dp[i-1][j-1] + 1 (if s1[i-1] == s2[j-1])
else dp[i][j] = max(dp[i-1][j], dp[i][j-1])
substring
public String substring(int beginIndex,int endIndex)
返回一个新字符串,它是此字符串的一个子字符串。
该子字符串从指定的 beginIndex 处开始,一直到索引 endIndex - 1 处的字符。
因此,该子字符串的长度为 endIndex-beginIndex。
示例:
“hamburger”.substring(4, 8) returns “urge”
“smiles”.substring(1, 5) returns “mile”
参数:
beginIndex - 开始处的索引(包括)。
endIndex - 结束处的索引(不包括)。
返回:
指定的子字符串。
def longestCommonSubsequence(self, text1, text2):
if not text1 or not text2:
return 0
m = len(text1)
n = len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = 1 + dp[i-1][j-1]
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
- Longest common substring (最长子串)
dp[i][j] = dp[i-1][j-1] + 1 (if s1[i-1] == s2[j-1])
else dp[i][j] = 0 // 此位置没有公共子串,最后的值是在所有位置中找到最大值即可
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
1 嵌套循环,枚举i(起点), j(终点),判断该子串是否回文
2 中间向两边扩张法
3 DP[i][j]
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;// 第一个字符是自己的回文子串
int begin = 0;
// s.charAt(i)每次都会检查数组下标越界,因此先转换成字符数组,这一步非必须
char[] charArray = s.toCharArray();
// 枚举所有长度严格大于1的子串 charArray[i..j]
for (int i = 0; i < len - 1; i++) {
for (int j = i+1; j < len; j++) {
if (j - i + 1 > maxLen && validPalindromic(charArray, i, j)) {
maxLen = j - i + 1;
begin = i; // 必须写到这里(记录当前回文子串开始的位置),因为如果写到该循环外面的话,i是一直走的,所以begin = i肯定到最后i 为len - 1了,
}
}
}
return s.substring(begin, begin + maxLen);
}
// 验证子串s[left...right]是否为回文串
private boolean validPalindromic(char[] charArray, int left, int right) {
while(left < right) {
if (charArray[left] != charArray[right]) {
return false;
}
left++;
right--;
}
return true;
}
// 中心扩散
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;// 第一个字符是自己的回文子串
int begin = 0;
// s.charAt(i)每次都会检查数组下标越界,因此先转换成字符数组,这一步非必须
char[] charArray = s.toCharArray();
// 枚举所有长度严格大于1的子串 charArray[i..j]
for (int i = 0; i < len - 1; i++) {
int oddLen = expandAroundCenter(charArray, i, i);
int evenLen = expandAroundCenter(charArray, i, i + 1);
int curMaxLen = Math.max(oddLen, evenLen);
if (curMaxLen > maxLen) {
maxLen = curMaxLen;
// 这一步要在纸上画图发现规律:i为当前回文子串中心的下标,(maxLen - 1) / 2是回文子串长度所对应的中心下标,当前的下标减去实际的下标就是回文子串的起始点
begin = i - (maxLen - 1) / 2;
}
}
return s.substring(begin, begin + maxLen);
}
//或者
public String longestPalindrome2(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
// 验证子串s[left...right]是否为回文串
/**
@param charArray 原始字符串的字符数组
@param left 起始左边界(可以取到)
@param right 起始有边界(可以取到)
@return 回文串的长度
*/
private int expandAroundCenter(char[] charArray, int left, int right) {
// 当left == right 回文中心是一个字符,回文串的长度是奇数
// 当right = left + 1 ,回文中心是两个字符,回文串的长度是偶数
int len = charArray.length;
int i = left;
int j = right;
while(i >= 0 && j < len) {
if(charArray[i] == charArray[j]) {
i--;
j++;
}else{
break;
}
}
return j - i + 1 - 2; // j - i + 1是回文串的长度包含了i和j所对应的元素.,
// 跳出while循环,是满足s.charAt(i) != s.charAt(j),
// 退出循环后i和j中间的部分是回文子串不包含i和j指向的字符, 回文串的长度是j - i + 1 - 2 = j - i - 1
}
方法三:DP
定义dp(i, j) = true; s[i, j]是回文串;dp(i, j) = false; s[i, j]不是回文串
如果i和j相等,或者i和j相差一位的话,说明子串长度为0,或者为1,肯定是回文串
if i ==j or j - i + 1
接下来只要是s[i] == sj[]是i和j不断的向外扩散, i每次+1,j每次-1,就是将ture传递出去
初始状态是空或者长度为1的子串,看能不能向上扩展到更长更大的子串
/*
枚举i(起点), j(终点)
*/
public String longestPalindrome(String s) {
int n = s.length();
String res = "";
boolean[][] dp = new boolean[n][n];
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
// 只要 s.charAt(i) == s.charAt(j)就可以吧dp[i][j]扩张出去 且(dp[i+1][j-1]也是true的情况下即子串也是回文串或者是空或者长度为1的子串)
dp[i][j] = s.charAt(i) == s.charAt(j) && (j-i < 2 || dp[i+1][j-1]);
if (dp[i][j] && j - i + 1 > res.length()) {
res = s.substring(i, j+1);
}
}
}
return res;
}
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = "a"
输出: true
解释: 因为 ‘’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:
s = “ab”
p = "."
输出: true
解释: "." 表示可匹配零个或多个(’’)任意字符(’.’)。
示例 4:
输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 '’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = "misisp."
输出: false
思路:
第一步:暂时不管正则符号,如果两个普通字符串进行比较
bool isMatch(string text, string pattern) {
if (text.size() != pattern.size()) {
return false;
}
for (int j = 0; j < pattern.size(); j++) {
if (pattern[j] != text[j])
return false;
}
return true;
}
稍微改一下为
bool isMatch(string text, string pattern) {
int i = 0; // text的索引位置
int j = 0; // pattern 的索引位置
while (j < pattern.size()) {
if (i >= text.size())
return false;
if (pattern[j++] != text[i++]) {
return false;
}
}
// 判断pattern和text是否一样长
return j == text.size()
}
递归
def isMatch(text, pattern) -> bool:
#如果pattern为空就返回text是否为空
if pattern is empty: return text is empty
#如果第一个字符是一样的就要看剩余的字符是否一样
first_match = (text not empty) and pattern[0] == text[0]
# text去掉第一个字符的子串和pattern去掉第一个字符的子串,两者是否match
return first_match and isMatch(text[1:] , pattern[1:])
第二步:处理点号.通配符
def isMatch(text, pattern) -> bool:
# pattern为空 返回text为空
if not pattern: return not text
# 如果text不为空,且pattern[0]等于text[0]或者是pattern[0]是一个点的话, first_match为true
first_match = bool(text) and pattern[0] in {text[0], '.'}
return first_match and isMatch(text[1:] , pattern[1:])
第三步:处理*通配符
def isMatch(text, pattern) -> bool:
# pattern为空 返回text为空
if not pattern: return not text
# 如果text不为空,且pattern[0]等于text[0]或者是pattern[0]是一个点的话, first_match为true
first_match = bool(text) and pattern[0] in {text[0], '.'}
# *永远要跟它前面的字符一起出现,因为*本身是要把前面的字符复制0份或多份
if len(pattern) >= 2 and pattern[1] == '*':
#发现通配符*有两种情况:
# 匹配该字符0次,跳过该字符和'*';意思就是把前面字符和*直接当成是0,这时候pattern要略过2位,但text没有任何略过
# 或者 当pattern[0]和text[0]匹配后,移动text;如果第一位符合,text匹配到1位,向后走1位,pattern不需要动因为*可以再重复出来
return isMatch(text, pattern[2:]) or first_match and isMatch(text[1:], pattern)
return first_match and isMatch(text[1:] , pattern[1:])
第四步 动态规划
# (带备忘录的递归)
def isMatch(text, pattern) -> bool:
memo = dict() # 备忘录
def dp(i, j):
if (i, j) in memo: return memo[(i, j)]
if j == len(pattern): return i == len(text)
first = i < len(text) and pattern[j] in {text[i], '.'}
#如果pattern的长度小于等于2的话且后面一个为*时,说明子串字符大于2且第二个字符是*
if j <= len(pattern) - 2 and pattern[j+1] == '*':
ans = dp(i, j+2) or first and dp(i+1, j)
else:
ans = first and dp(i+1, j+1)
memo[(i, j)] = ans
return ans
return dp(0, 0)
class Solution:
def isMatch(self, s: str, p: str) -> bool:
S = len(s)
P = len(p)
memo = {}
def dp(i, j):
if ((i, j) in memo):
return memo[(i, j)]
if (j==P):
return i ==S
pre=i<S and p[j] in {s[i], "."}
if (j<=P-2 and p[j+1]=="*"):
tmp = dp(i,j+2) or pre and dp(i+1,j)
else:
tmp = pre and dp(i+1,j+1)
memo[(i,j)] = tmp
return tmp
return dp(0,0)
#没有备忘录的递归
def isMatch(text, pattern) -> bool:
if not pattern: return not text
first_match = bool(text) and pattern[0] in {text[0], '.'}
if len(pattern) >= 2 and pattern[1] == '*':
return isMatch(text, pattern[2:]) or first_match and isMatch(text[1:], pattern)
return first_match and isMatch(text[1:] , pattern[1:])
给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
'’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = ""
输出: true
解释: ‘’ 可以匹配任意字符串。
示例 3:
输入:
s = “cb”
p = “?a”
输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
示例 4:
输入:
s = “adceb”
p = “ab”
输出: true
解释: 第一个 '’ 可以匹配空字符串, 第二个 '’ 可以匹配字符串 “dce”.
示例 5:
输入:
s = “acdcb”
p = "ac?b"
输出: false
动态规划思路
给定的模式p中,只会有三种类型的字符出现:
(1)小写字母a-z,匹配对应的一个小写字母
(2)问号?,可以匹配任意一个小写字母
(3)星号*,可以匹配任意字符串,可以为空,也可以匹配0或任意多个小写字母
其中小写字母和问号的匹配是确定的,星号匹配不确定因此我们需要枚举所有的匹配情况。为了减少重复枚举,我们可以使用动态规划来解决本题。
class Solution(object):
def isMatch(self, s, p):
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
for i in range(1, n+1):
if p[i-1] == '*':
dp[0][i] = True
else:
break
for i in range(1, m+1):
for j in range(1, n+1):
if p[j-1] == '*':
dp[i][j] = dp[i-1][j] | dp[i][j-1]
elif p[j-1] == '?' or s[i-1] == p[j-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:S = “rabbbit”, T = “rabbit”
输出:3
解释:
如下图所示, 有 3 种可以从 S 中得到 “rabbit” 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
示例 2:
输入:S = “babgbag”, T = “bag”
输出:5
解释:
如下图所示, 有 5 种可以从 S 中得到 “bag” 的方案。
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^ ^
babgbag
^ ^^
babgbag
^ ^^
babgbag
^^^
暴力法:
通过记录当前走到的下标 i 和 j 来判断是否相等,若 s[ j ] == t[ i ], 则可以将 目标字符串的下标加一然后继续匹配(即选中当前字符)。其中,无论 s[ j ] 是否等于 t[ i ] 都需要不选择当前字符,然后继续进行递归。递归的终止条件为字符串 s 或 t 的下标走到最后的位置,当字符串 t 走到最终位置时,发生了一次匹配, res++
class Solution {
public:
int numDistinct(string s, string t) {
int res = 0;
dfs(res, s, t, 0, 0);
return res;
}
void dfs(int& res, string s, string t, int i, int j){
if(i==t.size()){
res++;
return ;
}
if(j==s.size()) return ;
// 是否相等都要走这一步
dfs(res, s, t, i, j+1);
// 发生匹配时,字符串 t 的下标 +1 然后进行匹配
if(s[j]==t[i]) dfs(res, s, t, i+1, j+1);
}
};
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n1 = len(s)
n2 = len(t)
dp = [[0] * (n1 + 1) for _ in range(n2 + 1)]
for j in range(n1 + 1):
dp[0][j] = 1
for i in range(1, n2 + 1):
for j in range(1, n1 + 1):
if t[i - 1] == s[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]
else:
dp[i][j] = dp[i][j - 1]
#print(dp)
return dp[-1][-1]
public int numDistinct(String s, String t) {
int[][] dp = new int[t.length()+1][s.length()+1];
for (int i = 0; i < s.length()+1; i++) {
// 只要前者为空,后者随便s有多少个字符,那么出现的个数只有一个,只能全部删除变为空串
dp[0][i] = 1;
}
for(int i = 1; i<t.length()+1;i++){
for(int j =i; j<s.length()+1;j++){
if(t.charAt(i-1) == s.charAt(j-1)) {
dp[i][j] = dp[i][j-1] + dp[i-1][j-1];
}else{
dp[i][j] = dp[i][j-1];
}
}
}
return dp[t.length()][s.length()];
}
或者
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n1 = len(s)
n2 = len(t)
dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
for i in range(n1 + 1):
dp[i][0] = 1
for i in range(1, n1 + 1):
for j in range(1, n2 + 1):
if t[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]
#print(dp)
return dp[-1][-1]