数位DP 详解
序
天堂在左,战士向右
引言
数位DP在竞赛中的出现几率极低,但是如果不会数位DP,一旦考到就只能暴力骗分。
以下是数位DP详解,涉及到的例题有:
- [HDU2089]不要62
- [HDU3652]B-number
概述
首先我们要理清的是,到底数位DP是什么。
事实上,一般数位DP的题目题面描述都会有以下内容:
- 求出一段区间\([l,r]\)中,满足某一特殊条件的数有多少个
在例题1 不要62中,特殊条件是数中不能出现"62";在例题2 B-number中,特殊条件是数中出现了13且该数可以被13整除;
一般题目中的数据范围\([l,r]\)会使得\(O(n)\)超时。因此,直接遍历将无法拿到全分。而数位DP则是在范围内按位递推出最大值的快捷算法。以例题2 B-number中的数位DP为例:

足以显示出数位DP的优越性。
主要实现
由于DP的本质就是记忆化搜索,我们通过记忆化搜索的方式实现动态规划。这种方式相比正面递推,在大多数情况下要简介一些。
一、搜索过程
以例题1 不要62为例。
首先,我们需要定义以下基本的变量与函数,它们是在所有数位DP中通用的:
int digit[maxn];
int dfs(int len,int fp,int str){
}
其中,\(digit[i]\)表示一个数在所有数位都取最大值的情况下,第\(i\)位的最大值。
而dfs函数中的\(len\)表示当前层我们还需处理多少数位。当\(len\)的值为\(-1\)时,则代表我们已经处理到了最低位。
fp则代表当前数位是否受到最高位的限制。举个例子,我们规定在一次运算中\(r\)的值为\(530\)。此时我们已经计算到了第2位。若前一位为\(5\),则这一位最大也只能取\(3\),否则会超出\(r\)的限制,此时fp的值为1;若前一位小于\(5\),则当前位不受最高位的限制,我们可以取任意数字,则此时fp的值为0。
str则代表当前状态,我们稍后再做解释。
这时,我们分析题面,发现:
- 限制条件只有一个,即不能出现62
因此,我们可以将这一限制条件填入已经设出的状态\(str\)中。当之前的数位中已经出现了62,我们就使其为1,否则我们使其为0。
这时,根据这一条件,就可以设出DP数组了
int dp[maxn][100];//表示在处理到第i位、之前的数位中出现/未出现62使的方案数。
不过此时,我们会发现一个问题:如果上一位出现了6,而是否出现62由当前位决定时,怎么办呢?因此,我们要对\(str\)的定义稍作更改。
我们令\(str\)表示:若上一位出现了\(6\),则\(str=1\);若已经出现了\(62\),则\(str=2\);否则\(str=0\)。
此时,我们已经可以通过定义写下判断状态的子函数了。
int check(int str,int i){
if(str==0){
if(i==6)return 1;
return 0;
}
else if(str==6){
if(i==6)return 1;
if(i==2)return 2;
return 0;
}
return 2;
}
回过头来,我们再来继续完成dfs函数。
首先,写下当我们搜索到最后一位时的返回操作与记忆化搜索的返回操作。
int digit[maxn];
int dfs(int len,int fp,int str){
if(len==-1)return str==2;
if(!fp && dp[len][str])return dp[len][str];
//条件中的!fp是对[l,r]取开区间。
}
接下来我们要做的是判断当前状态下我们能取到的最大数位。
int fpmax=fp?digit[len]:9;
接着我们再遍历搜索下一个数位,并返回答案。
int ret=0;//返回值
for(register int i=0;i<=fpmax;i++){
ret+=dfs(len-1,fp && i==fpmax ,check(str,i));
}
return dp[len][str]=ret;
整个子函数的代码如下:
int dfs(int len,int fp,int str){
if(len==-1)return str==2;
if(!fp && ~dp[len][str])return dp[len][str];
int fpmax=fp?digit[len]:9,ret=0;
for(register int i=0;i<=fpmax;i++){
ret+=dfs(len-1,fp && i==fpmax,check(str,i));
}
return dp[len][str][rel]=ret;
}
而例题1 不要62的代码如下:
#include<bits/stdc++.h>
#define int long long
//#define local
using namespace std;
int dp[100][200][100],digit[100];
int check(int str,int i){
if(str==0){
if(i==6)return 1;
return 0;
}
else if(str==6){
if(i==6)return 1;
if(i==2)return 2;
return 0;
}
return 2;
}
int dfs(int len,int fp,int str){
if(len==-1)return str==2;
if(!fp && ~dp[len][str])return dp[len][str];
int fpmax=fp?digit[len]:9,ret=0;
for(register int i=0;i<=fpmax;i++){
ret+=dfs(len-1,fp && i==fpmax,check(str,i));
}
return dp[len][str][rel]=ret;
}
int f(int n){
int len=0;
while(n){
digit[len++]=n%10;
n/=10;
}
return dfs(len-1,1,0);
}
signed main(){
#ifdef local
freopen("1.txt","r",stdin);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int a,b;
memset(dp,-1,sizeof(dp));
while(cin>>b>>a){
//cout<<"-->"<<b<<endl;
printf("%d\n",f(b)-f(a-1));
}
//cout<<f(1000)<<endl;
return 0;
}
注意,在f函数中,可以看到我们首次dfs的代码是
dfs(len-1,1,0);
为什么\(len\)的值为总长度-1,而不是总长度本身呢?
因为这样我们处理到最后时\(len=-1\)而不是\(len=0\)
换句话说,只是笔者的习惯而已233333
细节-关于状态
事实上,不是所有时候DP数组都只用开二维。
在很多时候,我们都要在dfs函数中同时记录我们当前处理到的数是多少,例如例题2 B-number。
在这道题中,我们要处理我们记录的数是否能被13整除,因此我们要对DP数组作一点小小的微调。
int dp[maxn][5][20];
多出来的一维用于记录计算出来的数对13取模后的值。不记录其本身是因为空间限制,且失去了数位DP的优越性。
对于dfs中所处理的数的记录,不难想到用这样的方法:
int dfs(int len,int fp,int str,int rel){
for(register int i=1;i<=9;i++)dfs(len-1,fp && i==digit[i],check(str,i),rel*10+i);
}
因此,对于例题2 B-number,我们的完整代码变成了这样:
#include<bits/stdc++.h>
#define int long long
//#define local
using namespace std;
int dp[100][200][100],digit[100];
int check(int str,int i){//返回:0-->什么都没有,1-->已出现过1,2-->已出现过13
if(str==0){
if(i==1)return 1;
return 0;
}
else if(str==1){
if(i==1)return 1;
if(i==3)return 2;
return 0;
}
return 2;
}
int dfs(int len,int fp,int rel,int str){
if(len==-1)return rel==0&&str==2;
if(!fp && ~dp[len][str][rel])return dp[len][str][rel];
int fpmax=fp?digit[len]:9,ret=0;
for(register int i=0;i<=fpmax;i++){
ret+=dfs(len-1,fp&&i==fpmax,(rel*10+i)%13,check(str,i));
}
return fp?ret:dp[len][str][rel]=ret;
}
int f(int n){
int len=0;
while(n){
digit[len++]=n%10;
n/=10;
}
return dfs(len-1,1,0,0);
}
signed main(){
#ifdef local
freopen("1.txt","r",stdin);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int b;
memset(dp,-1,sizeof(dp));
while(cin>>b){
//cout<<"-->"<<b<<endl;
printf("%d\n",f(b));
}
//cout<<f(1000)<<endl;
return 0;
}
后记
数位dp虽然大多在套模板,但是里面的判断和细节还是很多的,多写几道数位dp之后才能发现其中的规律,完全将其掌握。
数位DP 详解的更多相关文章
- 动态规划晋级——HDU 3555 Bomb【数位DP详解】
转载请注明出处:http://blog.csdn.net/a1dark 分析:初学数位DP完全搞不懂.很多时候都是自己花大量时间去找规律.记得上次网络赛有道数位DP.硬是找规律给A了.那时候完全不知数 ...
- 数位DP详解
算法使用范围 在一个区间里面求有多少个满足题目所给的约束条件的数,约束条件必须与数自身的属性有关 下面用kuangbin数位dp的题来介绍 例题 不要62 题意:在一个区间里面求出有多少个不含4和6 ...
- 数位dp详解&&LG P2602 [ZJOI2010]数字计数
数位dp,适用于解决一类求x~y之间有多少个符合要求的数或者其他. 例题 题目描述 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除 ...
- 状压DP详解(位运算)
前言: 状压DP是一种非常暴力的做法(有一些可以排除某些状态的除外),例如dp[S][v]中,S可以代表已经访问过的顶点的集合,v可以代表当前所在的顶点为v.S代表的就是一种状态(二进制表示),比如 ...
- 状态压缩dp 状压dp 详解
说到状压dp,一般和二进制少不了关系(还常和博弈论结合起来考,这个坑我挖了还没填qwq),二进制是个好东西啊,所以二进制的各种运算是前置知识,不了解的话走下面链接进百度百科 https://baike ...
- 线性DP详解
顾名思义,线性DP就是在一条线上进行DP,这里举一些典型的例子. LIS问题(最长上升子序列问题) 题目 给定一个长度为N的序列A,求最长的数值单调递增的子序列的长度. 上升子序列B可表示为B={Ak ...
- 状态压缩动态规划(状压DP)详解
0 引子 不要999,也不要888,只要288,只要288,状压DP带回家.你买不了上当,买不了欺骗.它可以当搜索,也可以卡常数,还可以装B,方式多样,随心搭配,自由多变,一定符合你的口味! 在计算机 ...
- 状压DP详解+题目
介绍 状压dp其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有1或0 ,是另一类非常典型的动态规划 举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述 ...
- 树形DP详解+题目
关于树形dp 我觉得他和线性dp差不多 总结 最近写了好多树形dp+树形结构的题目,这些题目变化多样能与多种算法结合,但还是有好多规律可以找的. 先说总的规律吧! 一般来说树形dp在设状态转移方程时都 ...
随机推荐
- Docker学习总结(四)--应用部署
MySQL部署 1) 拉取 mysql 镜像 docker pull centos/mysql:5.7 2) 创建容器 docker run -di --name=mysql -p 33306:330 ...
- Java线程之线程简介
Java线程之线程简介 一.何谓线程 明为跟踪处理流程,实为跟踪线程 阅读程序时,我们会按处理流程来阅读. 首先执行这条语句 ↓ 然后执行这条语句 ↓ 接着再执行这条语句…… 我们就是按照上面这样的流 ...
- Spring依赖注入浅析
1. 概念理解 依赖注入 谁注入谁? 依赖对象注入IoC容器. 控制反转 谁控制谁?控制什么? IoC容器控制对象,控制依赖对象的创建与注入. 为什么称为反转?创建.注入对象的控制权由程序员的主观意愿 ...
- HelloDjango 第 13 篇:分类、归档和标签页
作者:HelloGitHub-追梦人物 文中涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 侧边栏已经正确地显示了最新文章列表.归档.分类.标签等信息.现在来完善归档.分类和标签 ...
- Foxmail管理多个邮箱
使用Foxmail管理邮箱还是很方便的. 1. 下载Foxmail. 2. 双击,输入想关联的邮箱名称和密码,收取邮件即可. 3. 如果想关联多个账号,可点击Foxmail右上角的菜单栏,选择账户管理 ...
- P2486 [SDOI2011]染色 维护区间块数 树链剖分
https://www.luogu.org/problemnew/show/P2486 题意 对一个树上维护两种操作,一种是把x到y间的点都染成c色,另一种是求x到y间的点有多少个颜色块,比如11 ...
- 洛谷-P1414 又是毕业季II -枚举因子
P1414 又是毕业季II:https://www.luogu.org/problemnew/show/P1414 题意: 给定一个长度为n的数列.要求输出n个数字,每个数字代表从给定数列中最合理地取 ...
- 牛客 136G-指纹锁 set容器重载
136G-指纹锁 题意: 设计一个容器,支持插入x,若与容器中的值最小相差为k,则自动忽略.删除操作,把与x相差为k的值都从容器中删除.查询操作,问容器中有没有和x相差为k的数值. 思路: 一个stl ...
- Joyful HDU - 5245 概率问题
Sakura has a very magical tool to paint walls. One day, kAc asked Sakura to paint a wall that looks ...
- CodeForces 402 E Strictly Positive Matrix
Strictly Positive Matrix 题解: 如果原来的 a[i][j] = 0, 现要 a[i][j] = 1, 那么等于 sum{a[i][k] + a[k][j]} > 1. ...