【动态规划】数位DP的原理、模板(封装类)

【动态规划】数位DP的原理、模板(封装类)

本文涉及知识点

C++动态规划

复杂但相对容易理解的解法

上界、下界的位数一样都为N。如果不一样,拆分一样。比如:[10,200],拆分[10,99]和[100,200]。由于要枚举到 1 ∼ N 1\sim N 1∼N,故实际复杂度是N倍。

动态规划的状态表示

dp[n][m][m1],n表示已经处理最高n位,m表示上下界状态:0非上下界,1下界,2上界,3上下界。m1是自定义状态。
某题范围是[110,190],处理一位后:1是上下界,无其它合法状态。处理二位后,11是下界,19是上界, 12 ∼ 18 12 \sim 18 12∼18是非上下界。
空间复杂度:O( 4 N 4N 4N)

动态规划的状态表示

第一层循环n从小到大;第二层循环m,任意顺序;第三层自定义状态。

动态规划的转移方程

前n位状态当前位状态前n+1位状态
上下界上下界上下界
上界上界
下界下界
下界上界之间非界
上界上界上界
上界<上界非界
下界下界下界
下界>下届非界
非界任意非界

单个状态的时间复杂度: ∑ \sum ∑。总时间复杂度:O(4N ∑ \sum ∑)

动态规划的初始值

枚举最高位。

动态的返回值

dp.back() 和自定义状态有关。

优化一

如果上下界没有公共前缀,则不存在状态上下界。
如果公共前缀为len,则目标串的前n位必定和上下界相同。故可以不处理 0 ∼ n − 1 0 \sim n-1 0∼n−1,直接从第n位开始处理。

优化二

[ l e f t ∼ r ] 的方案数 = [ 0 ∼ r ] 的方案数 − [ 0 ∼ l e f t − 1 ] 的方案数 = [ 0 ∼ r ] 的方案数 − [ 0 ∼ l e f t ] 的方案数 + l e f t 是否符合 [left \sim r]的方案数 =[0 \sim r]的方案数-[0\sim left-1]的方案数=[0 \sim r]的方案数-[0\sim left]的方案数 + left是否符合 [left∼r]的方案数=[0∼r]的方案数−[0∼left−1]的方案数=[0∼r]的方案数−[0∼left]的方案数+left是否符合。简化后只需要考虑是否是上限。

优化三

小于N位的数,可以可以看成前导0。

自己摸索的封装类

template<classELE,classResultType, ELE minEle, ELE maxEle>classCLowUperr{public:CLowUperr(int iCustomStatusCount):m_iCustomStatusCount(iCustomStatusCount){}voidInit(const ELE* pLower,const ELE* pHigh,int iEleCount){ m_vPre.assign(4, vector<ResultType>(m_iCustomStatusCount));if(iEleCount <=0){return;}InitPre(pLower, pHigh); iEleCount--;while(iEleCount--){ pLower++; pHigh++; vector<vector<ResultType>>dp(4, vector<ResultType>(m_iCustomStatusCount));OnInitDP(dp);//处理非边界for(auto tmp = minEle; tmp <= maxEle; tmp++){OnEnumOtherBit(dp[0], m_vPre[0], tmp);}//处理下边界OnEnumOtherBit(dp[1], m_vPre[1],*pLower);for(auto tmp =*pLower +1; tmp <= maxEle; tmp++){OnEnumOtherBit(dp[0], m_vPre[1], tmp);}//处理上边界OnEnumOtherBit(dp[2], m_vPre[2],*pHigh);for(auto tmp = minEle; tmp <*pHigh; tmp++){OnEnumOtherBit(dp[0], m_vPre[2], tmp);}//处理上下边界if(*pLower ==*pHigh){OnEnumOtherBit(dp[3], m_vPre[3],*pLower);}else{OnEnumOtherBit(dp[1], m_vPre[3],*pLower);for(auto tmp =*pLower +1; tmp <*pHigh; tmp++){OnEnumOtherBit(dp[0], m_vPre[3], tmp);}OnEnumOtherBit(dp[2], m_vPre[3],*pHigh);} m_vPre.swap(dp);}} ResultType Sum(int iMinMask,int iMaxMask)const{ ResultType iRet =0;for(int status =0; status <4; status++){for(int mask = iMinMask; mask <= iMaxMask; mask++){ iRet += m_vPre[status][mask];}}return iRet;}protected:constint m_iCustomStatusCount;virtualvoidOnEnumOtherBit(vector<ResultType>& dp,const vector<ResultType>& vPre, ELE curValue)=0;virtualvoidOnEnumFirstBit(vector<ResultType>& vPre,const ELE curValue)=0;virtualvoidOnInitDP(vector<vector<ResultType>>& dp){} vector<vector<ResultType>> m_vPre;private:voidInitPre(const ELE*const pLower,const ELE*const pHigh){for(ELE cur =*pLower; cur <=*pHigh; cur++){int iStatus =0;if(*pLower == cur){ iStatus =*pLower ==*pHigh ?3:1;}elseif(*pHigh == cur){ iStatus =2;}OnEnumFirstBit(m_vPre[iStatus], cur);}}};

参考通用模板:动态规划之记忆化搜索

Rec(i,bUpper)
返回值:bUpper,当前数据的前i位和上界是否相同。有两种解释:一,前i位自定义状态为m的方案数。二,后N-i位自定义状态m的方案数。大部分题,两种解释都行得通。
dp[i] = Rec(i,false)。
任意Rec(i,true) 只会被调用一次,故无需缓存。
令上界是数字n,N = logn,即n的位。
状态数:N。 每个状态的时间复杂度O( ∑ \sum ∑)。故总复杂度:O( ∑ \sum ∑N)。 1 ∼ N − 1 1 \sim N-1 1∼N−1位可以直接通过dp计算,故处理 1 ∼ N − 1 1 \sim N-1 1∼N−1的时间可以忽略。

回调类

template<classELE,classResultType>classIUpperDPCall{public:virtualvoidOnEnum(int n,vector<ResultType>& dp,const vector<ResultType>& vNext, ELE curValue,int iCustomStatusCount)=0;virtualvoidOnInitEnd(vector<ResultType>& dp, vector<ResultType>& upDp)=0;};

OnInitEnd:
dp= m_vDP[n]和dpUp=m_vDpUpper[n]。dp[m]和dpUp[m]表示自定义状态为m的方案数。
OnEnum有以下三种情况,三者的逻辑是一样的,故实现时无需考虑是那种情况。
情况一:dp=m_vDP[n],vNext=m_vDP[n+1]
情况二:dp=m_vDpUpper[n],vNext=m_vDpUpper[n+1]
情况三:dp=m_vDpUpper[n],vNext=m_vDP[n+1]
curValue当前元素的值。
iCustomStatusCount 自定义状态的数量。

最高位的取值范围和其它位相同

比如:枚举字母,容许前导0,即N-1位可以看成有一个前导0的N位数。

template<classELE,classResultType>classCUperrDP{public:CUperrDP(int iCustomStatusCount, IUpperDPCall<ELE, ResultType>& call, ELE minEle, ELE maxEle):m_iCustomStatusCount(iCustomStatusCount),m_call(call),m_minEle(minEle),m_maxEle(maxEle){}voidInit(const ELE* pHigh,int iEleCount){ m_vDP.assign(iEleCount +1, vector<ResultType>(m_iCustomStatusCount)); m_vDpUpper = m_vDP; m_call.OnInitEnd(m_vDP.back(), m_vDpUpper.back());//预处理增加的一位for(int i = iEleCount -1;i >0;i--){ m_call.OnEnum(i,m_vDpUpper[i], m_vDpUpper[i +1], pHigh[i],m_iCustomStatusCount);for(auto j = m_minEle; j < pHigh[i];j++){ m_call.OnEnum(i,m_vDpUpper[i], m_vDP[i +1], j, m_iCustomStatusCount);}for(auto j = m_minEle; j <= m_maxEle;j++){ m_call.OnEnum(i,m_vDP[i], m_vDP[i +1], j, m_iCustomStatusCount);}} m_call.OnEnum(0,m_vDpUpper[0], m_vDpUpper[1], pHigh[0], m_iCustomStatusCount);for(auto j = m_minEle; j < pHigh[0];j++){ m_call.OnEnum(0,m_vDP[0], m_vDP[1], j, m_iCustomStatusCount);}} ResultType Sum(int iMinCustomStatu,int iMaxCustomStatu){ ResultType ret =0;for(int i = iMinCustomStatu; i <= iMaxCustomStatu;i++){ ret += m_vDP[0][i]+ m_vDpUpper[0][i];}return ret;} ResultType Sum(){returnSum(0, m_iCustomStatusCount -1);} vector<vector<ResultType>> m_vDP, m_vDpUpper;const ELE m_minEle, m_maxEle;protected:constint m_iCustomStatusCount; IUpperDPCall<ELE, ResultType>& m_call;};

ELE:元素的类型,几乎全部是char。
ResultType:记录方案数量的类型,如果:int,long long,自定义数据类型。
minEle:最小元素
maxEle:最大元素。
pHigh, int iEleCount:上限字符串的数量和长度。
Init的大致逻辑:
一,初始化。
二,n = N-1 to 1。如果当前元素等于pHigh[n],dpUp[n]对应dpUp[n+1];如果当前元素小于pHigh[n],dpUp[n]对应dpUp[n+1];任意当前元素,dp[n]对应dp[n+1]。
三,第0个元素等于pHigh[0],则dpUp[0]对应dpUp[1]。第0个元素小于pHigh[0],则dp[0]对应dp[1]。

最高位的取值范围和其它位不同

如:正整数不能以0开始。原理类似上一部分,只描述不同点:
firstMinEle,最高位最小元素。
m_vFirstDP[n][m]:N-n位数,自定义状态为m的方案数。 m_vFirstDP[N]未使用。

template<classELE,classResultType>classCUperrDP2{public:CUperrDP2(int iCustomStatusCount, IUpperDPCall<ELE, ResultType>& call, ELE minEle, ELE maxEle, ELE firstMinEle):m_iCustomStatusCount(iCustomStatusCount),m_call(call),m_minEle(minEle),m_maxEle(maxEle),m_firstMinEle(firstMinEle){}voidInit(const ELE* pHigh,int iEleCount){ m_vDP.assign(iEleCount +1, vector<ResultType>(m_iCustomStatusCount)); m_vDpUpper = m_vDP; m_vFirstDP = m_vDP; m_call.OnInitEnd(m_vDP.back(), m_vDpUpper.back());//预处理增加的一位for(int i = iEleCount -1;i >0;i--){ m_call.OnEnum(i,m_vDpUpper[i], m_vDpUpper[i +1], pHigh[i], m_iCustomStatusCount);for(auto j = m_minEle; j < pHigh[i];j++){ m_call.OnEnum(i,m_vDpUpper[i], m_vDP[i +1], j, m_iCustomStatusCount);}for(auto j = m_minEle; j <= m_maxEle;j++){ m_call.OnEnum(i,m_vDP[i], m_vDP[i +1], j, m_iCustomStatusCount);}for(auto j = m_firstMinEle; j <= m_maxEle;j++){ m_call.OnEnum(i,m_vFirstDP[i], m_vDP[i +1], j, m_iCustomStatusCount);}} m_call.OnEnum(0,m_vFirstDP[0], m_vDpUpper[1], pHigh[0], m_iCustomStatusCount);for(auto j = m_firstMinEle; j < pHigh[0];j++){ m_call.OnEnum(0,m_vFirstDP[0], m_vDP[1], j, m_iCustomStatusCount);}} ResultType Sum(int iMinCustomStatu,int iMaxCustomStatu){ ResultType ret =0;for(int i =0;i +1< m_vFirstDP.size();i++){ ret +=accumulate(m_vFirstDP[i].begin()+ iMinCustomStatu, m_vFirstDP[i].begin()+ iMaxCustomStatu +1,(ResultType)0);}return ret;} ResultType Sum(){returnSum(0, m_iCustomStatusCount -1);} vector<vector<ResultType>> m_vDP, m_vDpUpper,m_vFirstDP; ELE m_minEle, m_maxEle, m_firstMinEle;protected:constint m_iCustomStatusCount; IUpperDPCall<ELE, ResultType>& m_call;};

样例

力扣

难度分
【二分查找 数位DP】:902最大为 N 的数字组合1990
【C++动态规划 数位dp】2376. 统计特殊整数2120
【数位dp 动态规划 状态压缩】【推荐】1012. 至少有 1 位重复的数字2230
【数位dp】3519. 统计逐位非递减的整数2246
【动态规划 数位dp】2827. 范围中美丽整数的数目2324
【数位dp】【数论】【动态规划】2999. 统计强大整数的数目2351
【C++算法 数位dp 动态规划】2719统计整数数目2355
【C++动态规划】2801. 统计范围内的步进数字数目2367
【数位dp】3704. 统计和为 N 的无零数对2419
【逆向思考 数位dp】3352. 统计小于 N 的 K 可约简整数2451
【动态规划 数位dp】3490. 统计美丽整数的数目2502
【数位dp KMP】1397. 找到所有好字符串2667
【C++ 数位dp】600. 不含连续1的非负整数无分数

洛谷

难度等级
【数学 进制 数位DP】P9362 [ICPC 2022 Xi‘an R] Find Maximum普及+

扩展阅读

我想对大家说的话
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
员工说:技术至上,老板不信;投资人的代表说:技术至上,老板会信。
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛
失败+反思=成功 成功+反思=成功

视频课程

先学简单的课程,请移步ZEEKLOG学院,听白银讲师(也就是鄙人)的讲解。
https://edu.ZEEKLOG.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.ZEEKLOG.net/lecturer/6176

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。

Read more

C++学习之旅【C++伸展树介绍以及红黑树的实现】

C++学习之旅【C++伸展树介绍以及红黑树的实现】

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》《数据结构与算法》 《C++知识内容》《Linux系统知识》 ✨逆境不吐心中苦,顺境不忘来时路!🎬 博主简介: 引言:前篇文章,小编已经介绍了关于C++AVL树的实现!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于C++伸展树介绍以及红黑树的实现!伸展树与红黑树是两类极具代表性的BBST,且在工程实践中各有不可替代的价值:伸展树摒弃了"严格平衡”的执念,通过“伸展”操作将最近访问的节点移至根节点,利用“局部性原理”优化频繁访问的场景,实现均摊O(logn)的时间复杂度,适合缓存、热点数据查询等场景;红黑树则通过给节点着色并遵守严格的颜色规则,确保树的最长路径不超过最短路径的两倍,以 “弱平衡” 换稳定的最坏O(logn)性能,是C++ STL 中 std::map、std:

By Ne0inhk
C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板 💡 学习目标:掌握模板进阶技术的核心用法,理解模板特化的深层应用、类型萃取的实现原理,以及可变参数模板的灵活使用,提升泛型编程的实战能力。 💡 学习重点:模板特化的进阶场景、类型萃取工具的设计与应用、可变参数模板的展开技巧、折叠表达式的使用方法。 一、模板特化进阶:处理复杂类型场景 💡 模板特化不只是针对单一类型的定制,还能处理指针、引用、数组等复杂类型,实现更精细的类型适配逻辑。 1.1 指针类型的模板特化 通用模板默认处理普通类型,我们可以为指针类型单独编写特化版本,实现指针专属的逻辑。 #include<iostream>#include<string>usingnamespace std;// 通用模板:处理普通类型template<typenameT>classTypeProcessor{public:staticvoidprocess(T data){ cout

By Ne0inhk

C++ 设计模式概述及常用模式

C++ 设计模式概述 本文介绍了C++中23种设计模式的分类及实现示例,主要分为三大类: 创建型模式(5个):单例模式(常用)、工厂方法模式(常用)、抽象工厂模式(常用)、建造者模式和原型模式。这些模式专注于对象的创建机制。 结构型模式(7个):适配器模式(常用)、桥接模式、组合模式和装饰器模式(常用)等。这些模式处理类和对象的组合方式。 行为型模式:未完整列出,但包含观察者模式等(未展示完整代码)。 文章通过简洁的C++代码示例展示了常用设计模式的实现方法,如单例模式通过私有构造函数和静态方法确保唯一实例,工厂方法模式通过抽象工厂类创建产品等。这些模式为解决特定设计问题提供了可重用的解决方案。 C++ 设计模式概述及常用模式 设计模式可分为三大类:创建型、结构型、行为型。以下是23个设计模式的分类及代码示例: 一、创建型模式(5个) 1. 单例模式(Singleton)⭐ 常用 classSingleton{private:static

By Ne0inhk
C++测试与调试:确保代码质量与稳定性

C++测试与调试:确保代码质量与稳定性

C++测试与调试:确保代码质量与稳定性 一、学习目标与重点 本章将深入探讨C++测试与调试的核心知识,帮助你确保代码的质量与稳定性。通过学习,你将能够: 1. 理解测试与调试的基本概念,掌握测试方法和工具 2. 学会使用单元测试框架,如Google Test和Catch2 3. 理解集成测试的重要性,确保系统的功能正确性 4. 学会使用调试工具,如GDB和Visual Studio调试器 5. 培养测试与调试思维,设计高质量的代码 二、测试的基本概念 2.1 测试的分类 测试可以分为以下几类: * 单元测试:测试单个函数或类的功能 * 集成测试:测试多个模块的集成功能 * 系统测试:测试整个系统的功能 * 验收测试:测试系统是否满足用户需求 * 性能测试:测试系统的性能指标 2.2 测试原则 测试应该遵循以下原则: * 测试应该尽可能早地进行 * 测试应该覆盖所有可能的场景 * 测试应该是自动化的

By Ne0inhk