算法-数位dp
算法-数位dp
前置知识:
\(\texttt{dp}\)
\(\texttt{Dfs}\)
参考文献
https://www.cnblogs.com/y2823774827y/p/10301145.html
https://www.luogu.com.cn/blog/mak2333/solution-p2602
\(\texttt{Introduction}\)
数位 \(\texttt{dp}\) 是指求在数位限制下有多少满足要求的数的 \(\texttt{dp}\)。例如,求“在 \([L,R]\) 范围内连续出现过 \(3\) 个 \(3\) 的数”,“相邻两位之间差为质数的 \(5\) 位数”或“在 \([L,R]\) 区间内 \(6\) 出现的次数”。读完这篇文章以后,你就都会做了。
数位 \(\texttt{dp}\) 有两种主要方法:循环递推或记忆化搜索。
先讲循环递推,例题是数字计数。
\(\texttt{Description}\)
求在 \([a,b]\) 区间内的数 \(0\sim9\) 数字分别出现次数,前导 \(0\) 不算。
数据范围:\(1\le a\le b\le 10^{12}\)。
\(\texttt{Solution}\)
为了讲得更透彻,蒟蒻会把同一个东西用不同的方法多次描述,文章较长,请见谅。
Step 1 预处理
设 \(sum_{i,j}(1\le i\le 12,1\le j\le 9)\) 表示数字 \(j\) 在满 \(i\) 位整数(\([1,10^i-1]\))中出现的次数。因为除了 \(0\) 以外,\(1\sim 9\) 在这题中其实是一模一样的,所以 \(sum_{i,1}=sum_{i,2}=...=sum_{i,9}\)。
所以蒟蒻们还不如直接用 \(sum_i\) 表示 \(sum_{i,j}\),表示数字 \(1\sim9\) 在满 \(i\) 位整数中出现的次数。所以 \(sum_1=1\),因为 \(sum_2\) 可以由 \(sum_1\) 个数前面加 \(0\sim 9\) 递推得,也可以把数放在首位,所以
\]
\(sum\) 数列打表出来就是 \(1,20,300,4000,...\)。
code
void pro(){ //其实代码很短
ten[0]=1;//10^0=1
for(int i=1;i<=12;i++){
ten[i]=ten[i-1]*10;
sum[i]=sum[i-1]*10+ten[i-1];
}
}
Step 2 DP
预处理完 \(sum_i\) 后,可以抓一只 \(p\) 位数 \(n\) 求 \(0\sim9\) 在 \([1,n]\) 中出现的次数。首先设 \(nl_i(1\le i\le p)\) 表示 \(n\) 的从右往左第 \(i\) 位的数字。即
\]
code
int p; lng bit=n;
for(p=0;n;n/=10) nl[++p]=n%10;//最后p就是n的位数
然后令 \(f_j(0\le j\le 9)\) 表示数字 \(j\) 在 \([1,n]\) 中出现的次数。考虑 \([1,n]\) 中 \(i\) 位数中数字 \(j(1\le j\le 9)\) 的出现次数:
- 如果 \(j\) 为第 \(i\) 位(从右往左,即最高位,\(j\) 满足 \(1\le j<nl_i\)),则 \(j\) 出现了 \(10^{i-1}\) 次。
- 如果 \(j\) 不是第 \(i\) 位(\(j\) 满足 \(1\le j\le 9\)),则 \(j\) 出现了 \(nl_i\times sum_{i-1}\)。
- 如果 \(j\) 为第 \(i\) 位并且 \(j==nl_i\),则 \(j\) 出现了 \(n \mod 10^{i-1}+1\) 次(包括 \(nl_i0...00\))。
最后的问题——这个我们一直避着的 \(0\) 出现次数怎么算?
\]
比如 \(n=1000\),如果考虑前导 \(0\),数就会是 \(0000,0001,0002,0003,0004,...0999,1000\) 这样,有:
- 对于第 \(i\) 位的前导 \(0\),出现了 \(10^{i-1}\)。
又因为 \(p\) 位数就没有前导 \(0\) 了,所以前导 \(0\) 的总数根据 \(p\) 而定,跟 \(nl_i(1\le i\le p)\) 无关。
code
for(int i=p;i>=1;i--){
for(int j=0;j<=9;j++)
f[j]+=sum[i-1]*nl[i];
for(int j=0;j<=nl[i]-1;j++)
f[j]+=ten[i-1];
bit-=nl[i]*ten[i-1];//维护bit=n mod 10^(i−1)
f[nl[i]]+=bit+1;
f[0]-=ten[i-1];
}
最后,把 \(b\) 和 \(a-1\) 各当做 \(n\) 跑一次数位 \(\texttt{dp}\),作差就是答案。
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
//&Start
#define lng long long
//&Debug
void debug(int x,lng*arr){
for(int i=1;i<=x;i++)
printf("%lld%c",arr[i],"\n "[i<x]);
}
//&dpight
const int W=15;
lng ten[W],sum[W],fa[10],fb[10];
void pro(){
ten[0]=1;
for(int i=1;i<=12;i++){
ten[i]=ten[i-1]*10;
sum[i]=sum[i-1]*10+ten[i-1];
}
}
int nl[W];
void dp(lng n,lng*f){
int p; lng bit=n;
for(p=0;n;n/=10) nl[++p]=n%10;
for(int i=p;i>=1;i--){
for(int j=0;j<=9;j++)
f[j]+=sum[i-1]*nl[i];
for(int j=0;j<=nl[i]-1;j++)
f[j]+=ten[i-1];
bit-=nl[i]*ten[i-1];
f[nl[i]]+=bit+1;
f[0]-=ten[i-1];
}
}
//&Main
lng a,b;
int main(){
scanf("%lld%lld",&a,&b);
pro();
dp(a-1,fa), dp(b,fb);
for(int i=0;i<=9;i++)
printf("%lld%c",fb[i]-fa[i],"\n "[i<9]);
return 0;
}
然后是记忆化搜索,例题是\(\texttt{windy}\)数。
\(\texttt{Description}\)
求在 \([A,B]\) 中满足“相邻两个数字之差至少为 \(2\)”的数的数量。
数据范围:\(1\le A\le B\le 2000000000\)。
\(\texttt{Solution}\)
有人说记忆化搜索的数位 \(\texttt{dp}\) 就是套模板,但是如果你不懂原理,模板都套不起来。
同理,把求 \([A,B]\) 范围中 \(\texttt{windy}\) 数的数量变成求 \([1,B]\) 中的减去 \([1,A-1]\) 中的。
直接抓 \(p\) 位数 \(n\),\(nl_i\) 表示 \(n\) 从右往左第 \(i\) 位数,代码就不放了。
Step 1 求不满 \(p\) 位 \(\texttt{windy}\) 数数量
令 \(f_{i,j}\) 表示有 \(i\) 位,最高位是 \(j\) 的 \(\texttt{windy}\) 数数量,所以递推方程明显:
\[f_{1,j}=1(0\le j\le 9)
\] \[f_{i,j}=\sum\limits_{J=0,|j-J|\ge2}^9f_{i-1,J}(2\le i\le p,0\le j\le 9)
\]
然后有 \(i(1\le i<p)\) 位的 \(\texttt{windy}\) 数量就为
\]
。
code
void Pre(){
for(int j=0;j<=9;j++) f[1][j]=1;
for(int i=2;i<=10;i++)
for(int j=0;j<=9;j++)
for(int J=0;J<=9;J++)
if(abs(J-j)>=2) f[i][j]+=f[i-1][J];
}
//...
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
//...
for(int i=p-1;i>=1;i--)
for(int j=1;j<=9;j++)
res+=f[i][j];
return res;
}
Step 2 求 \(p\) 位 \(\texttt{windy}\) 数数量
记忆化搜索上场。
\]
表示当前要求从右往左第 \(w\) 位,第 \(w+1\) 位是 \(d\),\(free\) 表示前面从左往右的 \(p-w\) 位是否不和 \(n\) 的前 \(p-w\) 位相同。从 \(w\) 递归到 \(w-1\)。\(\texttt{Dfs}\) 的值表示这样的 \(\texttt{windy}\) 数数量。
首先因为同理在这题中 \(0\sim9\) 也是几乎相同的,除了顶到 \(nl_i\) 的情况。所以把除了 \(free==0\) 以外的状态 \((w,d)\) 的答案用记忆化搜索的数组
\]
记录下来。刚开始时 \(g_{w,d}=-1(1\le w\le p,0\le d\le 9)\),如果某次 \(\texttt{Dfs}\) 中发现已经 \(g_{w,d}\neq-1\),就直接返回 \(g_{w,d}\) 的值。
如果 \(w==0\) 就 \(return~1\),具体递归数位 \(\texttt{dp}\) 的方法看代码。
code
lng Dfs(int w,int d,bool free){
if(!w) return 1;
if(free&&~g[w][d]) return g[w][d];
//输出记忆答案,~x为真表示x!=-1
int up=free?9:nl[w]; lng res=0; //up是递归的下一个d最大值
for(int i=0;i<=up;i++)
if(abs(i-d)>=2)//满足windy数要求
res+=Dfs(w-1,i,free||i<up);//递归
if(free) g[w][d]=res; //储存记忆
return res;
}
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
memset(g,-1,sizeof g);//初始化
for(int i=1;i<=nl[p];i++) res+=Dfs(p-1,i,i<nl[p]);
//第一位的取值为[1,nl[p]]
for(int i=p-1;i>=1;i--)
for(int j=1;j<=9;j++)
res+=f[i][j];//不足p位的windy数总数
return res;
}
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
//%Start
#define lng long long
//%dp
const int W=15,D=10;
int nl[W];
lng a,b,f[W][D],g[W][D];
void Pre(){
for(int j=0;j<=9;j++) f[1][j]=1;
for(int i=2;i<=10;i++)
for(int j=0;j<=9;j++)
for(int J=0;J<=9;J++)
if(abs(J-j)>=2) f[i][j]+=f[i-1][J];
}
lng Dfs(int w,int d,bool free){
if(!w) return 1;
if(free&&~g[w][d]) return g[w][d];
int up=free?9:nl[w]; lng res=0;
for(int i=0;i<=up;i++)
if(abs(i-d)>=2)
res+=Dfs(w-1,i,free||i<up);
if(free) g[w][d]=res;
return res;
}
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
memset(g,-1,sizeof g);
for(int i=1;i<=nl[p];i++) res+=Dfs(p-1,i,i<nl[p]);
for(int i=p-1;i>=1;i--)
for(int j=1;j<=9;j++)
res+=f[i][j];
return res;
}
//%Main
int main(){
scanf("%lld%lld",&a,&b);
Pre();
printf("%lld\n",DP(b)-DP(a-1));
return 0;
}
然后放道例题,手机号码。
\(\texttt{Description}\)
[CQOI2016]手机号码
求在 \([L,R]\) 中,满足:
- 不能同时有 \(4\) 和 \(8\)。
- 出现过 \(3\) 个连续相同数。
的 \(11\) 位数个数。
数据范围:\(10^{10}\le L\le R<10^{11}\)。
\(\texttt{Solution}\)
用记忆化搜索好,用循环递推代码至少 \(100\) 行。
\]
要找从右往左第 \(w\) 位的数。
个数(从右往左第 \(w+1\) 个数)是 \(d\)。
上上个数(从右往左第 \(w+2\) 个数)是 \(ld\)。
\(free\) 表示前 \(p-w\) 位是否不和 \(n\) 的前 \(p-w\) 位相同。
\(h4\) 表示 \(4\) 是否在前 \(p-w\) 位中出现过。
\(h8\) 表示 \(8\) 是否在前 \(p-w\) 位中出现过。
\(h3\) 表示 \(3\) 个连续相同数是否在前 \(p-w\) 位中出现过。
然后用记忆化搜索数组 \(f_{w,d,ld,h4,h8,h3}\) 储存 \(\texttt{Dfs}\) 值(注意了,不能缺斤少两,不能用 \(f_{w,d,h4,h8,h3}\),必须把所以状态作为下标!),然后类似 \(\texttt{windy}\) 数地 \(\texttt{Dfs}\) 一下。具体见代码。
code
lng Dfs(int w,int d,int ld,bool free,bool h4,bool h8,bool h3){
if(h4&&h8) return 0ll;//剪枝,如果4和8已经同时
if(!w) return 1ll*h3;//如果w==0并且h3==1,return 1
if(free&&~f[w][d][ld][h4][h8][h3]) return f[w][d][ld][h4][h8][h3];
//输出记忆答案
int up=(free?9:nl[w]); lng res=0;//up是下一个d的最大值
for(int i=0;i<=up;i++)
res+=Dfs(w-1,i,d,free||i<up,h4||(i==4),h8||(i==8),h3||(i==d&&i==ld));
//递归,如果i==d&&i==ld,h3=1
if(free) f[w][d][ld][h4][h8][h3]=res;//储存答案
return res;
}
然后这题还有一个坑点,因为最后答案是 \(DP(R)-DP(L-1)\),而 \(L-1\) 可能是 \(10\) 位数,所以 \(\texttt{dp(n)}\) 时特判,如果 \(p\neq 11\),\(\texttt{dp(n)}=0\)。
code
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
if(p!=11) return 0;
// debug(p,nl);
memset(f,-1,sizeof f);
for(int i=1;i<=nl[p];i++)//只有11位数
res+=Dfs(p-1,i,-1,i<nl[p],(i==4),(i==8),0);
return res;
}
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
//^Start
#define lng long long
//^Debug
void debug(int x,int*arr){
for(int i=1;i<=x;i++)
printf("%d%c",arr[i],"\n "[i<x]);
}
//^DP
const int W=15,D=10;
int nl[W];
lng f[W][D][D+1][2][2][2];
void Pre(){/*Nothing*/}
lng Dfs(int w,int d,int ld,bool free,bool h4,bool h8,bool h3){
if(h4&&h8) return 0ll;
if(!w) return 1ll*h3;
if(free&&~f[w][d][ld][h4][h8][h3]) return f[w][d][ld][h4][h8][h3];
int up=(free?9:nl[w]); lng res=0;
for(int i=0;i<=up;i++)
res+=Dfs(w-1,i,d,free||i<up,h4||(i==4),h8||(i==8),h3||(i==d&&i==ld));
if(free) f[w][d][ld][h4][h8][h3]=res;
return res;
}
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
if(p!=11) return 0;
// debug(p,nl);
memset(f,-1,sizeof f);
for(int i=1;i<=nl[p];i++)
res+=Dfs(p-1,i,10,i<nl[p],(i==4),(i==8),0);
return res;
}
//^Main
lng L,R;
int main(){
scanf("%lld%lld",&L,&R);
Pre();
printf("%lld\n",DP(R)-DP(L-1));
return 0;
}
到此,我们可以总结出记忆化搜索版数位 \(\texttt{dp}\) 的模板了。
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
//^Start
#define lng long long
//^DP
const int W=15,D=10;
int nl[W];
lng f[W][]...[][][];
void Pre(){
/*
写些预处理
*/
}
lng Dfs(int w,/*w+1位等相关的数字*/,bool free,/*布尔类型的要求*/){
if(/*已经不符合*/) return 0;
if(!w&&/*符合*/) return 1;
if(free&&~f[w][]...[][][]) return f[w][]...[][][];
int up=(free?9:nl[w]); lng res=0;
for(int i=0;i<=up;i++)
res+=Dfs(w-1,/*相关数组递推*/,free||i<up,/*要求完成递推*/);
if(free) f[w][]...[][][]=res;
return res;
}
lng DP(lng n){
int p; lng res=0;
for(p=0;n;n/=10) nl[++p]=n%10;
if(/*已经不符合*/) return 0;
memset(f,-1,sizeof f);
for(int i=1;i<=nl[p];i++)
res+=Dfs(p-1,/*初始相关数*/,i<nl[p],/*初始要求完成情况*/);
return res;
}
//^Main
lng L,R;
int main(){
scanf("%lld%lld",&L,&R);
Pre();
printf("%lld\n",DP(R)-DP(L-1));
return 0;
}
两种数位 \(\texttt{dp}\) 那种好?
本蒟蒻认为记忆化搜索好,毕竟时间复杂度、空间复杂度两种都没什么区别,但 \(\texttt{Dfs}\) 又好想,代码又短,而且还有模板。要说数位 \(\texttt{dp}\) 的时间复杂度和空间复杂度,是根据题目而定的,并且除非特别毒瘤的题目,绝对不会 \(\texttt{TLE}\) 或 \(\texttt{MLE}\) 什么的。
练习题
数位 \(\texttt{dp}\) 的题到处都是,我想我也没有提供练习题的必要。
然后我就讲完了,祝大家学习愉快!
算法-数位dp的更多相关文章
- 算法笔记--数位dp
算法笔记 这个博客写的不错:http://blog.csdn.net/wust_zzwh/article/details/52100392 数位dp的精髓是不同情况下sta变量的设置. 模板: ]; ...
- 牛客寒假算法基础集训营3处女座和小姐姐(三) (数位dp)
链接:https://ac.nowcoder.com/acm/contest/329/G来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语言52428 ...
- 算法复习——数位dp
开头由于不知道讲啥依然搬讲义 对于引入的这个问题,讲义里已经很清楚了,我更喜欢用那个建树的理解···· 相当于先预处理f,然后从起点开始在树上走··记录目前已经找到了多少个满足题意的数k,如果枚举到第 ...
- 算法复习——数位dp(不要62HUD2089)
题目 题目描述 杭州人称那些傻乎乎粘嗒嗒的人为 62(音:laoer). 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司 ...
- bzoj 1026: [SCOI2009]windy数 & 数位DP算法笔记
数位DP入门题之一 也是我所做的第一道数位DP题目 (其实很久以前就遇到过 感觉实现太难没写) 数位DP题目貌似多半是问从L到R内有多少个数满足某些限制条件 只要出题人不刻意去卡多一个$log$什么的 ...
- 【算法】数位 dp
时隔多日,我终于再次开始写博客了!! 上午听了数位 dp,感觉没听懂,于是在网上进行一番愉 ♂ 快 ♀ 的学习后,写篇博来加深一下印象~~ 前置的没用的知识 数位 不同计数单位,按照一定顺序排列,它们 ...
- 「算法笔记」数位 DP
一.关于数位 dp 有时候我们会遇到某类问题,它所统计的对象具有某些性质,答案在限制/贡献上与统计对象的数位之间有着密切的关系,有可能是数位之间联系的形式,也有可能是数位之间相互独立的形式.(如求满足 ...
- 浅谈数位DP
在了解数位dp之前,先来看一个问题: 例1.求a~b中不包含49的数的个数. 0 < a.b < 2*10^9 注意到n的数据范围非常大,暴力求解是不可能的,考虑dp,如果直接记录下数字, ...
- 数位dp/记忆化搜索
一.引例 #1033 : 交错和 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 给定一个数 x,设它十进制展从高位到低位上的数位依次是 a0, a1, ..., an ...
随机推荐
- JUC锁种类总结
在并发编程中有各种各样的锁,有的锁对象一个就身兼多种锁身份,所以初学者常常对这些锁造成混淆,所以这里来总结一下这些锁的特点和实现. 乐观锁.悲观锁 悲观锁 悲观锁是最常见的锁,我们常说的加锁指的也就是 ...
- 04、MyBatis DynamicSQL(Mybatis动态SQL)
1.动态SQL简介 动态 SQL是MyBatis强大特性之一. 动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似. MyBatis 采用功能强大的基于 OGNL 的表达式来 ...
- spring-boot-starter-parent和spring-boot-dependencies
如何创建一个SpringBoot项目,SpringBoot的依赖引入都是基于starter的,通常创建一个SpringBoot项目都是通过继承关系指定pom文件中的parent. <parent ...
- Unity Lod
LOD是Level Of Detais 的简称,多细节层次,根据摄像机与物体距离,unity会自动切换模型.一般离摄像机近的时候显示高模,离摄像机远的时候显示低模,借此来提升性能. 如果你在Blend ...
- Weblogic CVE-2020-2551漏洞复现&CS实战利用
Weblogic CVE-2020-2551漏洞复现 Weblogic IIOP 反序列化 漏洞原理 https://www.anquanke.com/post/id/199227#h3-7 http ...
- Maximum execution time of 30 seconds exceeded in
在执行一次php脚本的时候,遇到了这样的报错,经过c Maximum execution time of 30 seconds exceeded in 翻译过来就是:执行时间超过了30秒最长执行时间: ...
- webug第三关:你看到了什么?
第三关:你看到了什么? 右键源码 扫描到test目录
- Druid配置和初始化参数 转发地址图片有
配置数据源 1.添加上 Druid 数据源依赖. <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dep ...
- 在IDM上设置防止过度抓取网站信息
在使用Internet Download Manager(IDM)下载器时,有时会发现IDM自带的抓取功能过于强大,以至于有时会抓取一些无效的链接.那么,该如何避免IDM的过度抓取呢? 图1:IDM的 ...
- FL Studio中的Fruity slicer采样器功能介绍
本章节采用图文结合的方式来给大家介绍电音编曲软件FL Studio中的Fruity Slicer采样器的功能,感兴趣的朋友可一起来交流哦. Fruity slicer(水果切片器)插件是FL Stud ...