Luogu P3059 Concurrently Balanced Strings G 题解 [ 紫 ] [ 线性 dp ] [ 哈希 ] [ 括号序列 ]

Concurrently Balanced Strings G

模拟赛搬的题,dp 思路很明显,但难点就在于找到要转移的点在哪。

暴力

首先我们可以先考虑 \(k=1\) 的情况,这应该很好想,就是对于每一个右括号,找到其匹配的左括号,然后进行转移即可,这个过程可以用栈维护。

\(dp[i]\) 定义为以 \(i\) 为结尾的合法序列个数。假设当前右括号在 \(i\) 处,匹配的左括号在 \(j\) 处,则:

\[dp[i]=dp[j-1]+1 \]

注意一定是要在保证能找到的情况下,转移离自己最近的左括号,才能保证所有括号序列都被统计到了。

最后扫一遍把所有的 \(dp[i]\) 累加即可。考场做法拿了 40pts。

正解

上面的做法,我们发现可以拓展到全局,也就是同时有 \(k\) 个序列的情况。

我们考虑一个括号序列的常用 trick:把左括号看作 \(+1\),把右括号看作 \(-1\),一个括号序列合法,当且仅当其总和为 \(0\) 且任何一段前缀和都 \(\ge 0\)

总和为 \(0\) 很好考虑,我们主要想任何一段前缀和 \(\ge 0\) 怎么搞。

观察到前缀和数组每次相对前一项的变化量要么是 \(1\) 要么是 \(-1\),并且由于先要保证能找到,所以我们先找出可以匹配的左括号的区间左端点。

但是这样并不好做,因为如果 \([l,r]\) 的和 \(<0\)\([l-1,r]\) 的和却不一定 \(<0\)所以固定右括号的方式不可行。

因此,我们才考虑固定左括号,去寻找右括号,并且把 dp 倒着做。

于是找出前缀和 \(<0\) 的就很简单了,对于一个左括号,其最多能匹配到的右括号一定在后面离自己最近的使前缀和 \(<0\) 的地方。

这个我们可以通过从后往前扫描,记录下考虑序列第 \(i\) 位到第 \(n\) 位里面前缀和为每一种数的最小下标,这样我们就可以快速查询在左括号后面,第一个使前缀和 \(<0\) 的右括号在哪了。

如果找不到,就说明右括号在右边的哪里都可以,所以赋为最大值。

算完最大右端点后,我们对于每一列,求出其最大右端点中的最小值,这就是某一列里可能的匹配范围。

接下来考虑总和为 \(0\) 的限制,很容易发现对于前缀和数组而言是这样的:

\[f[i]-f[j-1]=0 \]

可得:

\[f[i]=f[j-1] \]

一个括号序列合法,必须每一行都满足这个条件,也就是说对于两个列而言,每一行的前缀和相同,它才可能合法。

所以我们对每一列哈希,存进 unordered_map,然后统计离自己最近的且在最大右端点左边的相同位即可。

最后来个 dp 就完事了,时间是 \(O(nk)\) 的,但 unordered_map 可能有点常数。

代码

代码还是比较好写的。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
const ll eps=500005,mod=998244353;
int n,k,a[15][50005],f[15][50005],tot[15][110005],r[15][50005],pr[50005],y[50005];
ll hs[50005],dp[50005],ans;
unordered_map<ll,int>mp;
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>k>>n;
	//处理原括号序列、前缀和数组、各列的哈希值
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=n;j++)
		{
			char c;
			cin>>c;
			if(c=='(')a[i][j]=1;
			else a[i][j]=-1;
			f[i][j]=f[i][j-1]+a[i][j];
			hs[j]=(hs[j]*10007%mod+f[i][j])%mod;
		}
	}
	//统计右边最远可达的括号
	memset(tot,0x3f,sizeof(tot));
	memset(r,0x3f,sizeof(r));
	for(int i=1;i<=k;i++)
	{
		for(int j=n;j>=1;j--)
		{
			tot[i][f[i][j]+eps]=j;
			r[i][j]=tot[i][f[i][j-1]-1+eps];
		}
	}
	//记录对于每一列而言的右边界
	memset(pr,0x3f,sizeof(pr));
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=k;j++)
		{
			pr[i]=min(pr[i],r[j][i]);
		}
	}
	//找出相同的哈希值
	for(int i=n;i>=1;i--)
	{
		mp[hs[i]]=i;
		y[i]=mp[hs[i-1]];
	}
	//dp
	for(int i=n;i>=1;i--)
	{
		if(y[i]!=0&&y[i]<=pr[i])
		{
			dp[i]=dp[y[i]+1]+1;
		}
	}
	//统计答案
	for(int i=1;i<=n;i++)
	{
		ans+=dp[i];
	}
	cout<<ans;
	return 0;
}
posted @ 2024-08-17 01:35  KS_Fszha  阅读(27)  评论(0)    收藏  举报