题目
TUOJ
https://sim.csp.thusaac.com/contest/38/problem/3
思路参考:
38次csp认证-月票发行-CSDN博客
https://blog.csdn.net/jrjzs/article/details/156539183
核心思路
动态规划(DP)
核心方法是DP,所以最重要的是理解递推方程
dp数组含义:dp[i][j]:考虑前i位,到达的状态为j
一共有10个状态
cpp
enum State {
STATE_CCF_0 = 0, // 未匹配ccf任何字符
STATE_CCF_1 = 1, // 匹配ccf的第一个字符'c'
STATE_CCF_2 = 2, // 匹配ccf的前两个字符'cc'
STATE_CSPARK_0 = 3,// 已有ccf,未匹配cspark任何字符
STATE_CSPARK_1 = 4,// 匹配cspark的第一个字符'c'
STATE_CSPARK_2 = 5,// 匹配cspark的前两个字符'cs'
STATE_CSPARK_3 = 6,// 匹配cspark的前三个字符'csp'
STATE_CSPARK_4 = 7,// 匹配cspark的前四个字符'cspa'
STATE_CSPARK_5 = 8,// 匹配cspark的前五个字符'cspar'
STATE_VALID_END = 9// 合法终止状态(已匹配完整cspark/ccf)
};
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量,它可以让数据更简洁,更易读。
枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
可以在定义枚举类型时改变枚举元素的值:
cppenum season {spring, summer=3, autumn, winter};没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5


按如上所述用dp求解,复杂度为O(n),只能得60分,还需用矩阵快速幂优化一下
优化:矩阵快速幂
把#分成的段的长度记录下来,每往后一位,则用到达当前状态的方法总数的向量(视为列向量),乘以转移矩阵(仔细观察就是递推公式里的那些系数,如下图),最后得到遍历到下一位的,方法总数的向量

如果遇到#(不可放置字符的位置),则
cpp
// 合并
state_vector[0] += state_vector[1] + state_vector[2];
state_vector[3] += state_vector[4] + state_vector[5] + state_vector[6] + state_vector[7] + state_vector[8];
// 重置
for (int j = 0; j < 9; j++) {
if (j != 0 && j != 3) {
state_vector[j] = 0;
}
}
- 合并 :如果你已经部分匹配了
cc(状态 1 或 2),但现在必须停下,由于这个位置不能放字符,你的进度就"作废"了,所有的方案数都会回退到"未匹配 ccf"的最原始状态(状态 0)。同理,正在匹配中的cspark(状态 4-8)也会回退到已完成 ccf 阶段的状态 3。 - 重置:将中间状态清零,因为在这个时间点,没有任何方案能合法地停留在"匹配了一半"的状态。
- 注意 :
state_vector[9](已完全成功的方案)不需要重置,因为它已经达成了目标,后续即便是禁用位置也不会影响它的合法性。
由于矩阵乘法有结合律,而且是有时间复杂度的,所以可以预计算,设转移矩阵为T,存下T, T^2, T^4......,这样就可以在矩阵快速幂的时候避免重复计算
- 时间复杂度 :O(size^3⋅log n+M⋅size^2⋅log n)。
- 第一项为预计算开销。
- 第二项为处理 M 个段的开销。
- size为转移矩阵维度(10),n为字符串长度,M为m个#切割出的段数
题外话:
学校的cg上的测试点貌似和官网的不一样,同样的代码cg上过不了,ccf官网上能过(还是以官网为准吧)


代码
可以让AI总结一下代码逻辑
整体功能
计算长度为n的字符串中,满足特定条件的字符串数量。字符串中有m个固定位置是"#"字符,其余位置可以是小写字母。
状态机设计
用10个状态记录两个模式串"ccf"和"cspark"的匹配进度:
-
状态0-2:匹配"ccf"的不同阶段
-
状态3-8:匹配"cspark"的不同阶段(前提是已经匹配完"ccf")
-
状态9:最终合法状态(完整匹配了其中一个模式串)
核心处理流程
第一步:分段处理
-
根据"#"的位置将字符串切分成若干连续段
-
每个段内都是普通小写字母,没有"#"
第二步:预计算矩阵幂
-
将转移矩阵的2的幂次方预先计算好
-
方便后续快速计算任意长度的转移
第三步:逐段递推
-
对每个段,用矩阵快速幂计算该段字符产生的状态转移
-
遇到段结束(即遇到"#")时,进行状态合并:
-
"ccf"的中间状态合并到状态0
-
"cspark"的中间状态合并到状态3
-
其他中间状态清零
-
第四步:输出结果
- 最后输出状态9的数值,即所有可能的合法字符串数量
算法本质
这是一个带特殊中断条件的自动机DP问题,用矩阵快速幂优化长段的连续转移,用分段处理处理中断字符的影响。
cpp
#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
#define ll long long
const int mod=998244353;
const int max_mi=32; //预计算的最大幂次 //2^30≈1.07*1e9
vector<vector<vector<ll>>> trans_pow(max_mi,vector<vector<ll>>(10,vector<ll>(10)));
vector<ll>segs;
vector<vector<ll>>trans(10,vector<ll>(10)); //转移矩阵
vector<ll>ans={1,0,0,0,0,0,0,0,0,0}; //满足状态j的方案总数 warn:记得初始化
/* 九个状态
STATE_CCF_0 = 0, // 未匹配ccf任何字符
STATE_CCF_1 = 1, // 匹配ccf的第一个字符'c'
STATE_CCF_2 = 2, // 匹配ccf的前两个字符'cc'
STATE_CSPARK_0 = 3,// 已有ccf,未匹配cspark任何字符
STATE_CSPARK_1 = 4,// 匹配cspark的第一个字符'c'
STATE_CSPARK_2 = 5,// 匹配cspark的前两个字符'cs'
STATE_CSPARK_3 = 6,// 匹配cspark的前三个字符'csp'
STATE_CSPARK_4 = 7,// 匹配cspark的前四个字符'cspa'
STATE_CSPARK_5 = 8,// 匹配cspark的前五个字符'cspar'
STATE_VALID_END = 9// 合法终止状态(已匹配完整cspark/ccf)
*/
vector<vector<ll>> matrix_multiply(vector<vector<ll>> a,vector<vector<ll>> b){
vector<vector<ll>> res(10,vector<ll>(10,0));
for(int i=0;i<10;i++)
for(int j=0;j<10;j++) //列
for(int k=0;k<10;k++)
res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
return res;
}
vector<ll> matrix_x_vector(vector<vector<ll>> a,vector<ll> b){ //10*10 x 10*1 ->10*1
vector<ll> res(10,0);
for(int i=0;i<10;i++) //行号
for(int k=0;k<10;k++)
res[i]=(res[i]+a[i][k]*b[k])%mod; //b列向量,仅一列
return res;
}
void matrix_quick_pow(int k){ //段长度为k,则往下一状态转移k次 13=8+4+1
int cnt=0;
while(k>0){
if(k&1) ans=matrix_x_vector(trans_pow[cnt],ans);
k=k>>1;
cnt++;
}
}
void solve(){
int n,m; cin>>n>>m;
int last_pos=0;
for(int i=0;i<m;i++){
int pos; cin>>pos;
segs.push_back(pos-last_pos-1); //#分出的段的长度
last_pos=pos;
}
if(last_pos!=n) segs.push_back(n-last_pos); //最后一段
//转移矩阵
trans[0][0]=25,trans[0][1]=25,trans[0][2]=24;
trans[1][0]=1;
trans[2][0]=0,trans[2][1]=1,trans[2][2]=1;
trans[3][2]=1,trans[3][3]=25,trans[3][4]=24,trans[3][5]=24,trans[3][6]=24,trans[3][7]=24,trans[3][8]=24;
trans[4][3]=1,trans[4][4]=1,trans[4][5]=1,trans[4][6]=1,trans[4][7]=1,trans[4][8]=1;
trans[5][4]=1;
trans[6][5]=1;
trans[7][6]=1;
trans[8][7]=1;
trans[9][8]=1,trans[9][9]=26;
trans_pow[0]=trans; //预计算 T^(2^1),T^(2^2) T的2^i次方,避免矩阵快速幂时倍增法重复计算
for(int i=1;i<32;i++) trans_pow[i]=matrix_multiply(trans_pow[i-1],trans_pow[i-1]); //矩阵乘法
for(auto seg_len:segs){
matrix_quick_pow(seg_len); //矩阵快速幂
//遇到#,匹配到一半的清0
ans[0]=(ans[0]+ans[1]+ans[2])%mod;
ans[3]=(ans[3]+ans[4]+ans[5]+ans[6]+ans[7]+ans[8])%mod;
for(int i=0;i<9;i++){
if(i==0||i==3) continue;
ans[i]=0;
}
}
cout<<ans[9]<<endl;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}