1 条题解

  • 0
    @ 2024-4-6 10:07:11

    【2021年省赛B组】试题J: 括号序列

    题解

    step 1

    我们将左括号的值看作1, 左括号的值看作-1。 定义sum[i]sum[i]表示前ii个括号的和, nn表示序列的长度, 那么对于一个合法序列,其必定满足i(1,n),sum[i]0\forall i \in (1, n), sum[i] \geqslant 0sum[n]=0sum[n] = 0

    step 2

    我们需要将添加括号使用原括号序列成为合法括号序列。 题目要尽可能少的添加括号,所以我们添加的左括号必然只能和原序列中的右括号匹配;我们添加的右括号必然只能和原序列中的左括号匹配。若我们添加的左括号和我们添加的右括号匹配了,那么它们无疑是一对没有意义的添加。 于是添加的左括号的方案只和原序列中的右括号相关,添加的右括号的方案只和原序列中的左括号相关,所以左右括号的添加是相互独立的。设左括号的添加方案为f1f1, 右括号的添加方案为f2f2, 那么显然最后的答案就是f1×f2f1 \times f2

    step 3

    60pts

    我们先考虑左括号的添加方案数。 设原序列中有cntcnt个右括号, 我们以原序列中的右括号为分界点, 那么一共可以划分出cnt+1cnt+1个区域。我们只能在第11~cntcnt个区域添加左括号,因为若是在第cnt+1cnt + 1​​区域添加左括号, 将不会有右括号可以与它匹配。

    image-20240406003252177

    定义dp[i][j]dp[i][j]表示前ii个区域(右括号), 在我们添加了若干个左括号后,添加了左括号一共有jj个左括号的方案数。对于两个不同的方案,它们至少有一个区域添加的左括号的个数不相同,于是可得:

    $$dp[i][j] = dp[i - 1][0] + dp[i - 1][1] + dp[i - 1][2] + \cdots + dp[i - 1][j] $$

    注意:

    由于我们要的是合法序列,所以当右括号的个数为ii时,它前面的左括号的个数必然大于等于ii。我们定义sum[i]sum[i]表示第ii个右括号之前的左括号个数, 当i>sum[i]i > sum[i]时, jj必须满足jisum[i]j \geqslant i - sum[i]

    for(int i  = 2; i <= n; i++)
    {
        for(int j = max(0ll, i - sum[i]); j <= cnt; j++)
        {
            for(int k = 0; k <= j; k++)
            {
                dp[i][j] += dp[i - 1][k];
                dp[i][j] %= mod;
            }
        }
    }
    

    于是添加左括号的方案数就计算完成了。对于右括号的添加方案,我们可以将原括号序列反转,并令( -> ), (->),然后按照求左括号的方案数的方法再求一遍即可。

    image-20240406083600241

    image-20240406084219750

    image-20240406092232605

    提交代码

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N = 5e3 + 10, mod = 1e9 + 7;
    string s;
    int dp[N][N], sum[N];
    
    int calc(int cnt, bool flag)
    {
    	if(!cnt) return 1;
    	memset(dp, 0, sizeof dp);
    	memset(sum, 0, sizeof sum);
    	memset(pre, 0, sizeof pre);
    	
    	if(!flag)
    	{
    		reverse(s.begin(), s.end());
    		for(int i = 0; i < s.size(); i++)
    		{
    			if(s[i] == ')') s[i] = '(';
    			else s[i] = ')';
    		}
    	}
    	
    	int n = 0, now = 0;
    	for(int i = 0; i < s.size(); i++)
    	{
    		if(s[i] == ')') sum[++n] = now;
    		if(s[i] == '(') now++;
    	}
    	
    	if(sum[1] > 0) dp[1][0] = 1;
    	for(int i = 1; i <= cnt; i++) dp[1][i] = 1;
    	
    	for(int i  = 2; i <= n; i++)
    	{
    		for(int j = max(0ll, i - sum[i]); j <= cnt; j++)
    		{
    			for(int k = 0; k <= j; k++)
    			{
    				dp[i][j] += dp[i - 1][k];
    				dp[i][j] %= mod;
    			}
    		}
    	}
    	return dp[n][cnt];
    }
    void solve()
    {
    	cin >> s;
    	int tot  = 0, cnt1 = 0, cnt2 = 0;
    	for(auto x : s)
    	{
    		if(x == '(') tot++;
    		else tot--;
    		
    		if(tot < 0) cnt1++, tot = 0;
    	}
    	cnt2 = tot;
    	cout << calc(cnt1, 1) * calc(cnt2, 0) % mod << "\n";
    }
    signed main()
    {
    	std::ios::sync_with_stdio(false);
    	std::cin.tie(0);
    	int t = 1;
    	while(t--) solve();
    	return 0;
    }
    

    100pts

    上述O(n3)O(n^3)做法怎么优化呢?

    其实很简单,如果你做过完全背包,那么应该一眼就能想到使用前缀和来优化。

    对于$dp[i-1][0] + dp[i - 1][1] + dp[i - 1][2] + \cdots + dp[i - 1][j]$,我们可以用其他的式子来代替。

    即$dp[i][j] = (dp[i - 1][0] + dp[i -1][1] + \cdots + dp[i - 1][j] + dp[i - 1][j + 1]) - dp[i - 1][j + 1] \\= dp[i][j +1] - dp[i - 1][j + 1]$. 移项得$dp[i][j + 1] = dp[i][j] + dp[i - 1][j + 1]。 j\in (i -sum[i], cnt), j + 1 \in (i - sum[i] - 1, cnt - 1)$.

    再令j=j1j = j -1,得dp[i][j]=dp[i][j1]+dp[i1][j]dp[i][j] = dp[i][j-1] + dp[i - 1][j], 这样就可以去掉一层循环,复杂度为O(n2)O(n^2)

    不过这是前缀优化,也就是要使dp[i][j]=dp[i1][j]+dp[i][j1]dp[i][j] = dp[i - 1][j] + dp[i][j - 1],需要满足所有的dp[i][j]=k=0jdp[i1][k]dp[i][j] = \sum\limits_{k = 0}^j dp[i - 1][k].由于上述提供的写法仅当j[(isum[i]),cnt]j\in [(i - sum[i]), cnt]时才满足dp[i][j]=k=0jdp[i1][k]dp[i][j] = \sum\limits_{k = 0}^{j} dp[i - 1][k], 所以dp[i][j]=dp[i1][j]+dp[i][j1]dp[i][j] = dp[i - 1][j] + dp[i][j - 1]的转移并不完全合法。 不过我们可以用类似的方法来优化:

    • 定义pre[j]pre[j]表示k=1jdp[i1][k]\sum\limits_{k = 1}^{j} dp[i - 1][k]
    • j[0,isum[i1]]j \in [0, i - sum[i -1]]时, pre[j]=0pre[j] = 0
    • j[(isum[i]),cnt]j \in [(i - sum[i]), cnt]时,pre[j]=pre[j1]+dp[i1][j]pre[j] = pre[j - 1] + dp[i - 1][j]
    • 那么当j[(isum[i]),cnt]dp[i][j]=pre[j]j \in [(i - sum[i]), cnt], dp[i][j] = pre[j]。 这样时间复杂度就可以优化到O(n2)O(n^2)了。

    image-20240406090451024

    提交代码

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N = 5e3 + 10, mod = 1e9 + 7;
    string s;
    int dp[N][N], sum[N], pre[N], nex[N];
    
    int calc(int cnt, bool flag)
    {
    	if(!cnt) return 1;
    	memset(dp, 0, sizeof dp);
    	memset(sum, 0, sizeof sum);
    	memset(pre, 0, sizeof pre);
    	
    	if(!flag)
    	{
    		reverse(s.begin(), s.end());
    		for(int i = 0; i < s.size(); i++)
    		{
    			if(s[i] == ')') s[i] = '(';
    			else s[i] = ')';
    		}
    	}
    	
    	int n = 0, now = 0;
    	for(int i = 0; i < s.size(); i++)
    	{
    		if(s[i] == ')') sum[++n] = now;
    		if(s[i] == '(') now++;
    	}
    	
    	if(sum[1] > 0) dp[1][0] = 1, pre[0] = 1;
    	for(int i = 1; i <= cnt; i++) dp[1][i] = 1, pre[i] = pre[i - 1] + 1;
    	
    	for(int i  = 2; i <= n; i++)
    	{
    		for(int j = 0; j <= cnt; j++) nex[j] = 0;
    		for(int j = max(0ll, i - sum[i]); j <= cnt; j++)
    		{
    			dp[i][j] = pre[j];
    			if(j - 1 < 0) nex[j] = dp[i][j];
    			else nex[j] = (nex[j - 1] + dp[i][j]) % mod;
    		}
    		for(int j = 0; j <= cnt; j++) pre[j] = nex[j];
    	}
    	return dp[n][cnt];
    }
    void solve()
    {
    	cin >> s;
    	int tot  = 0, cnt1 = 0, cnt2 = 0;
    	for(auto x : s)
    	{
    		if(x == '(') tot++;
    		else tot--;
    		if(tot < 0) cnt1++, tot = 0;
    	}
    	cnt2 = tot;
    	cout << calc(cnt1, 1) * calc(cnt2, 0) % mod << "\n";
    }
    signed main()
    {
    	std::ios::sync_with_stdio(false);
    	std::cin.tie(0);
    	int t = 1;
    	while(t--) solve();
    	return 0;
    }
    
    • 1

    信息

    ID
    985
    时间
    5000ms
    内存
    512MiB
    难度
    9
    标签
    递交数
    8
    已通过
    2
    上传者