数位DP
Posted shirlybaby
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数位DP相关的知识,希望对你有一定的参考价值。
讲解
https://blog.csdn.net/brazy/article/details/77427699
https://blog.csdn.net/wust_zzwh/article/details/52100392
数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数。所谓数位dp,字面意思就是在数位上进行dp。
数位的含义:一个数有个位、十位、百位、千位......数的每一位就是数位啦!
之所以要引入数位的概念完全就是为了dp。数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。
这些问题的特征是给定的区间特别大,不能一个个暴力的解决,必须用O(logN)的方法才行
模板
typedef long long ll; int a[20]; ll dp[20][state];//不同题目状态不同 ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零 { //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了 if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */ //第二个就是记忆化(在此前可能不同题目还能有一些剪枝) if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state]; /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/ int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了 ll ans=0; //开始计数 for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了 { if() ... else if()... ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的 /*这里还算比较灵活,不过做几个题就觉得这里也是套路了 大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论 去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目 要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类, 前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/ } //计算完,记录状态 if(!limit && !lead) dp[pos][state]=ans; /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/ return ans; } ll solve(ll x) { int pos=0; while(x)//把数位都分解出来 { a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行 x/=10; } return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛 } int main() { ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { //初始化dp数组为-1,这里还有更加优美的优化,后面讲 printf("%lld ",solve(ri)-solve(le-1)); } }
单纯的不要4,两种写法,递推、记忆化搜索(基本也是这两种方法)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //两种方法 ,递推和记忆化搜索 //单纯的不要4 //递推 int dp[20][10]; //表示第i位数,第1个数字是j时符合条件的数字数量 int a[20]; void inti(){ //初始化先 dp[0][0]=1; for(int i=1;i<=12;i++){ for(int j=0;j<10;j++){ for(int k=0;k<10;k++){ if(j!=4) dp[i][j]+=dp[i-1][k]; } } } } int solve1(int len){ int ans=0; for(int i=len;i>=1;i--){ //从高位到低位处理 for(int j=0;j<a[i];j++) if(j!=4){ ans+=dp[i][j]; if(a[i]==4) { ans--;break; } } return ans; } //记忆化搜索 int l,r,a[20]; int dp[20]; int dfs(int len,int ismax){ int ans=0; int up; if(!len) return 1; if(!ismax&&dp[len]!=-1) return dp[len]; up=ismax? a[len]:9; for(int i=0;i<=up;i++){ if(i==4) continue; ans+=dfs(len-1,ismax&&i==a[len]); } if(!ismax) dp[len]=ans; return ans; } int sovle2(int x){ int len=0; memset(dp,-1,sizeof(dp)); while(x){ a[++len]=x%10; x/=10; } return dfs(len,1) } int main(){ return 0; }
1、2089 不要62
数字不能出现4和62(连续的)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e7+10; const int INF=0x3fffffff; typedef long long LL; LL dp[maxn][2]; //后一维为0表示前面不是6,为1表示前面是6 int l,r; int len[10]; LL dfs(int pos,int pre,int state,int limit){ if(pos==-1) return 1; //当已经到底了,就返回 if(!limit&&dp[pos][state]!=-1) return dp[pos][state]; int up=limit? len[pos]:9; LL ans=0; for(int i=0;i<=up;i++){ //下标从0开始 if(i==4) continue; if(pre==6&&i==2) continue; ans+=dfs(pos-1,i,i==6?1:0,limit&&i==len[pos]); //state 为 i } //保存结果 if(!limit) dp[pos][state]=ans; return ans; } LL solve(LL x){ int l=0; while(x){ len[l++]=x%10; x/=10; } return dfs(l-1,0,0,1); //后面是1 } int main(){ while(scanf("%d %d",&l,&r)){ if(l==0&&r==0) break; memset(dp,-1,sizeof(dp)); printf("%lld ",solve(r)-solve(l-1)); } return 0; }
数字从左到右看不要有先递增后递减的情况、数位DP题的关键在于如何分析下一个数字的情况
//这道题要考虑前导0的影响 //而且对数字前后的要求 int t; char s[110]; int mod= 1000000007; int a[110]; LL dp[110][10][3]; //分别表示位数、前面的数pre,倾向turn LL dfs(int pos,int pre,int turn,bool limit,bool inv){ ///turn:0不清楚,1下降,2上升 //这条语句中pos是要DP的位置,pre,turn,limit,invalid...这些都是前提条件,或者 //说是之前位置上确定一些数之后的状态,而这个dfs要进行的就是在这个状态下继续 //确定下一位的数字 if(pos==-1) return inv? 0:1; if(!limit&&dp[pos][pre][turn]!=-1) return dp[pos][pre][turn]; int up=limit? a[pos]:9; LL ans=0LL; //注意要加LL for(int i=0;i<=up;i++){ if(turn==2&&i<pre) continue; //不能先上升在下降 int p=0; if(i==pre) p=turn; else if(i<pre) p=1; else p=2; if(inv) p=0; //随时控制前导0 ans+=dfs(pos-1,i,p,limit&&i==a[pos],inv&&i==0); ans%=mod; } ans%=mod; if(!limit) dp[pos][pre][turn]=ans; return ans; } int main(){ cin>>t; while(t--){ scanf("%s",s); memset(dp,-1,sizeof(dp)); int len=strlen(s); for(int i=0;i<len;i++) a[i]=s[len-i-1]-‘0‘; printf("%lld ",dfs(len-1,0,0,1,1)); } return 0; }
3、HDU 4734
题目给了个f(x)的定义:F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,Ai是十进制数位,然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。
常规想:这个f(x)计算就和数位计算是一样的,就是加了权值,所以dp[pos][sum],这状态是基本的。a是题目给定的,f(a)是变化的不过f(a)最大好像是4600的样子。如果要memset优化就要加一维存f(a)的不同取值,那就是dp[10][4600][4600],这显然不合法。
这个时候就要用减法了,dp[pos][sum],sum不是存当前枚举的数的前缀和(加权的),而是枚举到当前pos位,后面还需要凑sum的权值和的个数,
也就是说初始的是时候sum是f(a),枚举一位就减去这一位在计算f(i)的权值,那么最后枚举完所有位 sum>=0时就是满足的,后面的位数凑足sum位就可以了。
仔细想想这个状态是与f(a)无关的(新手似乎很难理解),一个状态只有在sum>=0时才满足,如果我们按常规的思想求f(i)的话,那么最后sum>=f(a)才是满足的条件。
但是在函数调用的时候
intdfs(int pos,int sum,bool limit) 里面的sum表示的还是当前已经有的数
//减法 int f(int x){ if(x==0) return 0; int ans=f(x/10); return ans*2+(x%10); } int dp[10][5000]; int a[12]; int all; int dfs(int pos,int sum,int limit){ if(pos==-1) return sum<=all; //注意这里 if(sum>all) return 0; //如果大的话,就直接返回 if(!limit&&dp[pos][all-sum]!=-1) return dp[pos][all-sum]; //减法 int up=limit? a[pos]:9; int ans=0; for(int i=0;i<=up;i++){ ans+=dfs(pos-1,sum+i*(1<<pos),limit&&i==a[pos]); //计算 } if(!limit ) dp[pos][all-sum]=ans; return ans; } int sovle(int x){ int len=0; while(x){ a[len++]=x%10; x/=10; } return dfs(len-1,0,1); } int main(){ int t,op=1; int a,b; scanf("%d",&t); memset(dp,-1,sizeof(dp)); while(t--){ scanf("%d %d",&a,&b); all=f(a); //sovle(b); printf("Case #%d: %d ",op++,sovle(b)); } return 0; }
4、POJ 3252
这题的约束就是一个数的二进制中0的数量要不能少于1的数量,通过上一题,这题状态就很简单了,dp[pos][num],到当前数位pos,0的数量减去1的数量不少于num的方案数,一个简单的问题,中间某个pos位上num可能为负数(这不一定是非法的,因为我还没枚举完嘛,只要最终的num>=0才能判合法,中途某个pos就不一定了),这里比较好处理,Hash嘛,最小就-32吧(好像),直接加上32,把32当0用。这题主要是要想讲一下lead的用法,显然我要统计0的数量,前导零是有影响的。至于!lead&&!limit才能dp,都是类似的,自己慢慢体会吧
//要考虑前导零的影响,因为题目的要求 //dp[pos][num],到当前数位pos,0的数量减去1的数量不少于num的方案数,一个简单的问题, //中间某个pos位上num可能为负数(这不一定是非法的,因为我还没枚举完嘛,只要最终的num>=0才能判合法, //中途某个pos就不一定了),这里比较好处理,Hash嘛,最小就-32吧(好像),直接加上32,把32当0用。这题主要是要想讲一下lead的用法, //显然我要统计0的数量,前导零是有影响的。!lead&&!limit才能dp int dp[35][100]; int a[60]; int dfs(int pos,int sta,bool lead,bool limit){ //位数、0-1的数,前导零,上限 if(pos==-1) return sta>=32; if(!lead&&!limit&&dp[pos][sta]!=-1) return dp[pos][sta]; int up=limit? a[pos]:1; //上限 int ans=0; for(int i=0;i<=up;i++){ if(lead&&i==0) //如果有前导零,就略过 ans+=dfs(pos-1,sta,lead,limit&&i==a[pos]); else ans+=dfs(pos-1,sta+(i==0? 1:-1),lead&&i==0,limit&&i==a[pos]); } if(!limit&&!lead) dp[pos][sta]=ans; return ans; } int solve(LL x){ int len=0; while(x){ a[len++]=x&1; x>>=1; } return dfs(len-1,32,1,1); //以32为起点,以免中间有负数 } int main(){ memset(dp,-1,sizeof(dp)); LL a,b; scanf("%lld %lld",&a,&b); printf("%d ",solve(b)-solve(a-1)); return 0; }
一本通
1585: 【例 1】Amount of Degrees
跟上面的减法一样的思想,但是这个也有树的思想
论文和题解:
https://wenku.baidu.com/view/d2414ffe04a1b0717fd5dda8.html
统计区间[0,x]内二进制表示含k个1的数的个数 。统计一棵高度为 i 的完全二叉树内二进制表示中恰好含有 j 个 1的数的个数
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //这道题感觉理解还是不是很彻底 //https://blog.csdn.net/primoblog/article/details/13168287?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3 int dp[32][32],a[32]; int k,b,x,y; void inti(){ dp[0][0]=1; for(int i=1;i<=31;i++){ dp[i][0]=dp[i-1][0]; //有题解提到用完全二叉树理解 for(int j=1;j<=i;j++) dp[i][j]=dp[i-1][j]+dp[i-1][j-1]; } } int calc(int xx){ ////统计区间[0,x]内二进制表示含k个1的数的个数 //统计一棵高度为 i 的完全二叉树内二进制表示中恰好含有 j 个 1的数的个数 int tot=0,ans=0,len=0;//tot记录当前路径上已有的1的数量,ans为答案 while(xx){ a[++len]=xx%b; xx/=b; } for(int i=len;i>0;i--){ if(a[i]==1){ ans+=dp[i-1][k-(tot++)]; if(tot==k) break; } else if(a[i]>1){ ans+=dp[i][k-tot]; break; } } return tot==k?ans+1:ans; } int main(){ scanf("%d %d %d %d",&x,&y,&k,&b); inti(); //初始化 printf("%d ",calc(y)-calc(x-1)); return 0; }
1586:【 例 2】数字游戏
指定一个整数闭区间 [a,b],问这个区间内有多少个不降数。
int f[32][32]/f[位数i][第i位(最高位)的数字] 的合理情况
递推的做法:
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; int f[32][32],a[32]; // //f[位数i][第i位(最高位)的数字] void inti(){ //初始化 for(int i=1;i<=31;i++) f[1][i]=1; for(int i=2;i<=31;i++){ for(int j=0;j<=9;j++){ for(int k=j;k<=9;k++){ f[i][j]+=f[i-1][k]; } } } } int solve(int x){ int len=0; memset(a,0,sizeof(a)); while(x){ a[++len]=x%10; x/=10; } int ans=0; //方案数 for(int i=len;i;i--){ if(a[i+1]>a[i]) break; //不降数,下降了 for(int j=a[i+1];j<a[i];j++){ //不降数,必须比前一个大,比现在这个小 ans+=f[i][j]; } if(i==1) ans++; //本身也是 (一位数) } return ans; } int main(){ int x,y; inti(); while(scanf("%d %d",&x,&y)!=EOF){ printf("%d ",solve(y)-solve(x-1)); } return 0; }
记忆化搜索的做法:
LL dp[20][20][2]; 分别表示位第i位,填的数字为j,是否是上界,是否是前导0
#include<bits/stdc++.h> using namespace std; const int maxn = 12; int n,l,r,f[maxn][maxn][2]; char buf[maxn]; int dfs(int pos,int pre,int limit){ if(pos==n) return 1; if(f[pos][pre][limit]) return f[pos][pre][limit]; int mx=limit?buf[pos]-‘0‘:9,ans=0; for( int i=0; i<=mx; i++ ) if(i>=pre) ans+=dfs(pos+1,i,limit&(i==mx)); return f[pos][pre][limit]=ans; } int solve(int num){ memset(f,0,sizeof(f)); sprintf(buf,"%d",num),n=strlen(buf); return dfs(0,0,1); } int main() { while(cin>>l>>r) cout<<solve(r)-solve(l-1)<<‘ ‘; return 0; }
1587: 【例 3】Windy 数
Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为2的正整数被称为 Windy 数。
递推的做法(感觉要好理解一点)TAT
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //看了一下,觉得这道题用递推更好理解 int f[32][32],a[32]; // //f[位数i][第i位(最高位)的数字] void inti(){ //递推都需要初始化 for(int i=0;i<=9;i++) f[1][i]=1; //只有1位 for(int i=2;i<=31;i++){ for(int j=0;j<=9;j++){ for(int k=0;k<=9;k++){ if(abs(j-k)>=2) f[i][j]+=f[i-1][k]; } } } } int solve(int x){ memset(a,0,sizeof(a)); int len=0; while(x){ a[++len]=x%10; // cout<<a[len]<<" "; x/=10; } // cout<<endl; //按照顺序处理位数 int ans=0; for(int i=1;i<len;i++){ //不足len的位的部分 for(int j=1;j<=9;j++) ans+=f[i][j]; //不能从0开始 } for(int i=1;i<a[len];i++) //第len位不足a[len]的部分 ans+=f[len][i]; //然后处理第len位为a[len]的数据,因为是很大的树,所以要控制上限 for(int i=len-1;i;i--){ for(int j=0;j<a[i];j++){ //最高位已经确定了,所以可以取到0了 if(abs(j-a[i+1])>=2) ans+=f[i][j]; ////跟前一位比较 } if(abs(a[i+1]-a[i])<2) break; if(i==1) ans++; //?因为上面处理 不足len的位的部分 的时候没有处理1位 } return ans; } int main(){ inti(); int x,y; cin>>x>>y; cout<<solve(y)-solve(x-1)<<endl; return 0; }
记忆化搜索 四维 dp[i][j][Bo1][Bo2]第i位,填的数字为j,是否是上界,是否是前导0
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; int a,b,len; int dp[30][30][2][2]; int num[30]; //https://www.cnblogs.com/gaojunonly1/p/10360015.html //https://blog.csdn.net/qq_42367531/article/details/82526133 //windy数,也是很友好的数位dp,也像数字游戏一样搞一搞,dp[i][j][Bo1][Bo2]第i位,填的数字为j,是否是上界,是否是前导0就over了 int dfs(int pos,int pre,int limit,int inv){ //位数,前面的数,上界,前导零 if(pos==1) return dp[pos][pre][limit][inv]=1; if(dp[pos][pre][limit][inv]) return dp[pos][pre][limit][inv]; int up=limit? num[pos-1]:9; //int ans=0; for(int i=0;i<=up;i++){ if(inv||abs(pre-i)>1){ int b1=(limit&&i==up); //判断方式!! int b2=(inv&&i==0); dp[pos][pre][limit][inv]+=dfs(pos-1,i,b1,b2); } } return dp[pos][pre][limit][inv]; } int solve(int n){ if(n==0) return 1; memset(dp,0,sizeof(dp)); len=0; while(n){ num[++len]=n%10; n/=10; } int ans=0; ans+=dfs(len,0,0,1); // for(int i=1;i<num[len];i++){ ans+=dfs(len,i,0,0); } ans+=dfs(len,num[len],1,0); return ans; } /* //只用二维 ////f[第i高位][填的数字] int f[10][10]; int dfs(int pos,int pre,int inv,int limit){ if(pos==0) return 1; if(limit==0&&dp[pos][inv]!=-1) return dp[pos][inv]; int ans=0,up; up=limit? a[pos]:9; for(int i=0;i<=up;i++){ if(limit==1){ //有前导零 int zz=(i==0)?1:0; //inv的设置也为zz if(limit==1&&i==up) ans+=dfs(pos-1,i,zz,1); //上界限制为1 else ans+=dfs(pos-1,i,zz,0); //上界限制为0 } else if(abs(pre-i)>=2){ if(limit&&i==up) ans+=dfs(pos-1,i,0,1); //前导零为0,但是上界限制为1 else ans+=dfs(pos-1,i,0,0); //上界限制也为0 } } if(!limit&&!inv) f[len][pre]=ans; return ans; } //调用的时候 return dfs(len,0,1,1); */ int main(){ scanf("%d %d",&a,&b); printf("%d",solve(b)-solve(a-1)); return 0; }
1588:数字游戏
某人又命名了一种取模数,这种数字必须满足各位数字之和mod N=0 。现在大家又要玩游戏了,指定一个整数闭区间[a,b],问这个区间内有多少个取模数。
这道题还比较简单,但是要注意函数写法,返回的是summ==0; //返回一个判断
ans+=dfs(pos+1,(summ+i)%k,limit&&i==up);
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; int l,r,k; int dp[40][110]; int num[40]; int len; int dfs(int pos,int summ,int limit){ if(pos>len) return summ==0; //返回一个判断 if(dp[pos][summ]!=-1&&!limit) return dp[pos][summ]; int up=limit?num[len-pos+1]:9; int ans=0; for(int i=0;i<=up;i++){ ans+=dfs(pos+1,(summ+i)%k,limit&&i==up); } if(!limit) dp[pos][summ]=ans; return ans; } int solve(int x){ memset(num,0,sizeof(num)); memset(dp,-1,sizeof(dp)); len=0; while(x){ num[++len]=x%10; x/=10; } return dfs(1,0,1); } int main(){ while(~scanf("%d %d %d",&l,&r,&k)){ printf("%d ",solve(r)-solve(l-1)); } return 0; }
1590:恨 7 不成妻
好难TAT https://blog.csdn.net/deerly_/article/details/79930085
需要维护三个值 --->定义结构体,假定dfs推出返回的结构体是tmp,当前结果的结构体是ans
1.符合条件数的个数 cnt
2.符合条件数的和 sum
3.符合条件数的平方和 sqr
三个条件
(1)数中某一位是 7; 基础的数位dp很好维护;
(2)整数的每一位加起来的和是7 的整数倍;
tmp.sum * 10 + (10 ^ pos * i) * ans.cnt 就是上一步状态的和加上这一步加的
这一步加的就是10的当前位次方乘以i,因为有ans.cnt个嘛,所以再乘以ans.cnt
(3)这个整数是 7 的整数倍。
3 首先重新构建一下这个数 (10^pos * i + x)x是这个数的后面部分,就是上一次状态得到的那个数,则平方和就是(10^len*i)^2+x^2+2*10^len*i*x, 其中x^2=tmp.sqr;
ans.sqr += (2*10^pos*i*x)*tmp.cnt=(2*10^pos*i)*next.sum(神奇的化简)
ans.sqr += (10^pos*i)^2*tmp.cnt;
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; const long long MOD = 1e9 + 7; typedef long long LL; //好难啊 /* 整数中某一位是 77; 整数的每一位加起来的和是 77 的整数倍; 这个整数是 77 的整数倍。 首先这三个条件都是基础数位dp,就不说了。 重点是怎么去求平方和。 需要维护三个值 1.符合条件数的个数 cnt 2.符合条件数的和 sum 3.符合条件数的平方和 sqr 为什么要维护这三个呢,接着往下看你就知道了 假定dfs推出返回的结构体是tmp,当前结果的结构体是ans 其中1是基础的数位dp很好维护; 2 tmp.sum * 10 + (10 ^ pos * i) * ans.cnt 就是上一步状态的和加上这一步加的 这一步加的就是10的当前位次方乘以i,因为有ans.cnt个嘛,所以再乘以ans.cnt 3 首先重新构建一下这个数 (10^pos * i + x)x是这个数的后面部分,就是上一次状态得到的那个数,则平方和就是(10^len*i)^2+x^2+2*10^len*i*x, 其中x^2=tmp.sqr; ans.sqr += (2*10^pos*i*x)*tmp.cnt=(2*10^pos*i)*next.sum(神奇的化简) ans.sqr += (10^pos*i)^2*tmp.cnt; 原文链接:https://blog.csdn.net/deerly_/article/details/79930085 */ LL p[25]; //这个是位数,1 10 100 1000 10000这种 LL num[40]; struct node{ //要用到结构体 LL cnt,summ,sqr; //符合条件数的个数 符合条件数的和 符合条件数的平方和 node(){cnt=-1,summ=sqr=0;} node(LL cnt,LL summ,LL sqr) : cnt(cnt),summ(summ),sqr(sqr){} }dp[20][20][20]; LL t,l,r,len; node dfs(int pos,int sum1,int sum2,bool limit){ //sum1为每一位加起来的和,sum2 if(pos==0){ if(sum1&&sum2){ return node(1,0,0); //个数为1 } return node(0,0,0); } if(!limit&&dp[pos][sum1][sum2].cnt!=-1) return dp[pos][sum1][sum2]; int up=limit? num[pos]:9; node ans; ans.cnt=0; for(int i=0;i<=up;i++){ if(i==7) continue; node temp=dfs(pos-1,(i+sum1)%7,(sum2*10+i)%7,limit&&i==up); ans.cnt+=temp.cnt; //这个可以直接加,很好维护 ans.cnt%=MOD; ans.summ+=(temp.summ+((p[pos]*i)%MOD)*temp.cnt%MOD)%MOD; ans.summ%MOD; //看看这个怎么计算的 ans.sqr+=(temp.sqr+((2*p[pos]*i)%MOD)*temp.summ)%MOD; ans.sqr%=MOD; ans.sqr+=((temp.cnt*p[pos])%MOD*p[pos]%MOD*i*i%MOD); ans.sqr%=MOD; } if(!limit) dp[pos][sum1][sum2]=ans; return ans; } LL solve(LL n){ len=0; while(n){ num[++len]=n%10; n/=10; } node v=dfs(len,0,0,1); return v.sqr; } int main(){ scanf("%d",&t); p[1]=1; for(int i=2;i<=20;i++) p[i]=(p[i-1]*10)%MOD; while(t--){ scanf("%lld %lld",&l,&r); LL ans=solve(r); ans-=solve(l-1); printf("%lld ",(ans%MOD+MOD)%MOD); } return 0; }
1591:数字计数
给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码digit 各出现了多少次。
10次dfs
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; LL l,r; LL len; LL dp[20][110]; LL num[20]; LL dfs(int pos,int dig,LL summ,int lead,int limit){ //位数 上一个数字 方案数 前导0 上界 if(pos>len) return summ; if(dp[pos][summ]!=-1&&!limit&&!lead) return dp[pos][summ]; int up=limit? num[len-pos+1]:9; //是递增的 LL ans=0; for(int i=0;i<=up;i++){ if(lead&&i==0) //有前导零 ans+=dfs(pos+1,dig,0,1,up==i&&limit); //方案数位0 else ans+=dfs(pos+1,dig,summ+(i==dig?1:0),0,i==up&&limit); //是对应的数字的话,那么方案数+1 } if(!limit&&!lead) dp[pos][summ]=ans; return ans; } LL work(LL x,int dig){ memset(dp,-1,sizeof(dp)); len=0; while(x){ num[++len]=x%10; x/=10; } return dfs(1,dig,0,1,1); } int main(){ scanf("%lld %lld",&l,&r); if(l){ for(int i=0;i<=9;i++){ printf("%lld ",work(r,i)-work(l-1,i)); } } else{ for(int i=0;i<=9;i++){ printf("%lld ",work(r,i)-work(l,i)); } } return 0; }
#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#include<stack>#include<cstdio>#include<queue>#include<map>#include<vector>#include<set>using namespace std;const int maxn=1e7+10;const int INF=0x3fffffff;typedef long long LL;LL dp[maxn][2]; //后一维为0表示前面不是6,为1表示前面是6int l,r;int len[10];LL dfs(int pos,int pre,int state,int limit){ if(pos==-1) return 1; //当已经到底了,就返回if(!limit&&dp[pos][state]!=-1) return dp[pos][state];int up=limit? len[pos]:9;LL ans=0;for(int i=0;i<=up;i++){ //下标从0开始 if(i==4) continue;if(pre==6&&i==2) continue;ans+=dfs(pos-1,i,i==6?1:0,limit&&i==len[pos]);//state 为 i } //保存结果 if(!limit) dp[pos][state]=ans;return ans; } LL solve(LL x){int l=0;while(x){len[l++]=x%10;x/=10;}return dfs(l-1,0,0,1); //后面是1 }int main(){while(scanf("%d %d",&l,&r)){if(l==0&&r==0) break;memset(dp,-1,sizeof(dp));printf("%lld ",solve(r)-solve(l-1));}return 0;}
以上是关于数位DP的主要内容,如果未能解决你的问题,请参考以下文章