问题描述
小F是一个好学的中学生,今天他学习了数列的概念。他在纸上写下了一个由 0
和 1
组成的正整数序列,长度为 n
。这个序列中的 1
和 0
交替出现,且至少由 3 个连续的 0
和 1
组成的部分数列称为「神奇数列」。例如,10101
是一个神奇数列,而 1011
不是。现在,小F想知道在这个序列中,最长的「神奇数列」是哪一个。你能帮他找到吗?
如果有多个神奇数列,那么输出最先出现的一个。
测试样例
样例1:
输入:inp = "0101011101"
输出:'010101'
样例2:
输入:inp = "1110101010000"
输出:'10101010'
样例3:
输入:inp = "1010101010101010"
输出:'1010101010101010'
解题过程:
问题分析
根据题目要求,不难看出这是经典的最长上升子序列问题,使用动态规划,但是所求的不是长度而是具体的子序列,不考虑其他手段。
定义「神奇数列」
• 由至少 3
个数字组成。
• 数字只能是 0
和 1
。
• 数字 0
和 1
交替出现,不能有重复的数字相邻。
目标: 找到给定二进制字符串中最长的「神奇数列」。
思路分析
🌟 动态规划的核心在于利用子问题的解来构建原问题的解。
1. 状态定义:
• 设定一个数组 dp[i]
,表示以第 i
个字符结尾的最长交替子串的长度。
• 初始化:dp[0] = 1
,因为第一个字符本身可以视为长度为 1
的交替子串。
2. 状态转移方程:
• 如果当前字符与前一个字符不同,则说明可以构成一个更长的交替子串:
dp[i] = dp[i - 1] + 1
• 如果当前字符与前一个字符相同,则无法继续之前的交替子串,只能重新开始:
dp[i] = 1
3. 结果更新:
• 在计算 dp[i]
的过程中,维护一个变量 maxLength
,记录当前最长的交替子串长度。
• 还需要记录对应的 startIndex
,以便最后输出最长的「神奇数列」。
4. 判断子串长度是否至少为 3:
• 由于「神奇数列」要求长度至少为 3
,所以在更新 maxLength
时,需要检查 dp[i]
是否大于等于 3
。
代码实现
以下是基于上述思路的 Java 实现:
public class Main {
public static String solution(String s) {
// Edit your code here
int n = s.length();
if (n < 3) {
return ""; // 字符串长度小于3,不可能存在神奇数列
}
int[] dp = new int[n];
dp[0] = 1;
int maxLength = 0;
int startIndex = -1;
for (int i = 1; i < n; i++) {
if (s.charAt(i) != s.charAt(i - 1)) {
dp[i] = dp[i - 1] + 1;
} else {
dp[i] = 1;
}
// 检查当前交替子串是否为最长神奇数列
if (dp[i] >= 3 && dp[i] > maxLength) {
maxLength = dp[i];
startIndex = i - dp[i] + 1;
}
}
// 如果未找到神奇数列,返回空字符串
if (maxLength == 0) {
return "";
}
// 返回最长神奇数列
return s.substring(startIndex, startIndex + maxLength);
}
public static void main(String[] args) {
// Add your test cases here
System.out.println(solution("0101011101"));
System.out.println(solution("0101011101").equals("010101"));
}
}
代码说明
• 初始化:
创建一个长度为 n
的数组dp
,dp[0] = 1
,表示第一个字符的交替子串长度为 1
。
• 动态规划过程:
从 i = 1
开始遍历字符串:
如果 s.charAt(i) != s.charAt(i - 1) : dp[i] = dp[i - 1] + 1
,当前字符可以延续前一个交替子串。
否则: dp[i] = 1
,需要重新开始新的交替子串。
• 结果更新:
在每次更新 dp[i]
后,检查:
如果 dp[i] >= 3 且 dp[i] > maxLength
,更新 maxLength
和 startIndex
。
• 输出处理:
如果 maxLength == 0
,说明没有符合条件的「神奇数列」,返回空字符串。
否则,使用 substring
提取最长的「神奇数列」。
示例测试
以输入 "0101011101"
为例:
• dp
数组的更新过程:
i=0, dp[0]=1
i=1, s[1]!=s[0], dp[1]=2
i=2, s[2]!=s[1], dp[2]=3
i=3, s[3]!=s[2], dp[3]=4
i=4, s[4]!=s[3], dp[4]=5
i=5, s[5]!=s[4], dp[5]=6
i=6, s[6]==s[5], dp[6]=1
i=7, s[7]!=s[6], dp[7]=2
i=8, s[8]==s[7], dp[8]=1
i=9, s[9]!=s[8], dp[9]=2
• maxLength
和 startIndex
的更新:
• i=5
时,dp[5]=6
,更新 maxLength=6
,startIndex=0
。
• 最终输出 s.substring(0, 6)
,即 "010101"
。
大伙在做动态规划题的时候都可以手动去推导推导,看看结果是否符合预期
优化方向和策略
当前的实现已经是线性时间复杂度 O(n)
,空间复杂度 O(n)
。我们可以进一步优化空间复杂度。
优化空间复杂度
• 不使用额外的数组 dp : 我们可以只使用一个变量来记录当前交替子串的长度。
优化后的代码
import java.util.Scanner;
public class MagicSequenceOptimized {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
String result = findLongestMagicSequence(s);
System.out.println(result);
}
public static String findLongestMagicSequence(String s) {
int n = s.length();
if (n < 3) {
return ""; // 字符串长度小于3,不可能存在神奇数列
}
int maxLength = 0;
int startIndex = -1;
int currentLength = 1; // 当前交替子串的长度
int currentStart = 0; // 当前交替子串的起始索引
for (int i = 1; i < n; i++) {
if (s.charAt(i) != s.charAt(i - 1)) {
currentLength++;
} else {
// 检查并更新最长神奇数列
if (currentLength >= 3 && currentLength > maxLength) {
maxLength = currentLength;
startIndex = currentStart;
}
// 重置当前交替子串
currentLength = 1;
currentStart = i;
}
}
// 检查最后一个交替子串
if (currentLength >= 3 && currentLength > maxLength) {
maxLength = currentLength;
startIndex = currentStart;
}
if (maxLength == 0) {
return "";
}
return s.substring(startIndex, startIndex + maxLength);
}
}
优化说明
• 空间复杂度降为 O(1):
不再需要数组 dp[]
,只使用几个变量即可完成计算。
• 逻辑调整:
(1). 使用 currentLength
和 currentStart
记录当前交替子串的信息。
(2). 在遇到重复字符时,及时更新 maxLength
和 startIndex
。
解题总结
一道经典的最长上升子序列问题,也是我第一次尝试解答此类问题,相比于动态规划中的其他大哥,比如路径,背包,分割来说又是不一样的解题思路,在此记录我学习的历程,与各位共勉。
• 时间复杂度: O(n)
,其中n
为字符串的长度。
• 空间复杂度: 由 O(n)
优化为 O(1)
。