牛客多校-Link with Bracket Sequence I-(子序列构造原序列问题+差值dp问题)

子序列

题意:
就是给你一个都是小写字母字符串t,现在求有多少长度为m的字符串s,满足t是s的一个子序列。

思考:

  1. 对于这种问题字串够原串方案数的,感觉也是第一次遇到。那么怎么去求呢?其实这有个dp的通用方法。
    比如现串abc
    假如原串aaabaabbbccaccc
    匹配应是aaaba[a]bb[b]ccacc[c]
    规则就是:对于某个被匹配到的字符ch开始,从右边第一个开始,到右边下一个匹配的字符左边为止,不得出现跟ch一样的字符。
    由子序列 S 构造原序列 T,问方案数这类问题,就是要保证:
    如果你在 T 确定 T 的第 i 个字符 ti,匹配的是S中的第p个字符sp
    并且你在 T 确定 T 的第 j 个字符 tj,匹配的是S中的第p+1个字符sp+1
    除了ti = sp,tj = sp+1以外,还要保证ti+1…tj-1所有字符都不包含ti
  2. 那么就可以dp了,定义dp[i][j]为原串匹配的i,现串匹配到j的方案数。转移的时候按上面的公式转移就可以了。
  3. 写完之后发现,确实是可以。但是这个题目给的数据太大了,没法两重循环去dp。那么对于本题就要换一种方法啦。不过思路本质没变,还是这样。那么还是这样的思路的话,我应该先确定现串的第一个在什么地方和原串匹配,确定好之后,前面的所有地方就26个字母随便填了。对于后面的,后面还剩m-i+1位置,第一个位置确定就是i了,那么剩下的所有位置就有C(m-i,n-1)种方案数填,那么剩下没填的呢?上面分析的就是不能和前面第一个匹配的一样,所以就是25个字母随便填。所以枚举第一个位置,然后组合数求一下就可以了。

代码:

dp方法
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2010;

int T,n,m,k;
char va[N];
int dp[M][M];

signed main()
{
	IOS;
	cin>>va+1>>m;n = strlen(va+1);
	dp[0][0] = 1; //都匹配到第0位的时候是1
	for(int i=1;i<=m;i++) //在匹配现串之前你想怎么放就怎么放
	{
		dp[i][0] = dp[i-1][0]*26%mod;
	}
	for(int i=1;i<=m;i++) //匹配到原串的第i位
	{
		for(int j=1;j<=min(i,n);j++) //匹配到现串的第j位
		dp[i][j] = (dp[i][j]+dp[i-1][j-1]+dp[i-1][j]*25%mod)%mod;
	} //要么第i位和第j位进行匹配,那么这个位置就填va[j]。要么第i-1位和j匹配,我的第i位随便放个va[j]不一样的就行。
	cout<<dp[m][n];
	return 0;
}
组合数:
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2010;

int T,n,m,k;
char va[N];
int fact[N],infact[N];

int ksm(int a,int b)
{
	int sum = 1;
	while(b)
	{
		if(b&1) sum = sum*a%mod;
		a = a*a%mod;
		b >>= 1;
	}
	return sum;
}

void init(int x)
{
	fact[0] = infact[0] = 1;
	for(int i=1;i<=x;i++) fact[i] = fact[i-1]*i%mod;
	infact[x] = ksm(fact[x],mod-2)%mod;
	for(int i=x-1;i>=1;i--) infact[i] = infact[i+1]*(i+1)%mod; 	
}

int C(int a,int b)
{
	if(a<b) return 0;
	return fact[a]*infact[b]%mod*infact[a-b]%mod;
}

signed main()
{
	IOS;
	init(2e5+5);
	cin>>va+1>>m;n = strlen(va+1);
	int ans = 0;
	for(int i=1;i<=m-n+1;i++)
	{
		ans = (ans+C(m-i,n-1)*ksm(26,i-1)%mod*ksm(25,m-i+1-n)%mod)%mod;
	}
	ans = (ans%mod+mod)%mod;
	cout<<ans;
	return 0;
}

2021ICPC上海-I. Steadily Growing Steam

题意:
就是给你最多100个物品,每个物品有个质量和价值,然后你最多可以操作s次,可以让某个物品的质量翻倍。现在让你选择一些物品,分到两个集合,分完之后,两个集合的体积要一样。然后你获得的价值就是两个集合所有物品的价值之和。

思考:

  1. 当时看到就发现质量最大才13很敏感,也就是可以把所有质量加起来,去dp。但是我怎么让两个集合的质量都一样呢?如果质量固定就是多少的话,直接dp质量的一半就可以了,但是这不固定呀。写了几个dp都是错的。实际上这个需要换种dp状态,就是维护一个第一个集合和第二个集合的差值是多少。
  2. 那么可以定义dp[i][j][k],为用到前i个物品,第一个集合和第二个集合质量差值为j的时候,操作次数用了不超过k次的最大价值是多少。转移的话就是,如果这个物品放到第一个集合,那么差值就变大,如果放到第二个集合差值就变小,然后同时看看要不要操作这个物品,不过记得要继承,这是定义的不超过。那么最后的答案就是dp[n][0][s]。
  3. 但是第一个和第二个的差值可能是负数,所以还要加给偏移量,都加质量总和的最大值2600就可以了。这样空间也会很有压力,不过发现每次转移都是用到前面一个的状态,所以第一维的100可以变成滚动数组就可以了。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2010;

int T,n,m,s;
int va[101],vb[101];
int dp[2][5201][101];

signed main()
{
	IOS;
	cin>>n>>s;m = 2600;
	for(int i=1;i<=n;i++) cin>>va[i]>>vb[i];
	for(int i=0;i<=5200;i++)
	{
		for(int j=0;j<=s;j++)
		dp[0][i][j] = dp[1][i][j] = -inf;
	}
	for(int i=0;i<=s;i++) dp[0][m][i] = dp[1][m][i] = 0; //刚开始差值为0的时候都是0
	for(int i=1;i<=n;i++)
	{
		for(int j=-m;j<=m;j++)
		{
			for(int k=0;k<=s;k++)
			{
				dp[i&1][j+m][k] = max(dp[i&1][j+m][k],dp[(i-1)&1][j+m][k]);
				dp[i&1][j+m][k] = max(dp[i&1][j+m][k],dp[(i-1)&1][j-vb[i]+m][k]+va[i]);
				if(k>=1) dp[i&1][j+m][k] = max(dp[i&1][j+m][k],dp[(i-1)&1][j-2*vb[i]+m][k-1]+va[i]);
				dp[i&1][j+m][k] = max(dp[i&1][j+m][k],dp[(i-1)&1][j+vb[i]+m][k]+va[i]);
				if(k>=1) dp[i&1][j+m][k] = max(dp[i&1][j+m][k],dp[(i-1)&1][j+2*vb[i]+m][k-1]+va[i]);
			}
		}
	}
	cout<<dp[n&1][m][s];
	return 0;
}

Link with Bracket Sequence I

题意:
就是给你一个长度为n的括号序列,现在问你有多少长度为m的合法括号序列,使得给出的序列是构造的序列的一个子序列。

思考:

  1. 这个也是明显的由现序列构造原序列。那么dp也要用到哪个策略,必须要和前面匹配的第一个不一样。但这里只有两种字母所以也不用乘以多少了。不过你也要保证括号序列的合法性,所以还要维护一个左括号和右括号的差值。这样就可以去转移了。
  2. 定义dp[i][j][k],匹配原串的第i位,现串的第j位,同时左括号和右括号的差值位k的方案数。这里不用偏移量,因为始终要保证左括号多于右括号。
  3. 对于有一点,当匹配到j,如果是左括号,我可以用第i位填左括号与他匹配,但是我也可以填左括号,但是我不和他匹配。但是这样就会错的,因为你这样方案数就多加了,定义的时候就是在匹配到j之后,不能出现与va[j]一样的字符。
  4. 不过左括号的时候我可以填右括号,或者右括号的时候填左括号,这样不会重复,因为我没和前面一次匹配的字母一样。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 201,M = 2010;

int T,n,m,k;
char va[N];
int dp[N][N][N];

signed main()
{
	IOS;
	cin>>T;
	while(T--)
	{
		cin>>n>>m>>va+1;
		for(int i=0;i<=m;i++)
		{
			for(int j=0;j<=n;j++)
			{
				for(int k=0;k<=m;k++)
				dp[i][j][k] = 0;
			}
		}
		dp[0][0][0] = 1;
		for(int i=1;i<=m;i++)
		{
			for(int j=0;j<=i;j++)
			dp[i][0][j] = ((j<1?0:dp[i-1][0][j-1])+dp[i-1][0][j+1])%mod;
		}
		for(int i=1;i<=m;i++)
		{
			for(int j=1;j<=n;j++)
			{
				for(int k=0;k<=i;k++)
				{
					if(va[j]=='(')
					{
						if(k>=1) dp[i][j][k] = (dp[i][j][k]+dp[i-1][j-1][k-1])%mod;
						if(k+1<=m) dp[i][j][k] = (dp[i][j][k]+dp[i-1][j][k+1])%mod;
					}
					if(va[j]==')')
					{
						if(k+1<=m) dp[i][j][k] = (dp[i][j][k]+dp[i-1][j-1][k+1])%mod;
						if(k>=1) dp[i][j][k] = (dp[i][j][k]+dp[i-1][j][k-1])%mod;
					}
				}
			}
		}
		cout<<dp[m][n][0]<<"\n";
	}
	return 0;
}

Link with Bracket Sequence II

题意:
给你一个数组,每种值是一种括号,正就是左括号,负就是右括号。为0的地方就是不确定的。然后一共有m种括号,问你这个给你的数组把0填完后有多少种合法的括号序列。

思考:

  1. 这个括号种类那么多,没法维护每一种括号的左括号和右括号的差值。那么怎么办呢?看到范围,感觉可以区间dp,其实就是对于[l,r]这段区间的方案数怎么算,才是不多不少的。
  2. 我们定义dp[l][r]为区间[l,r]可以产生多少种合法的括号序列,但是这样容易重复计数。()()()可以看为() ()()或者()() ()。所以在枚举中转点的时候要注意一下。枚举到中转点k的时候,如果让va[k]和va[l]匹配就不会重复了。
  3. 那么就是分类如果va[l]>0,那么如果va[k]=0||va[k]=-va[l],那么就可以从dp[l+1][k-1]×dp[k+1,r]来加一次方案数。
    如果va[l]==0,那么如果va[k]==0,那么可以随意放括号dp[l+1][k-1]×dp[k+1,r]×m。如果va[k]<0,那么就还是正常的直接固定转移dp[l+1][k-1]×dp[k+1,r]。
    可以直接传统的枚举区间dp,也可以用记忆化搜索直接搜。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2010;

int T,n,m,k;
int va[N];
int dp[501][501];

int dfs(int l,int r)
{
	if(r-l+1==0) return 1;
	if((r-l+1)%2!=0) return 0;
	if(dp[l][r]!=-1) return dp[l][r];
	int sum = 0;
	if(va[l]==0)
	{
		for(int i=l+1;i<=r;i+=2)
		{
			if(va[i]<0) sum = (sum+dfs(l+1,i-1)*dfs(i+1,r)%mod)%mod;
			if(va[i]==0) sum = (sum+dfs(l+1,i-1)*dfs(i+1,r)%mod*m%mod)%mod;
		}
	}
	if(va[l]>0)
	{
		for(int i=l+1;i<=r;i+=2)
		{
			if(va[i]==0||va[i]==-va[l])
			sum = (sum+dfs(l+1,i-1)*dfs(i+1,r)%mod)%mod;
		}
	}
	dp[l][r] = sum%mod;
	return sum;
}

signed main()
{
	IOS;
	cin>>T;
	while(T--)
	{
		cin>>n>>m;m%=mod;
		for(int i=0;i<=n;i++)
		{
			for(int j=0;j<=n;j++)
			dp[i][j] = -1;
		}
		for(int i=1;i<=n;i++) cin>>va[i];
		int ans = (dfs(1,n)%mod+mod)%mod;
		cout<<ans<<"\n";
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值