关于数位dp
本来是8个月前答应写的,咕咕了240天,汗
数位dp
定义:from-OI Wiki
说点人话:
当一道关于特定数字计数的问题出现,值域范围到达\(10^{18}\),可以通过枚举每个数位上的数, 递推/搜索 出答案
小思想:关于统计区间\([L,R]\)之间数的数量
前缀和:\(sum[L,R]=sum[1,R]-sum[1,L-1]\)
具体例题
1. P2602 [ZJOI2010] 数字计数
可以发现,如果直接枚举数字,会炸掉,联想一下会枚举的数字
-
11111111
-
......
-
21111111
-
''''''
-
31111111
-
......
发现重合部分好多,可不可以避免重合呢?
直接告诉你 : 爆搜+剪枝
(比如)搜到亿位时,发现后面的数字出现次数相当,直接返回搜过的值
给出基本代码并讲解
int dfs(int pos,bool limit,int sum,bool lead0,int d){
int res=0;
if(!pos) return sum;
if(!limit&&dp[pos][sum]!=-1) return dp[pos][sum];
int up=limit?a[pos]:9;
for(int i=0;i<=up;i++) res+=dfs(pos-1,limit&&i==up,sum+((i||lead0)&&(i==d)),lead0||i,d);
if(!limit&&lead0) dp[pos][sum]=res;
return res;
}
从高到低,一位一位枚举填写的数字
-
pos : 当前填到第几位
-
limit(0/1) : 当前这填到的位数有没有限制(比如我要求\([1,92]\)之间的数,个位就不能填5)
-
sum : 数字$ d $ 的出现次数
-
lead0(0/1) : 有没有前导零(肯定不能统计 0000000123 中有7个0出现嘛)
-
d : 统计的数字是那个
额,逐行解释叭(注释代码)
int dfs(int pos,bool limit,int sum,bool lead0,int d){
int res=0;//统计答案,为数字记忆化搜索做准备
if(!pos) return sum;//搜到最后一位,结束搜索
//填数没有限制,以前搜索过后继状态,记忆化
if(!limit&&dp[pos][sum]!=-1) return dp[pos][sum];
int up=limit?a[pos]:9;//有限制时最大只为该数位上数
for(int i=0;i<=up;i++) res+=dfs(pos-1,limit&&i==up,sum+((i||lead0)&&(i==d)),lead0||i,d);
//后文解释
if(!limit&&lead0) dp[pos][sum]=res; //没有特殊限制 作记忆化
return res;//返回答案
}
res+=dfs(pos-1,limit&&i==up,sum+((i||lead0)&&(i==d)),lead0||i,d);
-
$ pos-1 $ : 搜索下一位
-
$ limit $ && $ i==up $ : 该位置有限制且枚举填数最大->下一位有限制
-
\(sum\)+\(((i||lead0)\)&&\((i==d))\):
\(sum\)指原有搜索结果,当没有前导0或i不为0时,i又等与统计的d,才\(sum+1\)
- \(lead0||i\) : 没有前导0或i不为0
配上拆数函数,也就是数组a的来历
int work(int x,int w){//拆数
memset(dp,-1,sizeof(dp));//每次拆数都有统计吖,要清空
lint len=0;
while(x){
a[++len]=x%10;
x=x/10;
}
return dfs(len,1,0,0,w);
}
2. HDU 2089 不要 62
记录一个递归参数 from(0/1):表示上一位是不是6,如果是,该位不能填2
而不选4直接在dfs中判断掉
3. P13085 [SCOI2009] windy 数
一样的,记录上一位数字(int),每次往下搜时判断一下即可
4. P4317 花神的数论题
改一下dp设定,\(dp[pos][cnt]\)表示在\([pos+1,len]\)中已经
已经填了\(cnt\)个1,\([1,pos]\)任意填,合法方案的乘积