一个讲得很好的blog:洛谷日报第84期:数字组成的奥妙——数位dp
一 . 数位DP
数位DP是与数位相关的一类计数类DP,一般用于统计区间[l,r]满足特定条件的数位元素个数。数位指个位、十位、百位、千位等,数位DP指的就是在数位上进行动态规划。数位DP实质上就是有策略的枚举,子问题求解后进行记忆化搜索即可。
二 . 需要注意
① 记忆化 ②约束条件 ③高位前导0
三 . 数位DP模板
- int dfs(int pos,int limit,int lead,int sum...各种限制)
- //pos表示填数到了第几位,limit表示有无上界限制,lead表示有无前导0
- {
- if(!pos) return sum;//边界条件
- if(!limit&&!lead&&dp[pos][...]!=-1)//如果搜过并且没有上界限制和前导0,这一位可以随便填
- return dp[pos][...];//记忆化搜索
- int top=limit?a[pos]:9;
- //如果前一位有限制(或填到了最高位)那么这一位不能超过a这一位的大小,否则0~进制数-1随便取
- int res=0;
- for(int i=0;i<=top;i++)
- {
- if(...)//限制条件
- res+=DFS(pos-1,limit&&(i==top),lead&&!i,...);
- //如果所在位置位已经枚举到上界就把上界往后传,上一位有前导0而且这一位是0,那么下一位有前导0
- }
- if(!limit&&!lead)dp[pos][...]=res;
- //因为如果枚举到上限则答案并不是这一位上所有的和,所以就不更新
- //着重分析这一句话:当有前导0或有上界限制时,下一位有不能填的数,而dp数组记录的是下一位能全部填0~9的数,所以不更新.
- return res;
- }
-
- int solve(int x)
- {
- int num=0;//位数
- while(x)//这里是预处理出每一位数的上界
- {
- a[++num]=x%10;
- x=x/10;
- }
- return dfs(num,1,1,...);//是否有前导0和上界限制等,注意通常统计总和的时候,初始化为1
- }
- int main()
- {
- cin>>l>>r;
- cout<<solve(r)-solve(l-1)<
- //可以先求1~r中满足限制的个数,再求1~l-1中满足限制的数的个数,相减即可得区间l~r的数据
- }
模板题
1 .统计一个区间的数字的各个数位之和
例题: 洛谷 P4999 烦人的数学作业 / 洛谷 P1836 数页码
思路:该题要求在区间内的数字各个数位相加所得的和,但是看数据范围
,可以发现数据范围很大,不能通过暴力做法,那么采用数位DP
- #include
- #include
- #include
- #include
- #define ll long long
- using namespace std;
- ll int T,l,r;
- const int maxn=1e7+5,mod=1e9+7;
- ll int dp[305][305],a[maxn];
- inline ll int read()
- {
- ll int x=0,f=1;
- char c=getchar();
- while(c<'0'||c>'9')
- {
- if(c=='-') f=-1;
- c=getchar();
- }
- while(c>='0'&&c<='9')
- {
- x=(x<<1)+(x<<3)+(c^48);
- c=getchar();
- }
- return x*f;
- }
-
- inline ll int solve(ll int pos,ll int res,ll int limit)
- //pos表示填数到了第几位,limit表示有无上界限制,res表示最后的总数
- {
- if(!pos) return res; //到达边界退回res
- if(!limit && dp[pos][res]!=-1) return dp[pos][res];
- int top=limit?a[pos]:9; //记录当前数位最大值可以到多少
- ll int result=0;
- for(int i=0;i<=top;++i)
- {
- result=(result+solve(pos-1,res+i,limit && i==top))%mod;
- //当前区间各数数位和
- }
- if(!limit) dp[pos][res]=result;
- return result;
- }
-
- inline ll int make(ll int x) //数位拆分
- {
- ll int num=0;
- while(x)
- {
- a[++num]=x%10;
- x/=10;
- }
- return solve(num,0,1)%mod;
- }
-
- int main()
- {
- T=read();
- memset(dp,-1,sizeof(dp));
- while(T--)
- {
- l=read(); r=read();
- printf("%lld\n",(make(r)-make(l-1)+mod)%mod);
- //有个细节:如果取模后的结果相减是负数时,需要加上模数
- }
- return 0;
- }
相关练习
1 . SP17247 PR003004 - Digit Sum
- #include
- #include
- #include
- #include
- #define ll long long
- #define re register
- using namespace std;
- ll int n,l,r;
- const int maxn=1e7+5;
- ll int dp[305][305],a[maxn];
- inline ll int read()
- {
- ll int x=0,f=1;
- char c=getchar();
- while(c<'0'||c>'9')
- {
- if(c=='-') f=-1;
- c=getchar();
- }
- while(c>='0'&&c<='9')
- {
- x=(x<<3)+(x<<1)+(c^48);
- c=getchar();
- }
- return x*f;
- }
-
- inline ll int solve(ll int pos,ll int res,ll int limit)
- {
- if(!pos) return res;
- if(!limit&&dp[pos][res]!=-1) return dp[pos][res];
- ll int top=limit?a[pos]:9;
- ll int cnt=0;
- for(re ll int i=0;i<=top;++i)
- {
- cnt+=solve(pos-1,res+i,limit&&(i==top));
- }
- if(!limit) dp[pos][res]=cnt;
- return cnt;
- }
-
- inline ll int make(ll int x)
- {
- ll int num=0;
- while(x)
- {
- a[++num]=x%10;
- x/=10;
- }
- return solve(num,0,1);
- }
-
- int main()
- {
- n=read();
- memset(dp,-1,sizeof(dp));
- while(n--)
- {
- l=read(); r=read();
- l=max(l-1,0ll);
- //因为这里的l可以取到0,所以当l等于0时,需要特殊处理一下;
- //因为0这里属于int型,强制转换long long类型
- printf("%lld\n",make(r)-make(l));
- }
- return 0;
- }
2 . 洛谷 P4317 花神的数论题
思路:由题得“已知
表示 i 的二进制表示中 1 的个数,求sum(1)∼sum(N) 的乘积”,先通过位运算统计数据中总共有几个1,再利用数位dp计算乘积。
- #include
- #include
- #include
- #include
- #define ll long long
- using namespace std;
- ll int n;
- const int maxn=1e7+5,mod=10000007;
- ll int a[maxn],dp[105][105];
- inline ll int read()
- {
- ll int x=0,f=1;
- char c=getchar();
- while(c<'0'||c>'9')
- {
- if(c=='-') f=-1;
- c=getchar();
- }
- while(c>='0'&&c<='9')
- {
- x=(x<<1)+(x<<3)+(c^48);
- c=getchar();
- }
- return x*f;
- }
-
- inline int solve(ll int pos,ll int limit,ll int sum)
- {
- if(!pos) return max(sum,1ll); //统计乘积,赋值为1
- if(!limit&&dp[pos][sum]!=-1) return dp[pos][sum];
- ll int top=limit?a[pos]:1; //最后的数字表示:进制减1
- ll int cnt=1; //因为统计乘积,赋值为1
- for(int i=0;i<=top;++i)
- {
- cnt=(cnt*solve(pos-1,limit&&(i==top),sum+i))%mod;
- //记得取模运算
- }
- if(!limit) dp[pos][sum]=cnt;
- return cnt;
- }
-
- inline ll int make(ll int x)
- {
- ll int num=0;
- while(x)
- {
- a[++num]=x & 1; //相当于x%2
- x=x>>1; //相当于x/2
- }
- return solve(num,1,0);
- }
-
- int main()
- {
- memset(dp,-1,sizeof(dp));
- n=read();
- printf("%lld",make(n));
- return 0;
- }
2 . 统计各个数在区间内的数位出现的次数
例题: 洛谷 P2602 [ZJOI2010] 数字计数 / SP3928 MDIGITS - Counting Digits/洛谷 P1239 计数器 / UVA1640 统计问题 The Counting Problem (该题注意最后末位不能输出多余的空格)
思路:由题得,需要统计各个数在区间内的数位上出现的次数。需要考虑有前导零的情况,要特别加上一个判断前导零的参数 lead。如果有前导零,且当前数码正好为 0,sum不变,然后接着扫下去,递归条件:sum+((i || !lead) && (i == num))。
- #include
- #include
- #include
- #include
- #define ll long long
- using namespace std;
- ll int l,r;
- const int maxn=1e6+5;
- ll int dp[105][105],a[maxn];
- inline ll int solve(ll int pos,ll int lead,ll int limit,ll int sum,int num)
- //pos用于记录存到了第几项,lead用于判断是否含有前导零,limit用于判断是否到达上界,sum用于记录总数
- //num用于记录当前寻找的是数字0~9中的哪一个
- {
- if(!pos) return sum;
- if(!limit&&!lead&&dp[pos][sum]!=-1) return dp[pos][sum];
- int top=limit?a[pos]:9;
- ll int res=0;
- for(ll int i=0;i<=top;++i)
- {
- res+=solve(pos-1,lead&&i==0,limit&&(i==top),sum+((i||!lead)&&(i==num)),num);
- }
- if(!limit&&!lead) dp[pos][sum]=res;
- return res;
- }
-
- inline ll int make(ll int x,int no)
- {
- ll int num=0;
- while(x)
- {
- a[++num]=x%10;
- x/=10;
- }
- return solve(num,1,1,0,no);
- }
-
- int main()
- {
- memset(dp,-1,sizeof(dp));
- while(scanf("%lld %lld",&l,&r)==2 && l!=0 && r!=0)
- {
- if(l>r) swap(l,r); //输入可能会存在后面的数比前面大的情况
- for(int i=0;i<=9;++i)
- {
- printf("%lld ",make(r,i)-make(l-1,i));
- }
- cout<
- }
- return 0;
- }
3 . 有限制条件的区间数位问题
例题 : 洛谷 P2657 [SCOI2009] windy 数
思路:由题得:“不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。”,那么存在限制条件:当前所在位的数字比它前一个数字至少大2或小2;以及当前数据是否含有前导0。
- #include
- #include
- #include
- #include
- #include
- #define ll long long
- using namespace std;
- ll int l,r;
- const int maxn=1e7+5;
- ll int dp[305][305],a[maxn];
- inline ll int read()
- {
- ll int x=0,f=1;
- char c=getchar();
- while(c<'0'||c>'9')
- {
- if(c=='-') f=-1;
- c=getchar();
- }
- while(c>='0'&&c<='9')
- {
- x=(x<<3)+(x<<1)+(c^48);
- c=getchar();
- }
- return x*f;
- }
-
- inline ll int solve(ll int pos,ll int lead,ll int limit,ll int check)
- //check用于统计当前位置的前一个数
- {
- if(!pos) return 1; //pos已经找到了最后一个,返回1
- if(!limit&&!lead&&dp[pos][check]!=-1) return dp[pos][check];
- ll int top=limit?a[pos]:9;
- ll int cnt=0;
- for(ll int i=0;i<=top;++i)
- {
- if(abs(i-check)<2) continue; //如果两数之间的差小于2,这两个点不满足条件,跳过
- if(lead&&i==0) cnt+=solve(pos-1,1,limit&&(i == top),-2);
- //如果有前导0,下一位随意选择
- else cnt+=solve(pos-1,0,limit&&(i == top),i);//如果没有前导0,继续向后搜
- }
- if(!limit&&!lead) return dp[pos][check]=cnt;
- return cnt;
- }
-
- inline ll int make(ll int x)
- {
- ll int num=0;
- while(x)
- {
- a[++num]=x%10;
- x/=10;
- }
- return solve(num,1,1,-2); //注意最高位的下一位是没有数据的,赋值为-2进行标记
- }
-
- int main()
- {
- memset(dp,-1,sizeof(dp));
- l=read(); r=read();
- printf("%lld",make(r)-make(l-1));
- return 0;
- }
-
相关阅读:
MLAgents (0) Unity 安装及运行
网络安全入门教程(非常详细)从零基础入门到精通,看完这一篇就够了。
【数据结构】串
BTCs打造区块链加营销广告数字流量新形式
Windows 95 的辉煌诞生历史
Linux下如何切换多版本Python
第1关:构造函数与析构函数的实现
Linux操作系统~基于systemV共享内存的进程间通信
我在华为度过的 “两辈子”(学习那些在大厂表现优秀的人)
perl删除目录下过期的文件
-
原文地址:https://blog.csdn.net/gzkeylucky/article/details/126664804