题目大意
题目描述
给定一个长度为 的合法括号序列 。
我们定义对 的一个子序列进行向右循环移位操作:选中子序列所在的位置 ,将这些位置上的字符同时更新为:
也就是说,选中的这些位置上的字符整体向右移动一位,最后一个字符移动到第一个被选中的位置。
例如,对于括号序列 ()(()()):
-
若选中第 和第 个位置的字符(分别是
)和(),移位后第 个位置变为(,第 个位置变为),序列变成((())())。 -
若选中第 、、 个位置的字符(分别是
)、(、)),移位后第 个位置变为),第 个位置变为),第 个位置变为(,序列变成())((())。
请计算有多少个非空子序列,使得在对其进行向右循环移位后,原序列 仍然是一个合法的括号序列。由于答案可能很大,请输出其对 取模的结果。
注:两个子序列如果选中的位置集合不同,则被认为是不同的子序列。
输入格式
第一行包含一个整数 (),表示测试用例的数量。
每个测试用例包含两行:
-
第一行包含一个偶数 (),表示括号序列的长度。
-
第二行包含一个长度为 的合法括号序列 。
保证所有测试用例中 的总和不超过 。
输出格式
对于每个测试用例,在一行中输出一个整数,表示满足条件的非空子序列数量对 取模的结果。
样例输入
1 | 4 |
样例输出
1 | 2 |
样例解释
对于第二个测试用例 ()(),以下 个非空子序列在向右移位后能使原序列保持合法:
-
仅选中位置 :移位后序列为
()() -
仅选中位置 :移位后序列为
()() -
仅选中位置 :移位后序列为
()() -
仅选中位置 :移位后序列为
()() -
选中位置 和 :移位后序列为
()() -
选中位置 和 :移位后序列为
(()) -
选中位置 和 :移位后序列为
()() -
选中位置 、、:移位后序列为
(())

这个就是所谓的right shift操作,那么其实就是向右循环移动一位。题目给你一个正规合法的括号串,问你在所有子序列当中,哪些子序列在进行了right shift操作以后还是合法的括号串(注意,子序列进行了right shift操作后,会反作用于原来的正规合法括号串,那么我们就是要看原来的正规合法括号串,在进行了这个操作以后是否还合法)。
思路讲解
这种题目,先猜一手 dp。
CF-1073-D2. Sub-RBS (Hard Version)(注意虚拟节点可能的操作特判)(括号序列 dp 的状态设计)
这道题目使用了不平衡度作为 dp 的状态设计,不知道这个对我们有没有什么启示?

这提示我们可能不是 dp。
我们容易写出如下程序。
1 | for (int i=1;i<=N;++i) { |
对括号串一个比较有用的工具就是这个把括号看成 1,-1,合法的括号串就是其前缀和时刻都不为负数。(当然末尾必须是 0)
这样子我们就能够非常直观的看出,循环移动的影响范围只是从其第一个子序列的 id,到最后一个子序列的 id,其他的启示是不会影响的,因为对于他们的前缀和来说,包含的元素没有变化,和自然也没有变化。
好的,我仔细读完了你的程序。让我先总结你的做法,然后回答你的问题。
你的做法理解
- 末尾是
(的子序列:直接贡献 ,因为最后一位是(时循环移位只会让前缀和增加或不变,恒合法。这部分没问题。 - 末尾是
)且不是最后一位 :你认为"不能选的位置"只有等效第一位——即(且pre_sum = 1的位置(前面所有括号都匹配完了)。排除这些后,其余位置自由选取,贡献 。 - 末尾是最后一位 :特殊处理,加上
ans_else。
你的核心猜想是:对于末尾是)的子序列,只需要排除"等效第一位"(****pre_sum = 1的(****),其他位置都可以自由选取。
答案是:除了等效第一位以外,确实还有其他不能选的情况。
反例:$S = $ (())() (长度 6)
1 | 位置: 1 2 3 4 5 6 |
考虑子序列 ,即选中位置 2 的 ( 和位置 4 的 )。
- 位置 2 的
(的pre_sum = 2,不是等效第一位,你的算法不会排除它。 - 但移位后:,。
- 新序列:
())(()=())(()` - 前缀和: —— 出现负数,不合法。
所以 是无效的,但你的算法把它算进去了。
根本原因:位置 2 是(且pre_sum = 2,它本身看起来"够高"。但从位置 2 到下一个选中位置 4 之间,pre_sum经过了位置 3 的值 1,低于 2。根据正确的条件,选中一个(后,从该位置到下一个选中位置之间,前缀和必须始终 。位置 3 的前缀和是 1,违反了这个条件。
总结规律:一个(位置 能否被安全选入子序列,不仅取决于 本身,还取决于从 到下一个被选位置之间,前缀和是否一直 。这是一个和"下一个选了谁"相关的动态条件,无法简单地通过排除固定的位置集合来处理。等效第一位( 的()只是最极端的情况——它们在 处立刻就违反条件,所以无论如何都不能选。但 的(也可能不能选,取决于后面前缀和是否会掉到 2 以下。
当然这里的结论都是对这个最后一个是 ) 的情况来讲。

你的意思我理解了:
你的规则:在所选子序列的元素中,如果出现形如 (((...) 的模式——即一段连续的(子序列意义上的)( 后面跟着一个 ),设第一个 ( 在原串中的位置为 ,) 的位置为 ,那么原串在 区间内不允许出现 pre_sum 的位置。
初步结论:这个规则是正确的(在子序列末尾元素是 ) 的前提下)。
精确来说,约束区间应该是 而非 (因为位置 是下一个被选中的位置,约束范围到 为止),但差别不大,用 只是稍微更严格一点点。
原因:子序列中每个被选的 ( 到它在子序列中的下一个被选位置之间,前缀和必须 。一段连续的 ((( 加上一个 ) 形成的块,由 telescoping 可得,约束等价于从第一个 ( 到 ) 之前,前缀和都 。
用你之前的反例验证:$S = $ (())() 中子序列 ,这是一个 () 块,,而 ,违反规则,确实非法。
不过要注意:这个规则虽然正确,但它是上下文相关的——同一个 ( 位置,如果下一个选中的是近处的 )(中间前缀和都 ),就合法;如果下一个选中的是远处的 )(中间前缀和掉到了 ),就非法。所以没法简单地把某些位置标记为"不可选",这也是为什么需要用递推/DP 来追踪状态。
1 | _______________[ Sample Testcase #1 INPUT ]_______________ |
1 | }else { |
下面这个 ai 讲的太复杂了,其实就是我的代码要选择前面一段的 ( 话,一定会选前面一段的 ),但这个显然是不对的。但是什么时候都可以抛弃 )吗?显然也不是。什么时候?其实就是当 )的这个 presum>1 的时候才可以跨段。
好,我用第四个样例 $S = $ ()((())()) 给一个具体的被遗漏的子序列。
1 | 位置: 1 2 3 4 5 6 7 8 9 10 |
你的算法按 ) 位置分段:段 [0,2]、段 [2,6]、段 [6,7]、段 [7,9]、段 [9,10]。
考虑子序列 {4, 7}——选位置 4 的 ( 和位置 7 的 )。
验证合法性:移位后 ,,新序列:
1 | 原始: ( ) ( ( ( ) ) ( ) ) |
前缀和 0,1,0,1,0,1,0,1,2,1,0,全部非负,合法。
但你的算法漏掉了它。 原因:
- 位置 4 在段 [2,6] 里,位置 7 在段 [6,7] 里。
- 处理段 [2,6] 时(到达位置 6 的
)),lans = 8计入了sum_dp。这 8 个子序列都是以位置 6 结尾的。 - 但子集 {4} 是没有选位置 6 的——它选了
(在位置 4,一直到位置 6 都没遇到被选的),约束还"悬着"。 - 到了段 [6,7],处理位置 7 时,你用
lans = 2^0 × sum_dp = 10。这 10 只包含"已经没有活跃约束"的子集。子集 {4} 这种"约束还悬着"的,不在sum_dp里,所以 {4,7} 就漏了。
这就是"跨段延续"的意思:位置 4 的(在段 [2,6] 里被选中,但它的约束没有在段 [2,6] 的)处被消解(因为没选位置 6),而是跨过了位置 6,延续到了段 [6,7],在位置 7 的)处才被消解。你的sum_dp没追踪这些"悬而未决"的子集,所以漏计了。
加f1之后,f1 = 6恰好追踪了这 6 个悬而未决的子集:{4}、{5}、{2,4}、{2,5}、{4,5}、{2,4,5}。它们加上位置 7 就构成 6 个被遗漏的合法子序列。
有很多人觉得这道题目是 dp,认为是什么计数 DP 什么之类的,我觉得这个很难算是一个 DP, 或者说你如果你告诉一个比较拉的人,跟他讲这道题是 DP,他肯定是做不出来的,嗯,它更多的还是根据一个性质,然后去进行这么一个做法吧。
首先啊,我们使用前缀和工具不难得出这一结论:

然后我们就要想办法使用该结论进行计数。
哦,我们解释一下这个代码里面的各个东西吧。首先是这个 sum_dp 其实指的是这个包含 )的这个答案数量。lans 计算出来就是这个所有包含这一位 )的这个答案。
我们现在的目标,就是需要计算一下,如果要给下面一个 ) 用的话,合法的,只包含( 的子序列有几个 ?这个数量就是 valid_no_right,那么,我们其实就是要知道,当前,没有 )的子序列(当然是合法的),有多少个?实际上,要求出这个值,最好的方法就是从 lans 里面去求。lans 实际上是 i 自家带的兵,然后从所有的其他地方的兵转移过来的,那么这个其他地方的兵分为从带 )的兵和不带这个的兵。那么 lans-sum_dp 是什么东西呢?这样子看,实际上很难看出来,我们不妨从目标下手,我们其实就是要把所有带 i 这个 ) 的给除掉。
那么 实际上其实就是:
这里一大长串的2的次方实际上就是最初的 lans。那么这里的 lans-1 实际上是把只选了) 的情况给去掉,我们之所以会产生困惑,其实是我因为我们对 lans 有疑问,实际上 lans 是强制选取 i 的 )的,所以排除只选 ) 的情况,只需要 -1 即可。
1 | ll lans=binpow(2,i-1-up_i-invalid_cnt); |
1 | for (int i=1;i<=N;++i) { |
AC代码
AC
https://codeforces.com/contest/2202/submission/364279343
1 | /** |