括号匹配问题
Level1
最长合法括号子序列的长度(序列不要求连续性)
思路一:每当有括号匹配就加入答案
每当右括号的数量大于左括号的数量的时候,就说明加上当前的右括号就是不合法的了。这时候就把匹配到的括号数量(左括号数量*2)加到答案上去,在把左括号和右括号的数量置为0。注意最后可能会出现((()这种情况,虽然右括号数量小于左括号数量,但仍有合法的括号,所以在循环结束之后需要判断。
注意不能匹配左括号的数量等于有括号的数量,例如 )(,它也会认为是正确的答案,即不合法的括号也会匹配到
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main()
{
string s; cin >>s;
int l = 0, r = 0, res = 0;
for(int i = 0; i < s.length(); i ++ )
{
if(s[i] == '(') l ++ ;
else r ++ ;
if(r > l)//r>=l
{
res += l * 2;
r = l = 0;
}
}
if(l && r) res += r * 2;
cout << res << endl;
return 0;
}
思路二: 计算合法括号的个数,最后加入答案
主要思路就是如果输入一个上括号就把数量存下来,等下括号匹配;
如果输入下括号则也存储下来标志着与前面某一个上括号匹配;
如果下括号数量大于上括号则多余的下括号不能匹配,把多余的下括号给排除掉(b=a);
最后把下括号数量也就是匹配数量*2后输出即可显然这种方法更简洁,方便
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main()
{
string s; cin >>s;
int l = 0, r = 0, res = 0;
for(int i = 0; i < s.length(); i ++ )
{
if(s[i] == '(') l ++ ;
else r ++ ;
if(r >= l)
{
res += l * 2;
r = l = 0;
}
}
if(l && r) res += r * 2;
cout << res << endl;
return 0;
}
Level2
求最长合法括号子串的长度以及数量(注意这里的数量是最长合法括号字串的数量,不是全部合法括号字串的数量)(并且这里是字串而不是序列,要保证连续性)
思路一:栈
首先,一个括号串合法的充分必要条件:
- 左括号的数量等于右括号的数量
- 在所有前缀(字串)中,左括号的数量大于等于右括号的数量
我们可以通过一个变量(cnt)来判断是否满足这两个条件,我们令出现左括号时cnt++,出现右括号时cnt--,则对应的:
- cnt=0
- cnt[i]>=0
然后,我们根据括号序串画出类似卡特兰数的图(y总图),令往右下走的边代表右括号,往右上走的边代表左括号,接下来我们根据画出的图来对应括号串合法的充分必要条件
- 两条端点边在同一高度上(如下图的4,5号边)
- 在这两条边之间的所有边中,所有边的高度都要大于等于端点边的高度(如下图的1号边和7号边,如果中间的边是蓝色的话,那么就是合法的,如果是绿色的话,就是不合法的)
其次,在图形中,任意两个最大合法括号区间不可能重合,并且,任意一对配对边是唯一确定的(比如8的配对边只能是9,不可能是6,也就是配对离它最近的边)
最后,也是最重要的,我们如何根据图形找到最大的合法括号区间。根据上面我们推导得到的性质,一个合法区间的两个端点必须在同一高度,并且所有边的高度都不能小于这个高度,我们可以发现,以某个右括号为结尾的最大区间,就是到距它左边最近的高度比它低的边的距离(例如图中,1号边左边比它小的且距离最近的边是3号边,那么它与3号边之间的距离就是以该右括号为结尾的最长合法括号区间,注意不是7号边),显然不可能以左括号作为合法括号串的结尾,那么我们只需要找到这个高度比它小的边就找到了答案。
我们还可以发现,在这个最长区间当中,中间的边(不包括两个端点)全都可以配对并且可以忽略,而利用栈就可以消去一对匹配的括号(stack是括号问题经常用到的数据结构)。
栈中的具体的操作是:
- 如果我们遍历到一个左括号,直接放入栈
- 如果是一个右括号,并且有配对边,消去这两条边
- 否则,加入右括号
而消去了配对边之后,我们就可以惊喜的发现,在我们遍历到一个括号的时候,栈顶代表的下标值就是我们要找的那个高度更小的边,而这时有两种情况
- 此时以当前遍历到的边为结尾构成的括号串不合法。例如是左括号(显然),或者这样一种序列:()),当我们遍历到第三个括号时,虽然他是个右括号,但是他没有可以配对的括号,因为第二个括号与第一个左括号配对了,那么按照上面的操作,我们只能把这个括号对应的下标放入栈,那么栈顶代表的下标值不就是它自己的下标值吗?所以这里只要不合法,想减之后结果恒为0!
- 此时以当前遍历到的边为结尾构成的括号串合法。显然当前遍历到的边只能是右括号。那么栈顶代表的下标值就是我们我们要找的那个高度更小的边,如果栈为空,那说明前面的所有边都完成配对(包括当前遍历到的边)。
最后,我们只需要把所有答案去一个max就行了!
声明:这里的边和括号可能混用,但意思相同,一个括号在图中就代表一条边,而图中的一条边就代表一个括号
(y总图)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
using namespace std;
string s;
int res, cnt = 1;
stack<int> stk;
int main()
{
cin >> s;
int len = s.length();
for(int i = 0; i < len; i ++ )
{
//如果可以配对
//消去与它配对的边,这条边不再放入栈
if(stk.size() && s[stk.top()] == '(' && s[i] == ')') stk.pop();
//否则,放入栈当中
else stk.push(i);
int t;
//如果栈不为空,栈顶下标就是我们套找的距离当前边最近的且高度小于它的边
//或者当前边构成的括号串不合法
if(stk.size()) t = i - stk.top();
/*
如果栈为空,说明之前的边全部配对
那么距离当前边最近的且高度小于它的边就是下标0左边的边-1,虽然这条边并不存在
但我们可以假设他存在
或者理解为到目前为止所有边都符合条件
*/
else t = i + 1;
if(t > res) res = t, cnt = 1;
else if(t && t == res) cnt ++ ;//t=0就不要++了
}
cout << res << " " << cnt << endl;
return 0;
}
思路二:DP
我们在上面的栈分析中已经分析出来了,一个括号串如果是合法的,那么当前遍历到的边一定是右括号。
状态表示:F[i]表示以s[i]结尾的括号能构成的最长合法括号串
状态分析:(1)如果当前括号时左括号,F[i]=0。(2)是右括号,状态转移
难点在于状态转移,我们可以这样想,因为我们当前便利到了一个右括号,我们肯定希望可以让它拥有一个左括号完成配对,那么这个左括号如果存在的话,在哪呢?根据F[i]的含义,我们可以发现,这个左括号的位置就在(i - d[i-1] - 1)的位置,如果这个位置是一个左括号,那么可以与当前这个右括号构成一个合法组合,否则这个右括号不合法!这个右括号是一个没有配对的括号,就不需要转移了。
状态转移:
- i 与 i − F[i−1] - 1 不匹配 -----》 F[i]=0
- i 与 i − F[i−1] - 1 匹配 -----》F[i] = F[i−1] + 2 + F[i-F[i−1]−2]
其中, d[i - 1] + 2我们好理解,就是当前遍历到的右括号构成的最外层的一个括号和他们中间包裹的一堆合法括号(d[i-1]肯定是合法的),那么d[i - d[i-1] - 2]代表什么呢,我们可以发现(i - d[i-1] - 2)的位置正是我们配对的左括号前面一个位置,其含义可就不言而喻了,我们的右括号消去了一个左括号,原本这个左括号可能是一个分隔符,他分割了两个合法括号串,但现在有一个右括号把它消去了,那么这两个合法括号串就连通了,也就合并成了一个更大的合法括号串,举个例子吧:
原来的括号串(下标从1开始):()(()
当前遍历到一个右括号(i=6)
F[i-1]=F[5]=2,F[i - F[i-1] - 2]=F[2]=2
原本这两个括号对是不连通的,因为他们中间有个(分割了他们
通过F[]的值也可以看出
现在出现了一个右括号:()(())
那么在整体上,这两个括号对就连通了
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
string s;
int len, d[N];
int ans, cnt;
int main()
{
cin >> s;
len = s.size();
s = '0' + s;
for(int i = 1;i <= len;i++)
{
if(s[i] == '(') continue;
else
{
if(s[i - d[i - 1] - 1] == '(')
{
d[i] = d[i - 1] + 2 + d[i - d[i - 1] - 2];
if(ans < d[i]) ans = d[i], cnt = 1;
else if(ans == d[i]) cnt++;
}
}
}
if(ans) cout << ans << ' ' << cnt <<endl;
else cout << "0 1" <<endl;
return 0;
}
Level3
Level2的复杂版(不是更高难度,只是更复杂一点!)
Level2匹配的括号只有()一种形式,在本题中,括号有三种形式:(),{},【】
但思路同Level2相似,同样有DP和栈两种做法
先上DP(与Level2几乎一模一样):
状态表示:设F[i]表示以ii结尾(必须包括i)的最长的美观的子段的长度
状态转移:由原来的只判断一组括号变成需要判断三组括号
- i 与 i − F[i−1] - 1 不匹配 -----》 F[i]=0
- i 与 i − F[i−1] - 1 匹配 -----》F[i] = F[i−1] + 2 + F[i-F[i−1]−2]
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
string s;
int len, d[N];
int ans, cnt;
int main()
{
cin >> s;
len = s.size();
s = '0' + s;
for(int i = 1;i <= len;i++)
{
if(s[i] == '(' || s[i] == '{' || s[i] == '[') continue;
else
{
if(s[i] == ')' && s[i - d[i - 1] - 1] == '('
|| s[i] == '}' && s[i - d[i - 1] - 1] == '{'
|| s[i] == ']' && s[i - d[i - 1] - 1] == '[')
{
d[i] = d[i - 1] + 2 + d[i - d[i - 1] - 2];
if(ans < d[i]) ans = d[i], cnt = 1;
else if(ans == d[i]) cnt++;
}
}
}
// if(ans) cout << ans << ' ' << cnt <<endl;
// else cout << "0 1" <<endl;
cout << ans << endl;
return 0;
}
栈模拟:与Level2相似,也是更改一下判断条件即可
其实我们可以发现DP更简洁明了一点=。=
#include <iostream>
#include <cstring>
#include <stack>
using namespace std;
int main()
{
string str;
cin >> str;
stack<int> stk;
int res = 0;
for (int i = 0; i < str.size(); i ++ )
{
char c = str[i];
if (stk.size())//只需要改一下这里的判断即可
{
char t = str[stk.top()];
if (c == ')' && t == '(' || c == ']' && t == '[' || c == '}' && t == '{') stk.pop();
else stk.push(i);
}
else stk.push(i);
if (stk.size()) res = max(res, i - stk.top());
else res = max(res, i + 1);
}
cout << res << endl;
return 0;
}