一、最长公共子序列
题目描述:
- 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
- 样例:text1 = “abcde”,text2 = “ace”,输出3
思路:
想象一个二维数组,找的是一条不连续的从左上到右下的最长路径
因为是子序列,不要求连续,可以在比对两个串值是否相等给二维数组赋值的过程中顺遍把最长子序列长度给计算出来。
因为从左上到右下,所以每一个节点的值可以从(i-1,j-1)、(i,j-1)、(i-1,j)三个方向上来尝试,因为取最大值,所以取这三个方向的最大值。
实现:
public int longestCommonSubsequence(String text1, String text2) {
//确定状态----f[i][j]表示0-i和0-j时,最长的公共子序列的长度
//转移方程----f[i][j] = Math.min(Math.max(f[i-1][j-1] + 1 && text1[i] == text2[j],Math.max(f[i-1][j],f[i][j-1])))
//边界条件 f[0][0] = 0 f[0][j] = 0 f[i][0] = 0
int m = text1.length();
int n = text2.length();
int[][] f = new int[m+1][n+1];
f[0][0] = 0;
for(int i = 0;i<=m;i++){
f[i][0] = 0;
}
for(int i = 0;i<=n;i++){
f[0][i] = 0;
}
for(int i=1;i<=m;i++){
for(int j = 1;j<=n;j++){
// if(text1.charAt(i-1)==text2.charAt(j-1)){
// f[i][j] = Math.max(Math.max(f[i-1][j],f[i][j-1]),f[i-1][j-1] + 1);
// }else{
// f[i][j] = Math.max(Math.max(f[i-1][j],f[i][j-1]),f[i-1][j-1]);
// }
// f[i][j] = Math.min(f[i][j],Math.min(i,j));
//看别人题解,得知f[i-1][j-1]永远是最小的,所以上述逻辑可以改为
if(text1.charAt(i-1)==text2.charAt(j-1)){
f[i][j] = Math.max(Math.max(f[i-1][j],f[i][j-1]),f[i-1][j-1] + 1);
}else{
f[i][j] = Math.max(f[i-1][j],f[i][j-1]);
}
}
}
return f[m][n];
}
二、最长公共子串
题目描述:
- 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子串的长度。如果不存在 公共子序列 ,返回 0 。
- 样例:text1 = acbcbcef,text2 = abcbced,输出5
思路:
和子序列类似,将两个串组成一个二维数组,如果二维矩阵中对应的点行列所表示的字符相等,则为1,否则为0,找连续的值为1的从左上到右下的最长的线。
我们依然可以在计算哪些节点的值为1的过程中计算最长的字串长度。但是此时由于字串要求连续,所以计算当前节点的时候只能从(i-1,j-1)这个节点来计算,否则取0。
实现:
public int getLCS(String s, String t) {
//确定状态----f[i][j]表示0-i和0-j时,最长的公共子串的长度
//转移方程----f[i][j] = (f[i-1][j-1] + 1 && text1[i] == text2[j])||(0,text1[i] != text2[j])
//边界条件 f[0][0] = 0 f[0][j] = 0 f[i][0] = 0
if (s == null || t == null) {
return 0;
}
int result = 0;
int sLength = s.length();
int tLength = t.length();
int[][] dp = new int[sLength + 1][tLength + 1];
for (int i = 1; i <= sLength; i++) {
for (int k = 1; k <= tLength; k++) {
if (s.charAt(i - 1) == t.charAt(k - 1)) {
dp[i][k] = dp[i - 1][k - 1] + 1;
result = Math.max(dp[i][k], result);
}
}
}
return result;
}
如果是求具体的子串呢?
记录最长的子串的结束位置,然后再向前找到开始位置,截取字符串
public String LCS (String str1, String str2) {
// write code here
//动态规划,想象成一个二维数组,找连续的斜对角线最长的
//确认状态,dp[i][j]表示0-i和0-j两个子串的最长公共子串
//转移方程,dp[i][j] = dp[i+1][j-1]
//边界条件
if(str1==null || str2==null){
return null;
}
int len1 = str1.length();
int len2 = str2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
int end1 = 0;
int end2 = 0;
int max = 0;
for(int i = 1;i<len1;i++){
for(int j = 1;j<len2;j++){
if(str1.charAt(i)==str2.charAt(j)){
dp[i][j] = dp[i-1][j-1] + 1;
if(dp[i][j]>max){
max = dp[i][j];
end1 = i;
end2 = j;
}
}
}
}
int end = end1;
//记录好最长子串在两个字符串的结束位置,一起向前,找到开始字符
while(end1>=0 && end2 >= 0 && str1.charAt(end1)==str2.charAt(end2)){
end1--;
end2--;
}
int start = end1;
return str1.substring(start+1,end+1);
}
三、最长回文子序列
题目描述:
- 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
- 示例:s = “bbbab”,最长回文子序列长度为4,即 bbbb
思路:
使用动态规划。(为什么可以使用动态规划呢?)
- 确定状态:f[i][j]表示字符串 i 到 j 这段子串的最长子序列的长度
- 转移方程:
- f[i][j] = f[i+1][j-1] + 2,if s.charAt(i) == s.charAt(j)
- f[i][j] = Math.max(f[i+1][j],f[i][j-1]), else
- 边界条件:
f[i][i] = 1
实现:
public int longestPalindromeSubseq(String s) {
int len = s.length();
int[][] f = new int[len][len];
for(int i = len-1;i>=0;i--){
f[i][i] = 1;
for(int j = i+1;j<len;j++){
if(s.charAt(i)==s.charAt(j)){
f[i][j] = f[i+1][j-1] +2;
}else{
f[i][j] = Math.max(f[i+1][j],f[i][j-1]);
}
}
}
return f[0][len-1];
}
四、最长回文子串
题目描述:
- 给你一个字符串 s,找到 s 中最长的回文子串。
- 示例:s = “babad”,最长的回文字串为"bab"
思路:
- 确定状态:f[i][j]表示 子串 i 到 j的是否回文
- 转移方程:
- f[i][j] = f[i-1][j-1] ,if (s.charAt(i) == s.charAt(j))
- else f[i][j] = false
- 边界条件:f[i][i] = true,f[i][i+1] = (si==si+1)
实现:
public String longestPalindrome(String s) {
int len = s.length();
if(len==0){
return "";
}
int max = 1;
String ans = s.substring(0,1);
boolean[][] f = new boolean[len][len];
for(int i = len-1;i>=0;i--){
f[i][i] = true;
for(int j = i+1;j<len;j++){
if(s.charAt(i)==s.charAt(j)){
if(j - i == 1){
f[i][j] = true;
}else{
f[i][j] = f[i+1][j-1];
}
}else{
f[i][j] = false;
}
if(f[i][j] && j + 1 - i > max){
max = j+1-i;
ans = s.substring(i,j + 1);
}
}
}
return ans;
}
五、最短回文子串
题目描述:
思路:
实现:
六、最长重复子串
题目描述:
思路:
实现:
七、最长公共前缀
题目描述:
- 查找一个字符数组中的最长公共前缀,如果不存在,则返回""。
- 示例:[“flower”、“flow”、“flight”],输出"fl"
思路:挨个比对
实现:
public String longestCommonPrefix(String[] strs) {
if(strs.length == 0){
return "";
}
//选第一个当作当前的公共前缀
String prefix = strs[0];
int count = strs.length;
for(int i = 1;i<count;i++){
//从第一个开始,不断拿当前的公共前缀去和下一个字符串对比,更新公共前缀的长度
int length = Math.min(prefix.length(),strs[i].length());
int index = 0;
while(index < length && prefix.charAt(index) == strs[i].charAt(index)){
index++;
}
prefix = prefix.substring(0,index);
if(prefix.length() == 0){
break;
}
}
return prefix;
}
八、最长递增子序列
思路:
- 先求最长递增子序列的长度,再求具体的序列,求具体序列的时候逆序寻找
- 求最长递增子序列的长度,暴力的话即求出每个子序列,然后求其长度,但是每个子序列的长度可以作为状态保留,供后面的计算,所以可以使用动态规划。
- 确定状态,f[i] 表示以nums[i] 结尾的子序列的最大长度
- 转移方程,f[i] = Math.max( f[j] ) + 1,j<i && nums[i] > nums[j]
- 边界条件,f[0] = 1,f[i] = 1
- 注意:找字典序最小的序列的时候需要逆序寻找,为什么逆序就可以了呢? 因为如果大的数字在后面,那么递增的子序列长度会更长,如果逆序以某个字符为结尾的子序列刚好符合最大长度,说明该字符后面不存在比它更大的数字了,比它大的数字都在其前面,所以同样长度的递增子序列,这个是字典序最小的。
//求最长长度
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if(len == 0){
return 0;
}
int[] f = new int[len];
f[0] = 1;
int max = 1;
for(int i = 1;i<len;i++){
f[i] = 1;
for(int j = 0;j<i;j++){
if(nums[i] > nums[j]){
f[i] = Math.max(f[i],f[j]+1);
}
}
max = Math.max(max,f[i]);
}
return max;
}
//求字典序最小的那个序列
public int[] lengthOfLIS(int[] nums) {
int len = nums.length;
if(len == 0){
return 0;
}
int[] f = new int[len];
f[0] = 1;
int max = 1;
for(int i = 1;i<len;i++){
f[i] = 1;
for(int j = 0;j<i;j++){
if(nums[i] > nums[j]){
f[i] = Math.max(f[i],f[j]+1);
}
}
max = Math.max(max,f[i]);
}
//这里逆序
int[] res = new int[max];
for(int i = len-1;i>=0;i--){
if(f[i] == max){
max--;
res[max]= nums[i];
}
}
return res;
}
九、最长无重复子数组
使用双指针,left记录不重复数组的最左侧,right记录最右侧,当遇到重复的字符串,左侧指向前面的那个重复字符串的后一个字符。
即right不断添加新字符,遇到重复的则left更新,最后求right - left
这个题为啥不用动态规划,我觉得是因为状态转移没什么用处,因为新加入一个元素要判断是否和已有元素都判断一遍,无法记录状态。
public int maxLength (int[] arr) {
// write code here
//存储每个字符上一次出现的位置
Map<Integer,Integer> map = new HashMap();
int len = 0;
int cur = 0;
//双指针,一个记录最左侧,一个记录最右侧
for(int i = 0,left=0;i<arr.length;i++){
if(map.containsKey(arr[i])){
//这里可能left已经比某字符上一次出现的位置大了
//例如1,3,3,3,1,如果right = 4,指向字符1的时候,left要等于3而不是最开始的0
left = Math.max(left,map.get(arr[i]));
}
map.put(arr[i],i+1);
len = Math.max(len,i-left+1);
}
return len;
}