题目描述
对于两个字符串,请设计一个高效算法,求他们的最长公共子序列的长度,这里的最长公共子序列定义为有两个序列U1,U2,U3…Un和V1,V2,V3…Vn,其中 Ui<Ui+1,Vi<Vi+1。且A[Ui] == B[Vi]。
给定两个字符串A和B,同时给定两个串的长度n和m,请返回最长公共子序列的长度。保证两串长度均小于等于300。
测试样例:
"1A2C3D4B56",10,"B1D23CA45B6A",12
返回:6
思路 - DP
DP 定义
s[0:i] := s 长度为 i 的**前缀**
dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子序列的长度
DP 初始化
dp[i][j] = 0 当 i=0 或 j=0 时
DP 更新
当 s1[i] == s2[j]
时,dp[i][j] = dp[i-1][j-1] + 1
当 s1[i] != s2[j]
时,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
完整递推公式
dp[i][j] = 0 当 i=0 或 j=0 时
= dp[i-1][j-1] + 1 当 `s1[i-1] == s2[j-1]` 时
= max(dp[i-1][j], dp[i][j-1]) 当 `s1[i-1] != s2[j-1]` 时
参考答案:
# -*- coding:utf-8 -*-
class LCS:
def findLCS(self, A, n, B, m):
# write code here
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[n][m]
题目描述
对于两个字符串,请设计一个时间复杂度为O(m*n)的算法(这里的m和n为两串的长度),求出两串的最长公共子串的长度。这里的最长公共子串的定义为两个序列U1,U2,…Un和V1,V2,…Vn,其中Ui + 1 == Ui+1,Vi + 1 == Vi+1,同时Ui == Vi。
给定两个字符串A和B,同时给定两串的长度n和m。
测试样例:
"1AB2345CD",9,"12345EF",7
返回:4
思路 - DP
DP 定义
s[0:i] := s 长度为 i 的 前缀
dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子串的长度
dp[i][j]
只有当 s1[i] == s2[j]
的情况下才是 s1[0:i] 和 s2[0:j] 最长公共子串的长度
DP 初始化
dp[i][j] = 0 当 i=0 或 j=0 时
DP 更新
dp[i][j] = dp[i-1][j-1] + 1 if s[i] == s[j]
= ; else pass
参考答案:
# -*- coding:utf-8 -*-
class LongestSubstring:
def findLongest(self, A, n, B, m):
# write code here
dp = [[0] * (m+1) for _ in range(n+1)]
temp_res = 0
for i in range(1, n+1):
for j in range(1, m+1):
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
temp_res = max(temp_res, dp[i][j])
return temp_res
题目描述
对于一个数字序列,请设计一个复杂度为O(nlogn)的算法,返回该序列的最长上升子序列的长度,这里的子序列定义为这样一个序列U1,U2…,其中Ui < Ui+1,且A[Ui] < A[Ui+1]。
给定一个数字序列A及序列的长度n,请返回最长上升子序列的长度。
测试样例:
[2,1,4,3,1,5,6],7
返回:4
思路0 - O(N^2)
O(NlogN)
O(N^2)
参考答案:
# -*- coding:utf-8 -*-
class AscentSequence:
def findLongest(self, A, n):
# write code here
B = sorted(A)
return self.findCls(A, B)
def findCls(self, A, B):
dp = [[0] * (len(B) + 1) for _ in range(len(A)+1)]
for i in range(1, len(A) + 1):
for j in range(1, len(B) + 1):
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[-1][-1]
思路1 - O(N^2)解法
DP 定义
nums[0:i] := 序列 nums 的前 i 个元素构成的子序列
dp[i] := nums[0:i] 中 LIS 的长度
DP 初始化
dp[:] = 1 // 最长上升子序列的长度最短为 1
DP 更新 - O(N^2)的解法
dp[i] = max{dp[j]} + 1, if nums[i] > nums[j]
= max{dp[j]}, else
where 0 <= j < i
如果只看这个递推公式,很可能会写出如下的错误代码
// 牛客网
class AscentSequence {
public:
int findLongest(vector<int> nums, int n) {
vector<int> dp(n, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++)
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
else
dp[i] = max(dp[i], dp[j]);
}
return dp[n-1];
}
};
下面是网上比较流行的一种递推公式
dp[i] = dp[j] + 1, if nums[i] > nums[j] && dp[i] < dp[j] + 1
= pass, else
where 0 <= j < i
nums[i] > nums[j] && dp[i] < dp[j] + 1
时的 LIS;不满足该条件的情况跳过了;所以需要额外一个变量记录当前已知全局的 LIS# -*- coding:utf-8 -*-
class AscentSequence:
def findLongest(self, A, n):
# write code here
if n <= 1:
return 1
dp = [1] * (n)
res = 1
for i in range(1, n):
for j in range(i):
if A[i] > A[j] and dp[i] < dp[j] + 1:
dp[i] = dp[j] + 1
res = max(res, dp[i])
return res
最长回文子序列 - LeetCode
问题描述
给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
思路
相比最长回文子串,最长回文子序列更像最长公共子序列,只是改变了循环方向
DP 定义
s[i:j] := 字符串 s 在区间 [i:j] 上的子串
dp[i][j] := s[i:j] 上回文序列的长度
DP 初始化
dp[i][i] = 1 // 单个字符也是一个回文序列
DP 更新
dp[i][j] = dp[i+1][j-1] + 2, if s[i] == s[j]
= max(dp[i+1][j], dp[i][j-1]), else
比较一下 LCS 的递推公式
dp[i][j] = 0 当 i=0 或 j=0 时
= dp[i-1][j-1] + 1 当 `s1[i-1] == s2[j-1]` 时
= max(dp[i-1][j], dp[i][j-1]) 当 `s1[i-1] != s2[j-1]` 时
class Solution(object):
def longestPalindromeSubseq(self, s):
"""
:type s: str
:rtype: int
"""
if not s:
return 0
dp = [[0] * len(s) for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
for j in range(1, len(s)):
for i in range(j-1, -1, -1):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][len(s)-1]
最长回文子串_牛客网
最长回文子串 - LeetCode
牛客网只需要输出长度;LeetCode 还需要输出一个具体的回文串
问题描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
思路 - O(N^2)
DP 定义
s[i:j] := 字符串 s 在区间 [i:j] 上的子串
dp[i][j] := s[i:j] 是否是一个回文串
DP 初始化
dp[i][i] = 1 // 单个字符也是一个回文串
DP 更新
dp[i][j] = dp[i+1][j-1], if s[i] == s[j]
= 0, else
注意到:如果 j - i < 2 的话(比如 j=2, i=1),dp[i+1][j-1]=dp[2][1] 会出现不符合 DP 定义的情况
所以需要添加边界条件
dp[i][i+1] = 1, if s[i] == s[i+1]
= 0, else
该边界条件可以放在初始化部分完成;但是建议放在递推过程中完成过更好(为了兼容牛客和LeetCode)
Python DP 版本:
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
if not s or len(s) == 1:
return s
dp = [[0] * len(s) for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
begin = 0
end = 0
length = 1
for j in range(1, len(s)):
for i in range(j-1, -1, -1):
if j - i < 2:
dp[i][j] = 1 if s[i] == s[j] else 0
elif s[i] == s[j]:
dp[i][j] = dp[i+1][j-1]
else:
dp[i][j] = 0
if dp[i][j] and j - i + 1 > length:
begin = i
length = j - i + 1
return s[begin : begin+length]
Python 双指针版本:
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
if len(s) <= 1:
return s
self.begin, self.str_len = 0, 0
for index in range(len(s)-1):
self.helper(s, index, index)
self.helper(s, index, index+1)
return s[self.begin: self.begin + self.str_len]
def helper(self, s, left, right):
length = len(s)
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
if right - left - 1 > self.str_len:
self.begin = left + 1
self.str_len = right - left - 1
最大连续子序列_牛客网
牛客网要求同时输出最大子序列的首尾元素
思路 - 基本问题:只输出最大连续子序列和
DP 定义
a[0:i] := 序列 a 在区间 [0:i] 上的子序列
dp[i] := a[0:i] 上的最大子序列和
DP 初始化
dp[0] = a[0]
DP 更新
// 只要 dp[i] > 0 就一直累加下去,一旦小于 0 就重新开始
dp[i] = dp[i-1] + a[i], if dp[i-1] > 0
= a[i], else
ret = max{ret, dp[i]} // 只要大于 0 就累加会导致 dp[i] 保存的并不是 a[0:i] 中的最大连续子序列和
// 所以需要一个变量保存当前全局的最大连续子序列和
Python 原始DP:
void foo() {
int n;
while (cin >> n) {
vector<int> a(n);
for (int i = 0; i<n; i++)
cin >> a[i];
vector<int> dp(n);
dp[0] = a[0];
int ret = a[0];
for (int i = 1; i < n; i++) {
if (dp[i - 1] > 0)
dp[i] = dp[i - 1] + a[i];
else
dp[i] = a[i];
ret = max(ret, dp[i]);
}
cout << ret << endl;
}
}
/*
输入
5
1 5 -3 2 4
6
1 -2 3 4 -10 6
4
-3 -1 -2 -5
输出
9
7
-1
*/
DP 优化
注意到每次递归实际只用到了 dp[i-1]
,实际只要用到一个变量,空间复杂度 O(1)
void foo2() {
int n;
while (cin >> n) {
vector<int> a(n);
for (int i = 0; i<n; i++)
cin >> a[i];
int ret = INT_MIN;
int max_cur = 0;
for (int i = 0; i < n; i++) {
if (max_cur > 0) // 如果大于 0 就一直累加
max_cur += a[i];
else // 一旦小于 0 就重新开始
max_cur = a[i];
if (max_cur > ret) // 保存找到的最大结果
ret = max_cur;
// 以上可以简写成下面两行代码
//max_cur = max(max_cur + a[i], a[i]);
//ret = max(ret, max_cur);
}
cout << ret << endl;
}
}
LeetCode-编辑距离
问题描述
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思路
用一个 dp 数组维护两个字符串的前缀编辑距离
DP 定义
word[0:i] := word 长度为 i 的**前缀子串**
dp[i][j] := 将 word1[0:i] 转换为 word2[0:j] 的操作数
初始化
dp[i][0] = i // 每次从 word1 删除一个字符
dp[0][j] = j // 每次向 word1 插入一个字符
递推公式
word1[i] == word1[j]
时 dp[i][j] = dp[i-1][j-1]
word1[i] != word1[j]
时,有三种更新方式,
取最小
// word[1:i] 表示 word 长度为 i 的前缀子串
dp[i][j] = min({ dp[i-1][j] + 1 , // 将 word1[1:i-1] 转换为 word2[1:j] 的操作数 + 删除 word1[i] 的操作数(1)
dp[i][j-1] + 1 , // 将 word1[0:i] 转换为 word2[0:j-1] 的操作数 + 将 word2[j] 插入到 word1[0:i] 之后的操作数(1)
dp[i-1][j-1] + 1 }) // 将 word1[0:i-1] 转换为 word2[0:j-1] 的操作数 + 将 word1[i] 替换为 word2[j] 的操作数(1)
Python DP:
class Solution(object):
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
if (not word1) and (not word2):
return 0
if not word1:
return len(word2)
if not word2:
return len(word1)
dp = [[0] * (len(word2) + 1) for _ in range(len(word1) + 1)]
for i in range(len(word1)+1):
dp[i][0] = i
for j in range(len(word2)+1):
dp[0][j] = j
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
return dp[-1][-1]
LeetCode-221. 最大正方形
问题描述
在一个由 0 和 1 组成的二维矩阵 M 内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出:
4
思路
DP 定义
:
dp[i][j] := 以 M[i][j] 为正方形**右下角**所能找到的最大正方形的边长
dp
保存的不是全局最大值,所以需要用一个额外变量更新结果初始化
dp[i][0] = M[i][0]
dp[0][j] = M[0][j]
递推公式
dp[i][j] = min{dp[i-1][j],
dp[i][j-1],
dp[i-1][j-1]} + 1 若 M[i][j] == 1
= 0 否则
注意到,本题的递推公式与 编辑距离 完全一致
Python:
class Solution(object):
def maximalSquare(self, matrix):
"""
:type matrix: List[List[str]]
:rtype: int
"""
if not matrix or not matrix[0]:
return 0
dp = [[0] * len(matrix[0]) for _ in range(len(matrix))]
base = 0
for i in range(len(matrix)):
dp[i][0] = int(matrix[i][0])
base = max(base, dp[i][0])
for j in range(len(matrix[0])):
dp[0][j] = int(matrix[0][j])
base = max(base, dp[0][j])
for i in range(1, len(matrix)):
for j in range(1, len(matrix[0])):
if matrix[i][j] == '1':
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
base = max(base, dp[i][j])
else:
dp[i][j] = 0
return base * base
LeetCode - 322. 零钱兑换
问题描述
给定不同面额的硬币 coins 和一个总金额 amount。
编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
思路
定义:dp[i] := 组成总金额 i 时的最少硬币数
初始化:
dp[i] = 0 若 i=0
= INF 其他
状态转移
dp[j] = min{ dp[j-coins[i]] + 1 | i=0,..,n-1 }
其中 coins[i] 表示硬币的币值,共 n 种硬币
Python DP:
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if not coins or amount < 0:
return -1
dp = [amount + 1] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != amount + 1 else -1
LeetCode - 518. 零钱兑换 II
问题描述:
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
Python
class Solution(object):
def change(self, amount, coins):
"""
:type amount: int
:type coins: List[int]
:rtype: int
"""
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for i in range(coin, amount+1):
dp[i] += dp[i - coin]
return dp[amount]