题目:
给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。
示例 1:
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"
题解:
如何分治:
定义 dp[i] 表示以下标 i 字符结尾的最长有效括号的长度。
1.当s[i-1]=='(' && s[i]==')'时,即字符串形如"...()"时
dp[i]=dp[i-2]+2
2.当s[i-1]==')' && s[i]==')'时,即字符串形如"...))"时
(这个情况很复杂,详细分析)
2.1观察这个括号串
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
( | ( | ( | ( | ) | ) | ) | ) |
我们希望计算 dp[7] 的值
已知1~6是有效的括号串,所以 -- 条件1: dp[6]==6; 条件2: s[7 - 6 - 1] 即 s[0] == '('
所以 dp[7] = dp[6] + 2 = 8
所以
if( s[i - dp[i-1] - 1] == '(' ){
dp[i] = dp[i-1] + 2;
}
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
) | ( | ( | ( | ) | ) | ) | ) |
已知1~6是有效的括号串,所以 -- 条件1: dp[6]==6; 条件2: s[7 - 6 - 1] 即 s[0] == ')'
因为条件2 s[0] = ')' ; 所以 dp[7] = 0
然而实际上没有这么简单!!! 在情况2.1的基础上,还有一种进阶情况!!
2.2观察这个括号串
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
( | ( | ( | ( | ) | ) | ) | ) | ( | ( | ) | ) |
我们希望计算 dp[11] 的值
按照前文的情况2.1,
条件1: dp[10]==6; 条件2: s[11 - 2 - 1] 即 s[8] == '('
所以 dp[11] = dp[10] + 2 = 4
上面这个"递推结果"是错误的
为什么?
我发现,
dp[10]对应 "9~10这2个格子"
dp[11]在dp[10]的基础上,向左右两边都拓展了一格, 即"8~11这4个格子"
向左拓展一格对应于 情况2.1 中的这个判断:
s[i - dp[i-1] - 1] == '('
向右拓展一格对应于
s[i] = ')' 即我们正在求解的这一格
由于向左拓展了一格, 8号格子和前面的7号格子相连起来了~~~~
所以,我还需要把 dp[7] 计算在内
假如,7号格子是"某一有效括号串的结尾字符"
dp[11] = dp[10] + 2 + dp[7]
假如,7号格子不是"某一有效括号串的结尾字符"
dp[11] = dp[10] + 2 + 0
可以写出如下伪代码
if( dp[i - dp[i - 1] - 2] > 0 ){
dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2];
}
if( dp[i - dp[i - 1] - 2] == 0 ){
dp[i] = dp[i - 1] + 2 + 0;
}
//精简后实际上只需要一行
dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2];
写出递归树,检查有无漏掉情况
为什么要画递归树?
因为所谓"递归树"就是一种自顶向下分析问题的思路图
动态规划就是自顶向下分析,自底向上求解,所以做动态规划一定要画出递归树
就算已经推导出了状态转移方程,仍然需要画出递归树排查有无思维漏洞!!!!!
递归树可以很直观地看出动态规划的重叠子问题
边界条件:
有效的括号串一定以 ')' 结尾
所以 '(' 字符结尾的括号串有效长度移动为0,即
if(s.charAt(i)=='('){
dp[i]=0;
}
我设置的边界条件出错了,实话说我不知道错在哪
public class Solution {
public int trans(){
return 0;
}
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
Integer size = s.length();
//设置边界条件(似乎又不需要设置???)
for(int i=0;i<size;i++){
if(s.charAt(i)=='('){
dp[i]=0;
}else if(s.charAt(i)==')' && i==0){
dp[i]=0;
}else if(s.charAt(i)==')'){
dp[i]=-1;
}
}
for(int i=0;i<size;i++){
//递归树的"....()"分支
if(i-1>=0){
if(s.charAt(i-1)=='('&&s.charAt(i)==')'){
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
//递归树的"....))"分支
else if(s.charAt(i-1)==')'&&s.charAt(i)==')'){
if(i-dp[i-1]-1>=0){
if(s.charAt(i-dp[i-1]-1)=='('){
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;//加上"三元算符"是为了防止i - dp[i - 1] - 2处位于数组头部,防止数组从起始处越界
}
}
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
public static void main(String[] args) {
}
}
AC的代码
心得:
实际编码的时候和思路的时候不一样
解题思维分几个过程
1.纸上算法推演
2.书写伪代码
3.实际编码
1.纸上算法推演时,只是形成"人类可读"的算法思路
2.书写伪代码 已经开始关注"如何用代码表述算法"但是对于"数组越界,指针越界等"仍然缺乏探讨
3.实际编码要考虑到一切,比如数组越界等
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
//加上"三元算符"是为了防止i - dp[i - 1] - 2处位于数组头部,防止数组从起始处越界
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
Integer size = s.length();
for(int i=0;i<size;i++){
//递归树的"....()"分支
if(i-1>=0){
if(s.charAt(i-1)=='('&&s.charAt(i)==')'){
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
//递归树的"....))"分支
else if(s.charAt(i-1)==')'&&s.charAt(i)==')'){
if(i-dp[i-1]-1>=0){
if(s.charAt(i-dp[i-1]-1)=='('){
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;//加上"三元算符"是为了防止i - dp[i - 1] - 2处位于数组头部,防止数组从起始处越界
}
}
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}