<think>我们面对的问题:有n张黑白光盘排成一行,每次合并相邻两张:如果颜色相同,则合并后变为该颜色;如果颜色不同,则可以任意选择合并后的颜色(黑或白)。经过n-1次合并后,最终只剩一张光盘,问最后得到黑色光盘的方案数(对998244353取模)。
分析:
这个问题可以动态规划解决。我们定义dp[i][j][c]为将区间[i, j]的光盘合并成颜色c(0表示白色,1表示黑色)的方案数。
但是,注意合并规则:相邻两个合并,相同颜色合并后就是该颜色,不同颜色合并后可以任意选择颜色。
状态转移:
考虑区间[i, j],我们将其分成两部分:区间[i, k]和区间[k+1, j],其中k从i到j-1。
然后,将左右两个区间合并的结果再进行合并。合并规则如下:
设左边区间合并后的颜色为a,右边区间合并后的颜色为b。
如果a==b,则合并后颜色只能是a(或b)。
如果a!=b,则合并后可以选择黑色或白色(即两种选择)。
因此,状态转移方程为:
对于区间[i, j]合并成颜色c:
dp[i][j][c] = sum_{k=i}^{j-1} [
( dp[i][k][c] * dp[k+1][j][c] ) // 左右都是c,合并后一定是c,贡献一种情况
+ ( dp[i][k][c] * dp[k+1][j][1-c] + dp[i][k][1-c] * dp[k+1][j][c] ) * 1 // 左右不同,但合并后我们选择c,那么当左右分别是(c,1-c)或(1-c,c)时,都可以通过一次选择变成c,所以每种情况贡献1倍
+ ( dp[i][k][1-c] * dp[k+1][j][1-c] ) * 0 // 注意:左右都是1-c(即与c相反的颜色),那么合并后只能是1-c,所以不会产生c,因此为0
]
但是,上面这个转移并不完全正确。实际上,当左右都是c时,合并后一定是c(1种情况);当左右颜色不同时(即一个c一个非c),合并后可以选择为c(此时有1种选择)或者非c(我们不关心,所以这里我们只考虑c的情况,所以乘以1);当左右都是非c(即1-c)时,合并后一定是1-c,不可能得到c,所以为0。
因此,我们可以简化为:
dp[i][j][c] = 0;
for (int k = i; k < j; k++) {
dp[i][j][c] = (dp[i][j][c] +
dp[i][k][c] * dp[k+1][j][c] % mod +
dp[i][k][c] * dp[k+1][j][1-c] % mod +
dp[i][k][1-c] * dp[k+1][j][c] % mod) % mod;
}
注意:在左右颜色不同的情况下,我们只加上左右分别取(c,1-c)和(1-c,c)的情况,并且这两种情况下,我们都可以通过选择操作使得合并结果为c(且只有1种选择),所以直接相加即可。
然而,这样计算可能会重复计算?实际上不会,因为我们将区间划分点k从i到j-1枚举,并且左右子区间是独立的。
但是,这里有一个重要的问题:在合并左右两个子区间时,我们只关心它们合并后的颜色,而不关心它们内部有多少种方案,所以这样转移是合理的。
边界条件:
dp[i][i][color[i]] = 1 // 初始时,单个光盘,如果是黑色,那么黑色方案数为1,白色为0;如果是白色,则白色为1,黑色为0。
dp[i][i][1-color[i]] = 0
但是,注意题目输入:我们用一个数组来表示初始的光盘颜色,假设黑色用1表示,白色用0表示。
然而,n最大可能达到几千(题目没说,但动态规划O(n^3)可能超时),所以我们需要优化。
实际上,这个问题有O(n^2)的解法。但是,我们先考虑O(n^3)的动态规划,如果n比较小(比如几百)就可以通过。
题目并没有给出n的范围,但根据取模和方案数的性质,很可能n会达到1000以上,所以O(n^3)不可行。
我们需要更高效的动态规划。这里可以使用平行四边形优化或者重新定义状态。实际上,这个问题与“多边形三角剖分”类似,可以用区间dp的优化技巧,但常见的优化可能无法降到O(n^2)。
另一种思路:使用卡特兰数相关?或者组合数学?但合并操作有任意选择的特性,所以可能不是标准卡特兰数。
重新思考:合并操作具有结合性,但这里合并规则是相邻的,并且合并顺序任意。我们考虑用栈来模拟?或者用树形结构?
然而,有一个经典的类似问题:石子合并(但是带状态)。这里光盘的颜色只有两种,所以我们可以考虑另一种状态设计。
参考:ZOJ-3469 (题目类似?)实际上,有一个经典问题:表达式计算(添加括号)求方案数。
但是,我们观察到颜色只有黑白两种,所以我们可以用两个状态表示一个区间合并后为黑色和白色的方案数。
定义:
dp[i][j][0]: 将区间[i,j]合并成白色的方案数。
dp[i][j][1]: 将区间[i,j]合并成黑色的方案数。
转移方程:
for (int k = i; k < j; k++) {
ll black_left = dp[i][k][1], white_left = dp[i][k][0];
ll black_right = dp[k+1][j][1], white_right = dp[k+1][j][0];
// 合并后为黑色(1)的情况:
// 1. 左右都是黑色:合并后一定是黑色(1种方式)
// 2. 左右一个黑一个白:合并后可以选择黑色(1种选择)-> 两种情况:左黑右白,左白右黑
// 3. 左右都是白色:合并后一定是白色,不能得到黑色(0)
dp[i][j][1] = (dp[i][j][1] +
black_left * black_right % mod + // 黑+黑 -> 黑(1种)
black_left * white_right % mod + // 黑+白 -> 可以选择黑(1种选择)
white_left * black_right % mod // 白+黑 -> 可以选择黑(1种选择)
) % mod;
// 合并后为白色(0)的情况:
// 1. 左右都是白色:1种
// 2. 左右一黑一白:选择白色(1种选择)
// 3. 左右都是黑色:得到黑色,不能得到白色(0)
dp[i][j][0] = (dp[i][j][0] +
white_left * white_right % mod +
black_left * white_right % mod +
white_left * black_right % mod
) % mod;
}
但是,这样写的话,在合并后为白色时,我们加上了黑+白和白+黑的情况(并且乘以1),这表示在左右不同时,我们选择白色。所以正确。
然而,这个动态规划的时间复杂度是O(n^3),n最大可能500?但题目没有说明,如果n=1000,那么状态数有O(n^2),转移O(n),总共O(n^3)=10^9,可能超时(在C++中需要优化)。
但是,如果我们只能想到O(n^3)的方法,可以尝试优化常数或者使用平行四边形优化(但是平行四边形优化要求代价函数满足四边形不等式,这里方案数是组合的,不一定满足)。
另一种思路:使用分治与FFT?或者生成函数?但太复杂。
实际上,这个问题有一个更高效的做法:使用卡特兰数相关的状态或者链式结构。
我们注意到:合并操作最终结果只与初始序列中连续相同颜色段的数量有关。为什么呢?
合并规则:相邻两个合并,相同颜色合并后不变,不同颜色合并后可以任选。这类似于一个表达式求值,但具有结合性。
例如:B W B
先合并前两个:B和W -> 可以变成B或W(两种选择)
然后合并:如果前两个合并成B,那么B和B合并成B;如果前两个合并成W,那么W和B合并可以选择B或W。
所以最后得到黑色的方案数:第一种情况(前两个合并成B)然后合并成B:1种;第二种情况(前两个合并成W)然后选择合并成B:1种。所以总共2种。
但是,如果我们改变合并顺序:先合并后两个:W和B -> 可以变成W或B(两种选择)
然后合并:如果后两个变成W,那么B和W合并可以选择B或W(选B:1种);如果后两个变成B,那么B和B合并成B(1种)。所以也是2种。
那么,我们是否可以按连续段来设计状态?
定义:f(l, r, c) 表示将区间[l, r]合并成颜色c的方案数。但是连续段可以合并吗?连续段内部同色,但连续段之间的合并可能会产生新的连续段?不一定。
然而,有一个经典问题:矩阵链乘,这里合并操作具有结合律,因此我们可以用区间dp。
但是,O(n^3)对于n=500已经比较极限(500^3=1.25e8,在C++中可能勉强通过(如果优化常数)),如果n=1000则1000^3=1e9,会超时。
我们需要更高效的算法。
事实上,这个问题可以转化为:给一个序列,每个位置是0或1,然后建立一棵二叉树(叶节点是初始值,内部节点表示合并操作)。每个内部节点的值:
如果左右儿子相同,则节点值等于儿子值;
如果左右儿子不同,则节点值可以任意选择(0或1)。
问根节点为1的二叉树有多少种?
我们可以在序列上做区间dp,但也可以使用更高效的树形dp?但序列的顺序是固定的(从左到右),所以只能区间dp。
然而,有一个O(n^2)的算法:使用四边形优化或者Knuth优化?但是区间dp的四边形优化要求代价函数满足四边形不等式,这里的代价函数是组合数,不满足?
或者,我们可以用分治加记忆化搜索,但复杂度仍然是O(n^3)。
由于题目没有给出n的范围,我们假设n最大500(这样O(n^3)可接受)。如果n更大(比如1000),则需要进一步优化。
但是,在竞赛中,如果n=500,O(n^3)可能勉强通过(在C++中,常数小的话,1e8次操作大约1秒,1.25e8次操作大约1.25秒,而题目可能n=500,所以可以尝试)。
因此,我们采用区间dp,状态数为n^2(即O(n^2)),每个状态需要枚举分割点(O(n)),总时间复杂度O(n^3)。
步骤:
1. 输入:整数n,然后一个字符串s(长度为n),字符'B'表示黑色,'W'表示白色。
2. 初始化dp[i][i][0]和dp[i][i][1]:如果s[i]=='W',则dp[i][i][0]=1, dp[i][i][1]=0;否则dp[i][i][0]=0, dp[i][i][1]=1。
3. 枚举区间长度len从2到n,枚举起点i,终点j=i+len-1,然后枚举分割点k从i到j-1,进行状态转移。
4. 最终答案是dp[0][n-1][1](因为我们要黑色)。
注意:取模998244353。
但是,n=500时,三重循环:500^3=125000000,在C++中可能刚好卡在1秒左右(取决于机器速度和优化)。但我们可以尝试优化循环顺序(枚举长度,然后起点,然后分割点)以利用局部性,并且使用long long存储中间结果避免溢出。
然而,在C++中,125e6次循环,每次循环内做几次乘法和加法,在2秒的时限内可能通过(因为现代CPU每秒可以执行1e9次操作,但乘法较慢,但125e6次乘法和加法通常可以在1秒内完成)。
如果n最大为500,我们可以接受。如果n更大,比如1000,则1000^3=1e9,会超时。
因此,我们需要考虑优化为O(n^2)。如何优化?
有一个经典论文:Hu and Shing (1981) 关于矩阵链乘的优化,但这里不是矩阵链乘。
另一种思路:注意到合并操作具有结合律,并且颜色只有两种,我们可以用生成函数。但是区间dp的生成函数难以处理。
或者,我们观察到状态转移方程中,dp[i][j][c]的转移只依赖于子区间的颜色,而与子区间内的具体合并方式无关。并且,转移方程是线性的。因此,我们可以尝试使用前缀和优化?但是区间dp通常无法直接前缀和。
我们写出转移方程:
dp[i][j][1] = sum_{k=i}^{j-1} { dp[i][k][1]*dp[k+1][j][1] + dp[i][k][1]*dp[k+1][j][0] + dp[i][k][0]*dp[k+1][j][1] }
dp[i][j][0] = sum_{k=i}^{j-1} { dp[i][k][0]*dp[k+1][j][0] + dp[i][k][1]*dp[k+1][j][0] + dp[i][k][0]*dp[k+1][j][1] }
这个式子可以改写:
dp[i][j][1] = sum_{k} [ dp[i][k][1]*(dp[k+1][j][0]+dp[k+1][j][1]) + dp[k+1][j][1]*(dp[i][k][0]) ]
= sum_{k} [ dp[i][k][1] * (dp[k+1][j][0]+dp[k+1][j][1]) + dp[k+1][j][1] * dp[i][k][0] ]
同理:
dp[i][j][0] = sum_{k} [ dp[i][k][0]*(dp[k+1][j][0]+dp[k+1][j][1]) + dp[k+1][j][0] * dp[i][k][1] ]
但是,这样并没有减少计算量。
另一种想法:定义新的状态
f(i, j) = dp[i][j][0] + dp[i][j][1] // 区间[i,j]合并的所有方案数(不论最后颜色)
g(i, j) = dp[i][j][1] // 我们要求的黑色方案数
那么,转移方程能否用f和g表示?
dp[i][j][1] = sum_{k} [ g(i,k)*g(k+1,j) + g(i,k)*(f(k+1,j)-g(k+1,j)) + (f(i,k)-g(i,k))*g(k+1,j) ]
= sum_{k} [ g(i,k)*g(k+1,j) + g(i,k)*f(k+1,j) - g(i,k)*g(k+1,j) + f(i,k)*g(k+1,j) - g(i,k)*g(k+1,j) ]
= sum_{k} [ g(i,k)*f(k+1,j) + f(i,k)*g(k+1,j) - g(i,k)*g(k+1,j) ]
而dp[i][j][0] = f(i,j) - g(i,j)
同时,dp[i][j][0] = sum_{k} [ (f(i,k)-g(i,k))*(f(k+1,j)-g(k+1,j)) + g(i,k)*(f(k+1,j)-g(k+1,j)) + (f(i,k)-g(i,k))*g(k+1,j) ]
= ... 比较复杂,而且我们发现g的转移仍然需要两重循环,无法优化。
因此,我们暂时采用O(n^3)的区间dp。如果n太大,则需要进一步优化(比如使用Knuth优化),但这里我们先实现O(n^3),并假设n<=500。
如果题目中n更大,比如1000,那么我们需要O(n^2)的算法。事实上,存在O(n^2)的算法吗?
有一个思路:将连续相同颜色的段合并,因为连续相同颜色段在合并时会一直保持颜色不变,直到遇到不同颜色。
例如:BBW,我们可以将BB看作一个块(因为连续两个B,合并后还是B,所以相当于一个B),然后问题变成合并B和W。
所以,我们可以将连续相同颜色的段压缩成一个块,并记录其长度。但注意:合并规则中,不同颜色合并时可以任意选择,所以压缩后,序列变成了黑白交替的序列。
假设压缩后有m个块(m<=n),每个块的颜色为c_i,长度为l_i(但长度在方案数计算中会以什么形式影响?)
但合并操作不仅仅与颜色块有关,还与合并的顺序有关。然而,合并相邻两个块时,如果两个块颜色相同,那么它们会合并成一个更大的同色块(长度相加);如果两个块颜色不同,那么合并后可以选择颜色,并且新块的颜色会覆盖相邻的块,从而可能继续合并?
例如:B, W, B
第一步:合并B和W,如果选择B,则变成B, B;然后合并两个B,变成B。
第一步:合并W和B,如果选择B,则变成B, B;然后合并两个B,变成B。
所以最后得到黑色。
因此,我们只关心块的颜色序列,不关心长度(因为长度不会影响合并规则,只影响合并顺序?)。但是,实际上长度会影响合并的方案数吗?
考虑两个连续块:第一个块有a个B,第二个块有b个W。那么合并这两个块时,合并顺序有(a+b-1)种?不对,因为块内部已经合并成一个颜色,所以两个块合并只有1次操作,但是合并后可以选择颜色(两种选择)。
所以,在压缩后,每个块实际上是一个同色的整体,块与块之间的合并:
相同颜色:合并后颜色不变,方案数1。
不同颜色:合并后可以选择两种颜色,方案数2。
然后,问题转化为:给定一个颜色序列(黑白交替),每次合并相邻两个块,规则同上,最后得到黑色的方案数。
但是,这里合并两个块后,新块的颜色会与相邻的块继续合并,因此合并顺序(即树形结构)会影响结果吗?
例如:三个块:B, W, B
合并顺序1:先合并前两个(B和W),有2种选择(可以选择B或W)。然后合并结果与第三个块(B):
如果选择B:那么合并B和B -> B(1种)
如果选择W:那么合并W和B -> 可以选择B或W(2种),其中选B为1种,所以总共:2*(1+1)=4?但是之前我们手动计算的是2种(最后得到黑色)。
注意:我们要的是最后得到黑色的方案数。所以第一种合并顺序得到黑色的方案数:第一步选择B,然后合并B和B得到B:1种;第一步选择W,然后合并W和B时选择B:1种。所以总共2种。
而合并顺序2:先合并后两个(W和B),同理得到2种(最后黑色)。
因此,无论顺序如何,整个序列的合并方案数(最后得到黑色)是相同的吗?实际上,不同顺序得到的方案数相同吗?
但是,我们并不是计算所有合并顺序的方案数,而是固定序列顺序(只能合并相邻块),所以合并顺序由分割点决定,但最后所有合并顺序都要考虑。
压缩后的序列,状态:dp[i][j][c] 表示将压缩后的序列中第i块到第j块合并成颜色c的方案数。
转移:同样区间dp,枚举分割点k(i<=k<j),将区间[i, k]合并成一个块,[k+1, j]合并成一个块,然后再合并这两个块。
合并两个块:如果两个块的颜色相同,则合并后颜色为该颜色,方案数为1;如果不同,则合并后可以选择两种颜色,方案数为2。
但是,这里要注意:我们压缩后的序列是黑白交替的,所以相邻的块颜色一定不同!因此,合并两个块时,因为颜色不同,所以方案数为2(可以选择合并成任意颜色)。
那么,状态转移:
前提:压缩后的序列中,相邻块颜色不同。
设当前区间[i, j]合并,枚举最后一步合并:即在某个分割点k(将[i,k]和[k+1,j]分别合并成两个块),然后将这两个块合并。
设[i,k]合并后的颜色为c1,[k+1,j]合并后的颜色为c2(这里c1和c2在压缩序列中相邻,所以c1≠c2),那么合并这两个块时,可以选择黑色或白色(2种选择)。
因此,转移方程:
dp[i][j][0] = sum_{k} [ (dp[i][k][0]*dp[k+1][j][c2] + dp[i][k][1]*dp[k+1][j][0]?) ] * 1? 不对。
更准确:我们不知道左右两个子区间合并后的颜色,因为子区间合并后的颜色可以任意(只要符合转移结果),所以我们要枚举左右子区间合并后的颜色。
但是,因为压缩后序列是交替的,整个区间[i,j]合并后的颜色只与最后一次合并的选择有关。然而,在合并两个块时,由于颜色不同,所以有2种选择,并且这两个选择是独立的。
所以,转移方程:
dp[i][j][0] = sum_{k} [ (dp[i][k][0] + dp[i][k][1]) * (dp[k+1][j][0] + dp[k+1][j][1]) * ? ]
注意:左右两个子区间合并后,颜色各自有两种可能(黑或白),所以总共有4种情况。然后合并两个块时,因为颜色一定不同(因为原序列交替,且合并后我们得到两个块,但这两个块相邻的颜色由子区间的最后一块和下一块的头一块决定?)—— 实际上,我们压缩后的序列在合并过程中,相邻块的颜色还是会保持交替吗?不一定,因为合并两个块后,新块的颜色可能会和它相邻的块同色,但我们还没有合并。
所以,在区间[i,j]内,我们枚举最后一次分裂点k,那么最后一次合并时,两个块的颜色可以是任意颜色(0或1),但要注意:在压缩后的序列中,第i块到第j块,如果我们合并成同一个块,那么合并后的颜色由最后一次合并操作决定,并且这个颜色与区间外的块无关。
但是,在合并两个块时,无论这两个块的颜色是什么(可能相同或不同?),在压缩序列中,第k块和第k+1块是相邻的,所以它们颜色一定不同(因为压缩后是交替的)?注意,我们最开始压缩的序列是交替的,但在合并过程中,如果我们将连续的块合并,合并后的新块的颜色是任意指定的,所以可能会破坏交替性?例如:
初始:B, W, B (交替)
先合并B和W:可以选择变成B或W。
如果变成B:序列变成 B, B -> 此时两个B相邻,但我们压缩后应该合并成一个B(但是这里我们是在区间dp中,所以并不立即压缩,而是作为一个块?)
所以,我们设计的dp状态中,一个块区间合并后,我们并不认为它和相邻块有颜色关系,因为在我们枚举的区间内,相邻块在区间外。
因此,转移时,我们只关心左右两部分合并后的颜色,然后根据它们是否相同来决定方案数?但是,在压缩后的序列中,第k块和第k+1块的颜色在初始化时是交替的,但在合并过程中,可能内部合并后颜色改变,所以这两块在合并前的颜色是不确定的,可能相同也可能不同。
所以,我们必须考虑所有可能的颜色组合:
dp[i][j][0] = 0;
dp[i][j][1] = 0;
for (int k = i; k < j; k++) {
for (int c1: {0,1}) {
for (int c2: {0,1}) {
ll ways = dp[i][k][c1] * dp[k+1][j][c2] % mod;
if (c1 == c2) {
// 相同,合并后只能得到c1(1种选择)
dp[i][j][c1] = (dp[i][j][c1] + ways) % mod;
} else {
// 不同,合并后可以选择0或1(2种选择)
dp[i][j][0] = (dp[i][j][0] + ways) % mod;
dp[i][j][1] = (dp[i][j][1] + ways) % mod;
}
}
}
}
这个转移方程适用于压缩后的块吗?或者 even for the original problem?
注意:这个转移方程就是最通用的区间dp,它并不依赖于压缩。而且,它和我们在非压缩的O(n^3)算法中的转移方程 Similar 但不完全相同:在非压缩算法中,我们是这样分的:
dp[i][j][0] = sum_{k} [ (left0*right0) + (left0*right1 + left1*right0) ] -> 注意,当左右一个0一个1时,我们不管具体哪边0哪边1,都加到0中(因为可以选择0),而 not 像上面那样分相同和 разные.
实际上,我们非压缩的算法中,转移方程是:
dp[i][j][0] = sum_{k} [ left0*right0 + left0*right1 + left1*right0 ]
而上面这个通用转移方程(三层循环)如果应用到非压缩,就是:
for c1,c2 in {0,1}:
ways = left_{c1} * right_{c2}
if c1==c2:
dp[i][j][c1] += ways
else:
dp[i][j][0] += ways
dp[i][j][1] += ways
展开:
c1=c2=0: dp[i][j][0] += left0*right0
c1=c2=1: dp[i][j][1] += left1*right1
c1=0,c2=1: dp[i][j][0] += left0*right1, dp[i][j][1] += left0*right1
c1=1,c2=0: dp[i][j][0] += left1*right0, dp[i][j][1] += left1*right0
所以:
dp[i][j][0] = left0*right0 + left0*right1 + left1*right0
dp[i][j][1] = left1*right1 + left0*right1 + left1*right0
这和我们非压缩的转移方程一致。
所以,两种表示等价。
因此,压缩与否,通用区间dp。 非压缩的O(n^3)算法中,n是初始序列长度,压缩后的m<=n,所以如果压缩,m减小, O(m^3)会快一些,但最坏情况m=n(比如交替颜色),所以还是O(n^3)。
那么,我们是否还要压缩?压缩的好处:减少状态数(如果有很多连续相同颜色,则m很小),但是 worst-case 仍然是n(交替情况)。所以 worst-case 仍然是O(n^3),但 average-case 可能好一些。
因此,在实现时,我们可以先压缩初始序列,再进行区间dp。
压缩方法:将连续的相同颜色的光盘合并成一个块。
例如: "BWWWBBB" -> 压缩成:['B', 'W', 'B'],对应的 length 数组为[1,3,3](虽然 length 数组在 dp 中没用,但我们只需要颜色序列)。
注意:压缩后,一个块用一个字符表示,连续相同字符压缩成一个。
然后用这个 compressed 数组去做通用的区间dp(状态:dp[i][j][c])。
转移方程( for the通用的):
for (int len = 1; len <= m; len++) {
for (int i = 0; i+len-1 < m; i++) {
int j = i+len-1;
if (len==1) {
// 一个块
if (compressed[i]=='B') {
dp[i][j][1] = 1;
dp[i][j][0] = 0;
} else {
dp[i][j][1] = 0;
dp[i][j][0] = 1;
}
} else {
for (int k = i; k < j; k++) {
for (int c1: {0,1}) {
for (int c2: {0,1}) {
// 注意:我们状态的子区间:dp[i][k][c1] 和 dp[k+1][j][c2] 已经表示子区间合并后的颜色
if (c1==c2) {
dp[i][j][c1] = (dp[i][j][c1] + dp[i][k][c1]*dp[k+1][j][c2] % mod) % mod;
} else {
dp[i][j][0] = (dp[i][j][0] + dp[i][k][c1]*dp[k+1][j][c2] % mod) % mod;
dp[i][j][1] = (dp[i][j][1] + dp[i][k][c1]*dp[k+1][j][c2] % mod) % mod;
}
}
}
}
}
}
}
最后,答案为 dp[0][m-1][1]。
然而,这个三重循环(m^3)再嵌套两个颜色循环(4*m^3),常数较大。但m<=n,最坏情况下m=n=500,则循环次数 500^3 * 4 = 500000000,5e8,可能会超时(C++在2秒内勉强能过,但要注意常数优化)。
我们可以优化:避免枚举颜色,而是用两个数组分别存0和1。
for (int k = i; k < j; k++) {
ll ways00 = dp[i][k][0] * dp[k+1][j][0] % mod;
ll ways01 = dp[i][k][0] * dp[k+1][j][1] % mod;
ll ways10 = dp[i][k][1] * dp[k+1][j][0] % mod;
ll ways11 = dp[i][k][1] * dp[k+1][j][1] % mod;
// 相同:00 -> 0, 11 -> 1
// 不同:01 and 10: 0 and 1 各加两次
dp[i][j][0] = (dp[i][j][0] + ways00 + ways01 + ways10) % mod;
dp[i][j][1] = (dp[i][j][1] + ways11 + ways01 + ways10) % mod;
}
这样,每个分割点k,我们只需要4次乘法和4次加法(以及取模),然后加两次。总共次数:4 * (len-1) 次乘法。
而这是我们在非压缩情况下的转移方程。
所以,不管压缩还是不压缩,我们都可以用 this four-multiplication transfer.
而且,压缩的好处:如果有很多连续相同颜色,m会变小,从而加速。
步骤:
1. 压缩初始序列:遍历原序列,将连续相同字符压缩成一个字符,得到一个数组v(大小为m)。
2. 初始化dp数组(大小为m x m x 2)为0。
3. 初始化len=1:dp[i][i][0] and [1] according to v[i]。
4. 枚举区间长度len from 2 to m, for each i,j (j = i+len-1) and for each k in [i, j-1]:
dp[i][j][0] += ( dp[i][k][0]*dp[k+1][j][0] + dp[i][k][0]*dp[k+1][j][1] + dp[i][k][1]*dp[k+1][j][0] )
dp[i][j][1] += ( dp[i][k][1]*dp[k+1][j][1] + dp[i][k][0]*dp[k+1][j][1] + dp[i][k][1]*dp[k+1][j][0] )
5. 输出 dp[0][m-1][1]
注意:取模。
考虑一个简单的情况:"BW"(m=2):
v = ['B','W']
dp[0][0][1]=1, [0]=0; dp[1][1][0]=1, [1]=0.
合并区间[0,1]:k只能=0.
dp[0][1][0] = dp[0][0][0]*dp[1][1][0] + dp[0][0][0]*dp[1][1][1] + dp[0][0][1]*dp[1][1][0]
= 0*1 + 0*0 + 1*1 = 1
dp[0][1][1] = dp[0][0][1]*dp[1][1][1] + dp[0][0][0]*dp[1][1][1] + dp[0][0][1]*dp[1][1][0]
= 1*0 + 0*0 + 1*1 = 1
所以方案数:最后得到黑色的方案数=1?
但是BW:合并时可以选择 B->1, W->0,然后合并时可以选择0或1,所以得到黑色的方案数为1(选择1)。
但这里 dp[0][1][1] = 1,所以没问题。
另一个例子 "BB": 压缩后v=['B'] (m=1), 所以 dp[0][0][1]=1, 方案数=1.
而 "B,W" vs "BB": 手动计算 "BB": 合并两个B,只能得到B,所以方案数1.
因此,正确。
但我们 note:在 "BW" 中,dp[0][1][0] = 1, dp[0][1][1]=1,所以总方案数是2,最后得到黑色的是1种,但题目中 "BW" 合并后有两种可能(黑或白),所以我们得到黑色方案数1(即 dp[0][1][1])是正确的。
所以,我们用压缩后的序列,然后区间dp。
代码实现:
步骤1:压缩
vector<char> comp;
string s; // 输入字符串
comp.push_back(s[0]);
for (int i=1; i<n; i++) {
if (s[i] != s[i-1])
comp.push_back(s[i]);
}
步骤2:区间dp
注意:我们并没有利用长度信息,所以压缩后的数组只保留了颜色变化。
但是,这个 dp 适用于压缩后的序列吗? 是的,因为我们已经将连续的相同颜色合并,所以压缩后的序列中相邻的颜色一定不同,但这并不影响我们的dp,因为dp is designed for the original problem and it works for any sequence.
而且,压缩并没有改变序列的 merging 性质? 连续相同的光盘,在 merging 时,任意顺序合并都会得到该颜色,所以它们可以视为一个块。
因此,压缩是合理的。
最后,我们代码的框架:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MOD = 998244353;
int main() {
int n;
string s;
cin >> n >> s; // 或者先输入n,然后输入一个字符串?题目描述:排成一行,所以按字符串输入。
// 压缩
vector<char> v;
v.push_back(s[0]);
for (int i=1; i<n; i++) {
if (s[i] != s[i-1])
v.push_back(s[i]);
}
int m = v.size();
// dp[i][j][0] and dp[i][j][1]
vector<vector<vector<long long>>> dp(m, vector<vector<long long>>(m, vector<long long>(2, 0)));
// 或者开两个二维数组:dp0[i][j] and dp1[i][j]
vector<vector<long long>> dp0(m, vector<long long>(m, 0));
vector<vector<long long>> dp1(m, vector<long long>(m, 0));
// 初始化: len=1
for (int i=0; i<m; i++) {
if (v[i]=='B') {
dp1[i][i] = 1;
// dp0[i][i] = 0; // 默认0
} else { // 'W'
dp0[i][i] = 1;
}
}
// 枚举区间长度len=2 to m
for (int len=2; len<=m; len++) {
for (int i=0; i+len-1<m; i++) {
int j = i+len-1;
// 初始化
dp0[i][j] = 0;
dp1[i][j] = 0;
// 枚举分割点k: from i to j-1
for (int k=i; k<j; k++) {
// 左边区间[i,k] -> (dp0[i][k], dp1[i][k])
// 右边[k+1,j] -> (dp0[k+1][j], dp1[k+1][j])
long long ways00 = dp0[i][k] * dp0[k+1][j] % MOD;
long long ways01 = dp0[i][k] * dp1[k+1][j] % MOD;
long long ways10 = dp1[i][k] * dp0[k+1][j] % MOD;
long long ways11 = dp1[i][k] * dp1[k+1][j] % MOD;
dp0[i][j] = (dp0[i][j] + ways00 + ways01 + ways10) % MOD;
dp1[i][j] = (dp1[i][j] + ways11 + ways01 + ways10) % MOD;
}
}
}
cout << dp1[0][m-1] % MOD << endl;
return 0;
}
但是,注意:这样枚举,最坏情况m=500时,循环次数为:
len from 2 to 500: 共499个len
对于每个len,i有 (m-len+1) 个,所以大约 m^3/6 次,即 500^3/6 = 125000000/6 ≈ 20833333,这是内层循环次数,每次循环内有4次乘法,4次加法(取模运算忽略常数),总操作数大约 20833333 * 8 = 166666664,1.6e8,在C++中2秒内可以接受(因为乘法和加法是简单操作)。
但是,要避免使用vector的二维数组,因为它可能会慢,可以考虑用二维数组(静态)或者一维数组优化。
然而,题目要求取模998244353,注意乘法和加法都要取模。
我们测试一下小数据:
输入:n=2, s="BW"
压缩后:v = ['B','W'] (m=2)
dp1[0][0]=1, dp0[0][0]=0; dp0[1][1]=1, dp1[1][1]=0.
i=0, j=1, k=0:
ways00 = 0*1 = 0
ways01 = 0*0 = 0
ways10 = 1*1 = 1
ways11 = 1*0 = 0
dp0[0][1] = 0+0+1 = 1
dp1[0][1] = 0+0+1 = 1 [注意:这里按照我们的转移方程,dp1[0][1] = ways11 (0) + ways01 (0) + ways10 (1) = 1]
所以输出1?但BW合并后,最后得到黑色的方案数是1(两次乘法:1*1=1,得到1),但是注意:我们 dp1[0][1] 表示区间 [0,1] 合并成黑色的方案数,所以是1。
"BW" 合并:只有一次操作,操作时可以选择黑色或白色,所以最后得到黑色的方案数为1,正确。
再试 "BB": 压缩后v=['B'] (m=1), 输出 dp1[0][0]=1,正确。
再试 "BWB":
压缩后:['B','W','B'] -> m=3.
首先初始化:
位置0: 'B' -> dp1[0][0]=1, dp0=0
位置1: 'W' -> dp0[1][1]=1, dp1=0
位置2: 'B' -> dp1[2][2]=1, dp0=0
然后,合并区间:
len=2:
[0,1]: 如上,dp0[0][1]=1, dp1[0][1]=1.
[1,2]:
i=1, j=2, k=1 (only)
ways00 = dp0[1][1]*dp0[2][2] = 1 * 0 = 0
ways01 = dp0[1][1]*dp1[2][2] = 1 * 1 = 1
ways10 = dp1[1][1]*dp0[2][2] = 0 * 0 = 0
ways11 = dp1[1][1]*dp1[2][2] = 0 * 1 = 0
dp0[1][2] = 0+1+0 = 1
dp1[1][2] = 0+1+0 = 1
len=3: [0,2]
k=0: 分割为[0,0] and [1,2]
dp0[0][0]=0, dp1[0][0]=1; dp0[1][2]=1, dp1[1][2]=1.
ways00 = 0*1=0
ways01 = 0*1=0
ways10 = 1*1=1
ways11 = 1*1=1
-> 左边这一刀贡献:
dp0[0][2] += 0+0+1 = 1
dp1[0][2] += 1+0+1 = 2 [注意:这里ways11=1,然后 ways01+ways10=0+1=1,所以dp1加2]
k=1: 分割为[0,1] and [2,2]
dp0[0][1]=1, dp1[0][1]=1; dp0[2][2]=0, dp1[2][2]=1.
ways00 = 1*0=0
ways01 = 1*1=1
ways10 = 1*0=0
ways11 = 1*1=1
-> 贡献:
dp0[0][2] = 1 + (0+1+0) = 1+1=2 (注意:这里的0+1+0:ways00+ways01+ways10=0+1+0=1)
dp1[0][2] = 2 + (1 + 1 + 0) = 2 + 2 = 4? 注意:dp1的贡献:ways11 (1) + ways01 (1) + ways10 (0) = 2
所以最后:
dp0[0][2] = 1+1 = 2
dp1[0][2] = 2+2 = 4
因此,最后得到黑色的方案数为4。
手动计算 "B W B" 的方案数:
合并顺序1:先合并前两个(B和W)-> 得到X(X可以是B或W),然后合并X和B:
如果X=B:那么B和B合并得B(1种),第一次选择B(1种),所以1种。
如果X=W:那么W和B合并,可以选择B或W,其中B(1种),第一次选择W(1种),所以1种。
共2种。
合并顺序2:先合并后两个(W和B)-> 得到Y(Y可以是B或W),然后合并B和Y:
如果Y=B:那么B和B合并得B(1种),第一次选择B(1种),所以1种。
如果Y=W:那么B和W合并,可以选择B或W,其中B(1种),第一次选择W(1种),所以1种。
共2种。
总方案数=2+2=4。
所以dp得到4,正确。
因此,代码按照这个思路。
但是,题目中原题n最大多少?题目没有给出,所以我们需要测试一下性能。
如果n=500,且全部交替(如BWBWBW...),则压缩后m=n=500,区间dp时间复杂度O(m^3)=125e6,而内层循环中每次有4次乘法,4次加法,8次运算,总运算次数125e6 * 8 = 1e9,在C++中大约1秒(现代CPU可能更快,但为了保险,我们可以开启O2优化)。
因此,我们写代码时,将dp数组用二维数组(分开dp0和dp1)并用vector即可。
注意:long long 可能会占用较多内存,m=500,则dp0和dp1的大小为500*500,每个大约250000个元素,两个数组500000个long long,约4MB(long long 8字节,500000*8=4e6字节),内存足够。
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
int main() {
int n;
string s;
cin >> n >> s;
// 压缩
vector<char> v;
v.push_back(s[0]);
for (int i=1; i<n; i++) {
if (s[i]!=s[i-1])
v.push_back(s[i]);
}
int m = v.size();
// dp0[i][j] 表示区间[i,j]合并为0的方案数,dp1为1
vector<vector<long long>> dp0(m, vector<long long>(m,0));
vector<vector<long long>> dp1(m, vector<long long>(m,0));
// 初始化
for (int i=0; i<m; i++) {
if (v[i]=='B')
dp1[i][i] = 1;
else
dp0[i][i] = 1;
}
// 区间dp
for (int len=2; len<=m; len++) {
for (int i=0; i<=m-len; i++) {
int j = i+len-1;
dp0[i][j] = 0;
dp1[i][j] = 0;
for (int k=i; k<j; k++) {
// 计算左右区间的组合
long long left0 = dp0[i][k], left1 = dp1[i][k];
long long right0 = dp0[k+1][j], right1 = dp1[k+1][j];
long long ways00 = (left0 * right0) % mod;
long long ways01 = (left0 * right1) % mod;
long long ways10 = (left1 * right0) % mod;
long long ways11 = (left1 * right1) % mod;
dp0[i][j] = (dp0[i][j] + ways00 + ways01 + ways10) % mod;
dp1[i][j] = (dp1[i][j] + ways11 + ways01 + ways10) % mod;
}
}
}
cout << dp1[0][m-1] % mod << endl;
return 0;
}
注意:在取模时,我们每做一次乘法和加法都取模,以避免溢出。
但是,加法是在最后取模,因为最多有500次加法,每次加的数不超过mod(1e9+级别)的平方,所以不会溢出long long?
mod=998244353,则两个数相乘最大为 (1e9)^2 = 1e18,而long long 最大约9e18,所以两个数相乘不会溢出,但四个相乘的结果相加最多有500* (1e18) 会溢出。
所以,我们在乘法后立即取模,然后加法时再取模。
修改:
ways00 = (left0 * right0) % mod;
... 其他同理。
然后,dp0[i][j] = (dp0[i][j] + ways00) % mod; 然后再加上 ways01 再取模? 这样太慢,可以累加完三个再取模,因为三个数都小于mod,所以和最大3*mod,不会溢出long long(long long 最大9e18,mod约1e9,所以3*mod=3e9,不会溢出,但多个累加?区间长度最多500,所以内层循环最多500次,所以每次加三个数,那么dp0[i][j]最多加500*3*mod,即1500*mod≈1.5e12,而long long 最大约9e18,所以不会溢出。因此,我们可以在内层循环结束后再取模。
但是,为了保险,我们在每个加法后取模:
dp0[i][j] = (dp0[i][j] + ways00) % mod;
dp0[i][j] = (dp0[i][j] + ways01) % mod;
dp0[i][j] = (dp0[i][j] + ways10) % mod;
dp1[i][j] = (dp1[i][j] + ways11) % mod;
dp1[i][j] = (dp1[i][j] + ways01) % mod;
dp1[i][j] = (dp1[i][j] + ways10) % mod;
或者,因为三个数都是小于mod的,所以可以:
long long total0 = (ways00 + ways01 + ways10) % mod;
dp0[i][j] = (dp0[i][j] + total0) % mod;
long long total1 = (ways11 + ways01 + ways10) % mod;
dp1[i][j] = (dp1[i][j] + total1) % mod;
这样减少取模次数。
我们选择在加法后立即取模,因为运算次数不多。
最后,注意输入输出。
如果题目输入:
n
一个字符串s
例如:输入:
3
BWB
输出:4
但是,注意题目中光盘颜色表示:'B'和'W',所以输入字符串可能没有空格。
提交代码,按照上面写的。
注意:在区间dp中,我们枚举len,然后枚举i,j,然后k。注意j的边界。
如果n=0?但是题目说n张光盘,n>=1,所以不考虑。
我们完成代码。
但是,性能:n=500时,m=500,最坏情况(交替)下,循环次数为:
len=2: 499个区间
len=3: 498
...
len=500:1
总区间数:m*(m-1)/2 = 125250
每个区间内层循环次数(分割点):len-1,即区间长度为len,有len-1个分割点。
所以总分割点次数: for (len=2 to 500) { for (i=0 to m-len) { 内层循环次数= len-1 } }
即:sum_{len=2}^{500} (len-1) * (500-len+1)
= sum_{i=1}^{499} i * (500-i) [令i=len-1]
= 500 * sum_{i=1}^{499} i - sum_{i=1}^{499} i^2
= 500*(499*500/2) - 499*500*999/6 [但是这里近似,实际需要计算]
第一项:500 * (499*500/2) = 500 * 124750 = 62375000
第二项:499*500*999/6 ≈ 41,583,250
所以总操作数≈62,375,000 - 41,583,250 = 20,791,750? 不对,这个表达式含义不对。
应该是:总分割点次数 = sum_{len=2}^{500} (500-len+1) * (len-1)
= 对于每个len,有 (500-len+1) 个区间,每个区间有 (len-1) 个分割点。
= 令k=len-1,则 len from 2 to 500 -> k from 1 to 499
= sum_{k=1}^{499} (500- (k+1) +1) * k = sum_{k=1}^{499} (500-k) * k = 500*sum k - sum k^2
= 500*(499*500/2) - (499*500*999)/6
≈ 500 * 124750 = 62,375,000 减去 499*500*999/6 ≈ 41,583,250
= 20,791,750
所以总分割点次数2080万,然后每次分割点有4次乘法,4次加法(然后还有几次取模,以及数组访问),大约10次操作,那么总操作数2.08e8,在C++中1秒内可以完成。
因此,我们直接编写。
最终代码:
</think>以下是解决光盘合并问题的C++代码实现。该代码使用动态规划方法,通过压缩连续相同颜色的光盘块来优化计算,最终计算得到最后一张光盘为黑色的方案数,并对998244353取模。
```cpp
#include <iostream>
#include <vector>
using namespace std;
const int mod = 998244353;
int main() {
int n;
string s;
cin >> n >> s;
// 压缩连续相同颜色
vector<char> comp;
comp.push_back(s[0]);
for (int i = 1; i < n; i++) {
if (s[i] != s[i-1]) {
comp.push_back(s[i]);
}
}
int m = comp.size();
// 初始化动态规划数组
vector<vector<long long>> dp0(m, vector<long long>(m, 0));
vector<vector<long long>> dp1(m, vector<long long>(m, 0));
// 边界条件:单个光盘块
for (int i = 0; i < m; i++) {
if (comp[i] == 'B') {
dp1[i][i] = 1;
} else {
dp0[i][i] = 1;
}
}
// 区间DP:枚举区间长度
for (int len = 2; len <= m; len++) {
for (int i = 0; i <= m - len; i++) {
int j = i + len - 1;
dp0[i][j] = 0;
dp1[i][j] = 0;
// 枚举分割点
for (int k = i; k < j; k++) {
// 计算左右子区间组合方案
long long left0 = dp0[i][k], left1 = dp1[i][k];
long long right0 = dp0[k+1][j], right1 = dp1[k+1][j];
// 乘法后取模防溢出
long long ways00 = (left0 * right0) % mod;
long long ways01 = (left0 * right1) % mod;
long long ways10 = (left1 * right0) % mod;
long long ways11 = (left1 * right1) % mod;
// 更新状态
dp0[i][j] = (dp0[i][j] + ways00 + ways01 + ways10) % mod;
dp1[i][j] = (dp1[i][j] + ways11 + ways01 + ways10) % mod;
}
}
}
// 输出最终结果
cout << dp1[0][m-1] << endl;
return 0;
}
```
### 算法说明
1. **输入处理**:读取光盘数量 `n` 和颜色字符串 `s`。
2. **颜色压缩**:将连续相同颜色的光盘压缩为单个块,减少状态数量。
3. **动态规划初始化**:
- `dp0[i][j]` 表示区间 `[i, j]` 合并成白色的方案数
- `dp1[i][j]` 表示区间 `[i, j]` 合并成黑色的方案数
4. **边界条件**:单个光盘块直接初始化对应颜色方案数为1。
5. **区间DP**:
- 枚举区间长度 `len` 从2到 `m`
- 枚举区间起点 `i`,计算终点 `j`
- 枚举分割点 `k`,组合左右子区间的方案
- 根据合并规则更新状态:
- 相同颜色合并:直接继承该颜色
- 不同颜色合并:可任选黑或白(方案数累加)
6. **结果输出**:最终结果为区间 `[0, m-1]` 合并成黑色的方案数。
### 时间复杂度
- 时间复杂度:$O(m^3)$,其中 $m$ 是压缩后的块数($m \leq n$)
- 空间复杂度:$O(m^2)$
此代码在 $m=500$(最坏情况)时,可在1秒内完成计算。