题意
有n个只含左右括号的字符串(长度不超过300),求选出几个字符串组成一个括号匹配的字符串的最大长度。
题解
开始考虑时,想用三个状态变量表示状态。定义表示处理到第
个字符串,待匹配的左右括号数分被为
和
时的最大长度。状态转移方程为
。实现时用刷表的方法。
然而这样写有一个很大的问题,这种dp只能处理左右待匹配的括号数最大只有300的情况。而本题最多会有90000个待匹配的左(右)括号。这样的话,开三维数组开不下,而且时间复杂度也太高。
之后看了网上代码,发现了一种优化算法。
再次分析问题可以发现,所有的括号字符串可以归纳为如下形式,*表示左右括号已匹配的子段,不同字符串未匹配的括号数不同,且右括号在左,左括号在右。而要将几个字符串组合起来,第一字符串必然为
,最后一个必然为
。
这样,就可以得到另一种解决这个问题的步骤。先将所有字符串排列成如下形式:
之后依次选取字符串,这样就可以保证已经拼好的字符串中只有未匹配的左括号。整个过程中,拼接字符串的未匹配左括号数会先增大,之后减小到0。由此状态得到简化,可定义为表示处理到第i个字符串且已拼好的字符串的待匹配左括号数为j的最大长度。状态转移方程也就可以直接参考01背包。
回顾整个过程,个人认为比较重要的一步是将字符串进行了排序(预处理)。这种预处理与题意紧密相连,可以看作一种找规律。
在该题中,这种规律的目的是减少我们要考虑的决策。在自己最开始考虑的状态中,要考虑将字符串拼接到左边还是右边,或是不拼。而在之后的状态中,只需要考虑是否将该字符串拼到右边。
不过这种方法是否是一个普遍方法还有待检验。
代码细节日后补。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 305;
int dp[maxn][maxn*maxn];
int n;
int len[maxn],L[maxn],R[maxn],r[maxn];
bool cmp(const int a,const int b){
if(!(R[a]) ^ !(R[b])) return R[a]<R[b];
if(!(L[a]) ^ !(L[b])) return L[a]>L[b];
return L[a]-R[a]>L[b]-R[b];
}
int main(){
scanf("%d", &n);
char st[maxn];
for(int i = 1; i<=n; i++){
r[i] = i;
scanf("%s", st);
len[i] = strlen(st);
L[i] = R[i] = 0;
int cnt = 0;
for(int j = 0; j<len[i]; j++){
if(st[j]=='(') ++cnt;
else{
if(cnt>0) --cnt;
else R[i]++;
}
}
L[i] = cnt;
}
sort(r+1, r+n+1, cmp);//这里用了外排序
int MAX = maxn*maxn;
for(int i = 0; i<=n; i++)
for(int j = 0; j<MAX; j++)
dp[i][j] = -0x3f3f3f3f;//...
dp[0][0] = 0;
for(int i = 1; i<=n; i++){
int dv = L[r[i]]-R[r[i]];
for(int j = 0; j<MAX; j++){
dp[i][j] = max(dp[i-1][j],dp[i][j]);
if(j-dv<MAX && j>=L[r[i]]) dp[i][j] = max(dp[i][j], dp[i-1][j-dv]+len[r[i]]);
}
}
printf("%d", dp[n][0]);
}