括号序列问题
life_is_movie · · 个人记录
判断是否合法
判断
我们维护一个栈,对于
如果
例题
for (auto x : s)
{
if (x != '(' && x != ')') continue;
if (x == '(') stk[++top] = x;
if (x == ')')
{
if (top) top--;
else
{
cout << "NO\n";
return 0;
}
}
}
if (top) cout << "NO\n";
else cout << "YES\n";
合法括号序列计数
合法的括号序列必须满足
- 左括号数量等于右括号
- 对于任意的前缀,左括号数量大于等于右括号。
所以合法括号序列是一个卡特兰数。
求一个括号序列的合法子串个数
考虑
那么答案就是
状态转移:
使用栈对于每一个左括号开始记录它的位置,当遇到一个右括号,它可以由和自己匹配的位置减去
if (s[i] == '(') stk[++top] = i;
else if (top)
{
dp[i] = dp[stk[top--] - 1] + 1;
}
ans += dp[i];
树上括号序列合法子串个数
例题:csp-s2019T2
考虑给定一个括号序列的时候怎么求合法的括号子串的个数。
借助一个栈做 DP 即可,遇到左括号入栈,遇到右括号尝试匹配。可以证明在括号序列中,每个右括号唯一对应的一个左括号即栈顶的左括号就适合自己去匹配的。
设
状态转移:分为选择第
-
若不选第
i 个右括号,则转移到f_{i-1} -
若选择第
i 个右括号,那么就要和此时栈顶的左括号配对构成一对,设栈顶的位置为j ,则选择后需要转移到f_{j-1}-f_{j-2}+1 首先加1 不必多说因为多了一对括号的贡献,为何是f_{j-1}-f_{j-2} 这个实际是在求以j-1 结尾的合法括号子串的个数,因为我们选择了j,i 这段括号如若转移到之前必须保证连续,如果直接转移到f_{j-1} 这会把不连续的也计算进去,因为我们的状态设计是前i 个字符而不是以i 结尾。
那么我们将这样的问题引入到树上,我们在每次 DFS 的同时记录此时的括号情况,如果回溯我们就弹栈即可。对于从根节点出发到某个点的路径来说其实就是一个 序列。
设
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5 + 5;
ll n, f[N];
string s;
int fa[N];
vector<int> e[N];
int stk[N], top;
void dfs(int u)
{
if (s[u] == '(')
{
stk[++top] = u;
f[u] = f[fa[u]];
for (auto v : e[u]) dfs(v);
top--;
}
else
{
if (!top)
{
f[u] = f[fa[u]];
for (auto v : e[u]) dfs(v);
}
else
{
int t = stk[top--];
// f[fa[u]] 为不选,f[fa[t]] 为栈顶的父亲,f[fa[fa[t]]] 为栈顶的父亲的父亲
f[u] = f[fa[u]] + f[fa[t]] - f[fa[fa[t]]] + 1;
for (auto v : e[u]) dfs(v);
stk[++top] = t;
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> s;
s = " " + s;
for (int i = 2; i <= n; i++)
{
cin >> fa[i];
e[fa[i]].emplace_back(i);
}
dfs(1);
ll res = 0;
for (int i = 1; i <= n; i++) res ^= i * f[i];
cout << res;
return 0;
}
P10115 [LMXOI Round 1] Placer
定义
转移自然是枚举上一个位置
那么
这么转移的时间复杂度为
考虑如何优化转移。
记
当
因此
我们需要在这些状态中找到
我们使用一个数组
则
那么
注意若
#include <bits/stdc++.h>
#define rep(i, j, k) for (int i = j; i <= k; i++)
#define per(i, j, k) for (int i = j; i >= k; i--)
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
const int N = 3e6 + 5, mod = 998244353;
int n, a[N], stk[N], top, lst[N];
ll f[N], g[N];
string s;
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> s;
s = " " + s;
rep(i, 1, n) cin >> a[i];
rep(i, 1, n)
{
if (s[i] == '(') stk[++top] = i;
else if (top) lst[i] = stk[top--];
}
memset(f, 0xc0, sizeof(f));
memset(g, 0xc0, sizeof(g));
f[0] = 0;
rep(i, 1, n)
{
f[i] = f[i - 1];
if (lst[i])
{
g[i] = max(f[lst[i] - 1] - a[lst[i]], g[lst[i] - 1]);
f[i] = max(f[i], g[i] + a[i]);
}
}
cout << f[n];
return 0;
}
[蓝桥杯 2021 省 AB] 括号序列
合法的括号序列满足如下条件:
- 左括号数量等于右括号数量
- 任意前缀左括号个数大于等于有括号的前缀。
本题需要我们求多少种本质不同的添加结果,首先如果添加一对括号是没有意义的,因此我们可以将添加左右括号分别来处理。
例如求解添加左括号的方案,我们可以采取如下方法:
以原字符串的每个右括号为一个结尾,这样可以将字符串分成若干段,对每一段内去考虑添加多少个左括号,如此可以打达到本质不同。
定义
- 若
s[i] = '(',此时f_{i,j}=f_{i-1,j-1} - 若
s[i] = ')',此时我们就在这个段内添加左括号。由于目前左比右多j 个,因此可以添加的数量为0\sim j 个。
若我们添加了
若我们添加了
即
由于
所以
这是添加左括号的情况,那么添加右括号同理,以每一个左括号作为结尾,分成若干段,在这个段内填右括号。
我们可以将原字符串翻转,翻转以后左右对调来处理。
在一个段内即填左又填右,会产生新的贡献吗?
答案是不会的,因为如果在一个段内填写了匹配的括号,去掉它是一样的,所以段内的方案是确定的。
由于要填写最少的括号变得合法,那么答案就是枚举
#include <bits/stdc++.h>
#define rep(i, j, k) for (int i = j; i <= k; i++)
#define per(i, j, k) for (int i = j; i >= k; i--)
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pll = pair<long, long>;
using a3 = array<int, 3>;
const int N = 5e3 + 5, mod = 1e9 + 7;
string s;
ll n, f[N][N];
ll solve()
{
memset(f, 0, sizeof(f));
f[0][0] = 1;
for (int i = 1; i <= n; i++)
{
if (s[i] == '(')
{
for (int j = 1; j <= n; j++) f[i][j] = f[i - 1][j - 1];
}
else
{
f[i][0] = (f[i - 1][0] + f[i - 1][1]) % mod;
for (int j = 1; j<= n; j++) f[i][j] = (f[i - 1][j + 1] + f[i][j - 1]) % mod;
}
}
for (int i = 0; i <= n; i++) if (f[n][i]) return f[n][i];
return 0;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
cin >> s;
n = s.size();
s = " " + s;
ll l = solve();
reverse(s.begin() + 1, s.end());
for (int i = 1; i <= n; i++)
{
if (s[i] == ')') s[i] = '(';
else s[i] = ')';
}
ll r = solve();
cout << (l * r) % mod;
return 0;
}