看到这题用循环写的dp代码瑟瑟发抖~
数位dp一般记忆化搜索的写法思维难度较低,也比较常用,这题的简洁代码应该就可以显现出其优越性
(用时4ms,可能比用循环写的dp还要快)
那这里补充一下记忆化搜索的写法叭qwq~保姆式超详细讲解哦~
有注释代码
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
const ll P=1e9+7;
int A[25];
ll pw[25];
struct node {ll cnt,sum,sum2;} f[20][7][7];
/*
f[I][sum][num]表示的状态为:
从第I位开始,最高位到第I位每位数字之和(%7)为sum,整个数字(%7)为num
如对于数123***,I=3时,sum=6,num=123
注:f存储的是在没有贴合上界的情况下
因为没有贴合上界,即剩下i位可以从00…00~99…99随便填,所以无论数a[]是多少都可以适用,不需要每次都重置f数组
在该状态下,结构体中的
cnt表示与7无关的数的个数
sum表示所有与7无关的数的和
sum2表示所有与7无关的数的平方和
*/
node dfs(int I,int sum,int num,bool lim){ //当前在第I位,最高位到第I位每位数字之和(%7)为sum,整个数字(%7)为num,lim表示是否贴合上界
if (!I) return (node){sum && num , 0 , 0}; //数字已填完,根据题目要求,若sum和num都不为0(不能被7整除),则算一种方案
if (!lim && f[I][sum][num].cnt>=0) return f[I][sum][num]; //记忆化,如果不贴合上界(!lim),直接放回记录过的答案
int up=lim ? A[I] : 9; //第I位最大能填的数
node ans=(node){0,0,0};
for (int i=0 ; i<=up ; i++) //枚举第I位填的数
if (i!=7){
node J=dfs(I-1,(sum+i)%7,(num*10+i)%7,lim && i==up);
ll B=i*pw[I-1]; //B可以理解为当前层的基值,例如第I=5位填6,则B=60000
(ans.cnt+=J.cnt)%=P; //统计与7无关数出现次数
(ans.sum+=J.cnt*B+J.sum)%=P;
/*
统计所有与7无关数的和(用dfs(I-1)已经求出了所有无关数第I-1位到最后一位所组成的数之和,即J.sum,再加上第I位即可,即J.cnt*B)
例如I=5,已知无关数有**61111,**62222,**63333(随便瞎写的几个数字)
则B=60000,J.sum=1111+2222+3333,J.cnt=3,ans.sum=61111+62222+63333
*/
(ans.sum2+=J.cnt*B%P*B%P+J.sum2+2*J.sum%P*B%P)%=P;
/*
统计所有与7无关数第I位到最后一位所组成的数的平方和
例如I=5,已知无关数有**61111,**62222,**63333(随便瞎写的几个数字)
对于61111^2=(60000+1111)^2=(60000)^2+(1111)^2+2*60000*1111
62222,63333同理
则ans.sum2=61111^2+62222^2+63333^2
=3*(60000)^2 + (1111^2+2222^2+3333^2) + 2*60000*(1111+2222+3333)
=J.cnt*B*B + J.sum2 + 2*B*J.sum
可以发现,我们用后I-1位的sum2即可推算出后I位的sum2
*/
}
if (!lim) f[I][sum][num]=ans; //记忆化:如果不贴合上界(!lim),则记录
return ans;
}
ll solve (ll X){ //分解数位
int len=0;
for ( ; X ; X/=10) A[++len]=X%10;
return dfs(len,0,0,1).sum2;
}
int main(){
int T;
cin>>T,pw[0]=1,memset(f,-1,sizeof f);
for(int i=1 ; i<21 ; i++) pw[i]=pw[i-1]*10%P; //预处理10的幂
for (ll L,R ; T ; T--)
scanf("%lld%lld",&L,&R),printf("%lld\n",(solve(R)-solve(L-1)+P)%P);
}
无注解代码
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
const ll P=1e9+7;
int A[25];
ll pw[25];
struct node {ll cnt,sum,sum2;} f[20][7][7];
node dfs(int I,int sum,int num,bool lim){
if (!I) return (node){sum && num , 0 , 0};
if (!lim && f[I][sum][num].cnt>=0) return f[I][sum][num];
int up=lim ? A[I] : 9;
node ans=(node){0,0,0};
for (int i=0 ; i<=up ; i++)
if (i!=7){
node J=dfs(I-1,(sum+i)%7,(num*10+i)%7,lim && i==up);
ll B=i*pw[I-1];
(ans.cnt+=J.cnt)%=P;
(ans.sum+=J.cnt*B+J.sum)%=P;
(ans.sum2+=J.cnt*B%P*B%P+J.sum2+2*J.sum%P*B%P)%=P;
}
if (!lim) f[I][sum][num]=ans;
return ans;
}
ll solve (ll X){
int len=0;
for ( ; X ; X/=10) A[++len]=X%10;
return dfs(len,0,0,1).sum2;
}
int main(){
int T;
cin>>T,pw[0]=1,memset(f,-1,sizeof f);
for(int i=1 ; i<21 ; i++) pw[i]=pw[i-1]*10%P;
for (ll L,R ; T ; T--)
scanf("%lld%lld",&L,&R),printf("%lld\n",(solve(R)-solve(L-1)+P)%P);
}
有问题的话欢迎在评论区留言哦~
好强呀,tql,感觉数位dp比较复杂的题目应该使用dfs来写
是所有数位dp都可以用dfs来写,而且套路性极强。
😵😵是不是一开始就应该学记忆化搜索版本
记忆化版本确实好写些,不过感觉会写迭代说明对数位dp理解更深
安利一下算法基础课&提高课 笔记要点 + OIer必备小知识
题解制作不易,希望能对大家有帮助啦
没有特别懂lim在这里的作用诶
Orz
太强了,终于找到一个dfs写法了
tql %%% stO Orz
写的真好
记忆化搜索yyds
%%%%%%
巨巨,你的状态定义是怎么推出来的?
tql
if (!I) return (node){sum && num , 0 , 0};这里面为什么返回的为什么后面两个是0,0
好问题!
观察归并的代码,我们发现,其实第一层如果安排i这个数,
此时,如果ret.s1不是0,就加重复了,说白了,这个边界值到底是什么,还是有递推的关系式有关,研究一下边界时的关系式到底是啥,对不对,就明白到最后时需要给边界值啥样的初始值了,此题目中,数字和,数字平方和都是返回0才能使得推出来的式正确。
作者的题解写的很好,赞!但是记忆化搜索和dp的用时都是4ms(亲测)~不过博主的记忆化搜索写的很简洁!思维难度上确实小优于数位dp!
哈哈我当时没测dp的时间呢谢谢夸奖啦QwQ!嘻嘻
wuwu,太强啦
太强了顶一下
哈哈谢啦
呜呜呜tql聚聚
你这个题解写的也太完美太清楚了吧! 我一直认为题解写的清楚的人 才是真正了解题目trick的人. 太棒了! 收藏o( ̄▽ ̄)d
“恨7不成妻”<全网>最完美题解! 变量名清晰 逻辑结构化! 枉我浪费了三天百度搜csdn的题解!
哈哈过奖了过奖了,能对你有帮助也是我的荣幸呀qwq~
注释里“num表示所有与7无关的数的平方和”应该是“sum2表示所有…”
已更正,谢谢啦!
注释里“从第I位开始,最高位到第I位每位数字之和(%P)为sum,整个数字(%P)为num”,这里“%P”是不是应该变成%7啊。。
哦哦,不好意思看错了,的确是%7哈哈,已更正