算法-数位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 ...
随机推荐
- 【JVM】肝了一周,吐血整理出这份超硬核的JVM笔记(升级版)!!
写在前面 最近,一直有小伙伴让我整理下关于JVM的知识,经过十几天的收集与整理,初版算是整理出来了.希望对大家有所帮助. JDK 是什么? JDK 是用于支持 Java 程序开发的最小环境. Java ...
- @AliasFor注解
@AliasFor注解 @AliasFor是一个注解,用于为注解属性声明别名. 代码如下:它有两个属性value和attribute @AliasFor注解注释了 自身,并且value和attribu ...
- Innodb之全局共享内存
参考链接: https://blog.csdn.net/miyatang/article/details/54881547 https://blog.csdn.net/wyzxg/article/de ...
- Angular 之装饰器@Input
Input 一个装饰器,用来把某个类字段标记为输入属性,并提供配置元数据. 该输入属性会绑定到模板中的某个 DOM 属性.当变更检测时,Angular 会自动使用这个 DOM 属性的值来更新此数据属性 ...
- bWAPP----HTML Injection - Reflected (POST)
bWAPP--low--HTML Injection - Reflected (POST) 只不过是把传递方式换成post, 防护的三个级别和内容与GET相同 1 function htmli($da ...
- 还不懂java类加载机制的,建议看下这份阿里技术官总结的笔记!
类加载机制 把class文件加载到内存,并对数据进行校验,准备,解析,初始化,形成可以被虚拟机直接使用的字节码 类加载的时机(触发类的初始化) 使用new关键字实例化对象 读取一个类的静态代码块 使用 ...
- 移动端调试Web页面
移动端调试Web页面 虽然可以在PC下,通过开发者工具,模拟移动端,但是这样只能模拟页面样式,对于代码的执行情况是无法模拟的,所以在此结合实际调试经验,针对安卓与IOS设备,进行总结. IOS 安卓 ...
- python中操作excel数据 封装成一个类
本文用python中openpyxl库,封装成excel数据的读写方法 from openpyxl import load_workbook from openpyxl.worksheet.works ...
- web自动化 下拉框、切换到新窗口
一.下拉框 相信大家在手动测试web页面时,遇到过下拉框吧,那进行web自动化测试时,如何操作下拉框,且看下文 1.selenium中提供了方法,先导入Select方法 from selenium.w ...
- 早安打工人! 来把你的.NET程序模块化吧
嗨朋友们,大家好! 还记得我是谁吗? 对了! 我就是 .NET 打工人 玩双截棍的熊猫 今天呐,我特别要向 写框架 的朋友们,想要写框架 ** 的朋友们,已经有框架** 的朋友问声好! 为什么呢?因为 ...