【动态规划】数位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。 ### 自己摸索的封装类 ```cpp template class CLowUperr { public: CLowUperr(int iCustomStatusCount) :m_iCustomStatusCount(iCustomStatusCount) { } void Init(const ELE* pLower, const ELE* pHigh, int iEleCount) { m_vPre.assign(4, vector(m_iCustomStatusCount)); if (iEleCount <= 0) { return; } InitPre(pLower, pHigh); iEleCount--; while (iEleCount--) { pLower++; pHigh++; vector> dp(4, vector(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: const int m_iCustomStatusCount; virtual void OnEnumOtherBit(vector& dp, const vector& vPre, ELE curValue) = 0; virtual void OnEnumFirstBit(vector& vPre, const ELE curValue) = 0; virtual void OnInitDP(vector>& dp) { } vector> m_vPre; private: void InitPre(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; } else if (*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的时间可以忽略。 ### 回调类 ```cpp template class IUpperDPCall { public: virtual void OnEnum(int n,vector& dp, const vector& vNext, ELE curValue, int iCustomStatusCount) = 0; virtual void OnInitEnd(vector& dp, vector& 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位数。

cpp 复制代码
template<class ELE, class ResultType>
class CUperrDP
{
public:
	CUperrDP(int iCustomStatusCount, IUpperDPCall<ELE, ResultType>& call, ELE minEle, ELE maxEle)
		:m_iCustomStatusCount(iCustomStatusCount),m_call(call),m_minEle(minEle),m_maxEle(maxEle)
	{
	}
	void Init(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() {
		return Sum(0, m_iCustomStatusCount - 1);
	}
	vector<vector<ResultType>> m_vDP, m_vDpUpper;
	const ELE m_minEle, m_maxEle;
protected:
	const int 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]未使用。

cpp 复制代码
template<class ELE, class ResultType>
class CUperrDP2
{
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)
	{
	}
	void Init(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() {
		return Sum(0, m_iCustomStatusCount - 1);
	}
	vector<vector<ResultType>> m_vDP, m_vDpUpper,m_vFirstDP;
	ELE m_minEle, m_maxEle, m_firstMinEle;
protected:
	const int 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 普及+

扩展阅读

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

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771

如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

测试环境

操作系统:win7 开发环境: VS2019 C++17

或者 操作系统:win10 开发环境: VS2022 C++17

如无特殊说明,本算法 用**C++**实现。

相关推荐
豆沙沙包?3 小时前
2025年--Lc194-516. 最长回文子序列(动态规划在字符串的应用,需要二刷)--Java版
java·算法·动态规划
胖咕噜的稞达鸭3 小时前
二叉树搜索树插入,查找,删除,Key/Value二叉搜索树场景应用+源码实现
c语言·数据结构·c++·算法·gitee
showmethetime3 小时前
基于相空间重构的混沌时间序列预测MATLAB实现
算法
进击的大海贼4 小时前
QT-C++ 自定义加工统计通用模块
开发语言·c++·qt
地平线开发者4 小时前
大模型 | VLM 初识及在自动驾驶场景中的应用
算法·自动驾驶
lingran__4 小时前
算法沉淀第四天(Winner)
c++·算法
AIzealot无4 小时前
Qwen3 Embedding报告随笔
人工智能·深度学习·算法·论文·embedding·论文笔记·搜广推
zzzsde4 小时前
【C++】深入理解string类(5)
java·前端·算法
清风wxy4 小时前
C语言基础数组作业(冒泡算法)
c语言·开发语言·数据结构·c++·windows·算法