CCF-CSP 38-4 月票发行【C++】考点:动态规划DP+矩阵快速幂

题目

TUOJhttps://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 语言中的一种基本数据类型,用于定义一组具有离散值的常量,它可以让数据更简洁,更易读。

枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。

可以在定义枚举类型时改变枚举元素的值:

cpp 复制代码
enum 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;
}
相关推荐
北漂Zachary2 小时前
Mysql中使用sql语句生成雪花算法Id
sql·mysql·算法
OxyTheCrack2 小时前
【C++】详细拆解std::mutex的底层原理
linux·开发语言·c++·笔记
aini_lovee2 小时前
MATLAB圆锥滚子轴承滚子参数分析程序
人工智能·算法·matlab
_olone2 小时前
牛客每日一题:显生之宙(Java)
java·开发语言·算法·牛客
嫂子开门我是_我哥2 小时前
心电域泛化研究从0入门系列 | 第二篇:心电信号预处理全攻略——扫清域泛化建模的第一道障碍
人工智能·算法·ecg
wefg13 小时前
【算法】算数基本定理、分解质因数
算法
j_xxx404_3 小时前
力扣困难算法精解:串联所有单词的子串与最小覆盖子串
java·开发语言·c++·算法·leetcode·哈希算法
挠头猴子3 小时前
一个数组去重,两个数组找不同或相同
数据结构·算法
big_rabbit05023 小时前
[算法][力扣167]Two Sum II
算法·leetcode·职场和发展