链接:https://ac.nowcoder.com/acm/problem/21303 来源:牛客网 题号:NC21303 时间限制:C/C++/Rust/Pascal 1秒,其他语言2秒 空间限制:C/C++/Rust/Pascal 512 M,其他语言1024 M 64bit IO Format: %lld 题目描述 给你一个合法的括号序列s1,每次你可以删除一个"()" 你可以删除0个或者多个"()" 求能否删成另一个括号序列s2 输入描述: 第一行输入一个字符串s (2 ≤ |s| ≤ 100) 第二行输入一个字符串t (2 ≤ |t| ≤ 100 ) 输出描述: 如果可以输出"Possible" 否则输出"Impossible"

比较有意思的题目,如果能想明白括号是怎么删的就不难解决,用动态规划是因为每步走完后会产生新状态,一种情况可能有好几种状态, 代码2里面的k就是记录状态数的

每一步可能遇到哪些情况?

1.匹配,"("与"("或者")"与")"

2.不匹配,"("与")""或者")"与"("

不匹配就要删括号,难点来了,当时思考")"前面全是匹配过的"("怎么办,比如()(())变成(()),直接回退是不行的,遇到"(())(()()())"删成"(()()())"这种情况会直接乱套,

怎么办?细想会发现,如果括号序列1能合法删成括号序列2,括号序列2中如果有x个连续的'(',那么序列1中也一定要有x个连续的'(',很多题解说不会出现要删的'('数量一定是大于等于')'就是这个原因.(假设序列2有三个'('连续,那么就在序列1三个'('连续的位置开始匹配)。

现在再来看"()(())"删成"(())"这种情况,"()"完全是不需要匹配的,那么怎么用代码实现呢?处理'('与'('时不止是拓展"匹配成功,往后看",还要考虑"放弃匹配往后看"。

怎么保证删的括号一定合法?

在串1要多删字符才能到串2时,只选择删,不选择匹配,这样删的括号一定是匹配的,而且可以让序列1从连续左括号数相同的位置开始与序列2匹配。 代码1

dp[i][j][k]表示括号串1的前i位与括号串2的前j位在1多删除k个左括号后是否可以匹配

#include<vector>
#include<string>

using namespace std;
void DP(const string& s, const string& evil)
{
	int s_len = s.length();
	int evil_len = evil.length();
	vector<vector<vector<bool>>>dp(s_len + 1, vector<vector<bool>>(evil_len+1,vector<bool>(s_len/2+1,false)));
	dp[0][0][0] = true;//两串前0个字符可以在不多删左括号时匹配
	for (int j = 0; j <= evil_len; j++)
	{
		for (int i = 0; i <= s_len; i++)
		{
			for (int k = 0; k <dp[i][j].size(); k++)
			{
				if (!dp[i][j][k])continue;//过滤掉未出现的情况
				if (k==0&&s[i] == evil[j])
				{
					if(i+1<=s_len&&j+1<=evil_len)
					dp[i+1][j+1][k] = true;//考虑匹配
					if (i + 1 <= s_len && s[i] == '(')
						dp[i + 1][j][k + 1] = true;//考虑不匹配,待删左括号数加一
				}
				//不匹配或者有待删左括号
				else if (i + 1 <= s_len && s[i] == '(')
				{
						dp[i+1][j][k + 1] = true;//待删左括号加一
				}
				else if (s[i] == ')')
				{
					if (i + 1 <= s_len && k - 1 >= 0)
						dp[i+1][j][k - 1] = true;//待删左括号减一
				}
			}
		}
		
	}
	//debug
	/*for (int i = 0; i <= s_len; i++)
	{
		for (int j = 0; j <= evil_len; j++)
		{
			for (int k = 0; k <= evil_len / 2; k++)
			{
				cout << "dp[" << i << "][" << j << "][" << k << "]:" << dp[i][j][k] << endl;
			}
		}
	}*/
	if (dp[s_len][evil_len][0])cout << "Possible";
	else cout << "Impossible";
}
int main(void)
{
	string s, evil;
	cin >> s >> evil;
	DP(s, evil);
	return 0;
}

代码2

代码1考虑情况是设dp为true,过滤掉不存在的情况,这个代码是直接开情况

#include<string>
#include<vector>

using namespace std;


void DP(const string&s,const string&evil)
{
	int s_len = s.length();
	int evil_len = evil.length();
	vector<vector<vector<int>>>dp(s_len+1, vector<vector<int>>(evil_len+1,vector<int>()));//s前i个字符,evil前j个字符,有k种情况对应待删左括号数
	dp[0][0].push_back(0);
	for (int j = 0; j <= evil_len; j++)
	{
		for (int i = 0; i <= s_len; i++)
		{
			for (int k = 0; k < dp[i][j].size(); k++)
			{
				if (s[i] == evil[j]&&dp[i][j][k]==0)
				{
					if(i+1<=s_len&&j+1<=evil_len)
					dp[i + 1][j + 1].push_back(0);
					if (i + 1 <= s_len && s[i] == '(')
						dp[i + 1][j].push_back(1);
				}
				else if (i + 1 <= s_len && s[i] == '(')
				{
					dp[i + 1][j].push_back(dp[i][j][k] + 1);
				}
				else if (i + 1 <= s_len && s[i] == ')')
				{
					if(dp[i][j][k]>0)
					dp[i + 1][j].push_back(dp[i][j][k]-1);
				}
			}
		}
	}
	//debug
	/*for (int i = 0; i <= s_len; i++)
	{
		for (int j = 0; j <= evil_len; j++)
		{
			for (int k = 0; k < dp[i][j].size(); k++)
			{
				cout << "dp[" << i << "][" << j << "][" << k << "]:" << dp[i][j][k] << endl;
			}
		}
	}*/
	bool ans = false;
	for (int k = 0; k < dp[s_len][evil_len].size(); k++)
	{
		if (dp[s_len][evil_len][k] == 0)ans = true;
	}
	if (ans)cout << "Possible";
	else cout << "Impossible";
}
int main(void)
{
	string s, evil;
	cin >> s >> evil;
	DP(s, evil);
	return 0;
}

代码2虽然AC了,但是内存比代码1多了将近一倍,实际上绝对不止一倍,如果字符串更长的话这个代码内存增长是指数级的,因为这个代码会重复记录效果相同的情况,把debug注释去掉就明白了,i,j相同,有的k对应的是相同的,如果统计dp[s_len][evil_len][k]里面值为0的dp的个数,得到的应该是把s合法删成evil的方案数(个人理解,未验证哈),要针对这题优化的话,可以考虑用map去重

这题的dp就是不断增加合法新情况,在新情况的条件上再加合法新情况,是不是对动态规划“把问题化成重叠子问题”有新理解了呢