讲解
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\n",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\n",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\n",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\n",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\n",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的数的个数
我一直在这里不太理解,终于懂了,戳这里【算法•日更•第十二期】信息奥赛一本通1585:【例 1】Amount of Degrees题解 - c1714-gzr - 博客园 (cnblogs.com)
但是结尾处有一句话:对于询问n,我们需要求出不超过n的最大B进制数,表示只含0、1的数:找到n的左起第一位非0、1的数位,将它变为1,并将它右面所有数位设成1。将得到的B进制数表示视为二进制进行询问即可。(出自刘聪的浅谈数位&信息奥赛一本通提高篇)
这句话只写作法,不写原因,使小编很长时间不能理解。
其实很好理解,比方说13,我们将它变成2进制的形式,那么就是(1101)2,也可以表示为这样的形式:13=1*23+1*22+0*21+1*20,那么对于一个数n,就可以这样表示:n = ai*ni + ai-1*ni-1 + ai-2*ni-2……所有的ai、ai-1、ai-2……不就都是0或1吗?(因为是二进制),注意审题,题中满足条件的数像这样表示,那么ai就只能是0或1,否则就不满足条件。回归正题:比方说593,变成8进制就是(1121)8,那么2就是第一个非1、0数位,那么就会把它变成(1111)8,这是为什么呢?我们会轻易的发现(1111)8到(1121)8区间内是绝对没有满足条件的数的,所以无需再找。其实说白了这个操作并不是转进制,而是1是能选,0是不选罢了,这个操作只是找到最大的区间内符合条件的数,刚好能当二进制罢了。
董老师的讲解:https://www.bilibili.com/video/BV1Ff4y1e7YW/?vd_source=23dc8e19d485a6ac47f03f6520fb15c2
#include<bits/stdc++.h>
using namespace std;
const int N=34;
int a[N]; //把B进制数的每一位抠出存入数组
int f[N][N]; //f[i][j]表示在i个位置上,放置j个1的组合数
int K, B;
void init(){ //预处理出组合数
for(int i=0; i<N; i++) f[i][0]=1;
for(int i=1; i<N; i++)
for(int j=1; j<=i; j++)
f[i][j]=f[i-1][j-1]+f[i-1][j];
}
int dp(int n){
if(!n) return 0; //特判,n==0返回0
int cnt=0;
while(n) a[++cnt]=n%B,n/=B; //把B进制数的每一位抠出存入数组
int res=0, last=0; //last表示第i位之前放置1的个数
for(int i=cnt; i>=1; i--){ //从高位到低位枚举
int x=a[i]; //取出第i位数字
if(x){ //第i位==0时,直接跳过,继续枚举下一位
res+=f[i-1][K-last]; //第i位放0
if(x>1){ //第i位>1时
if(K-last-1>=0) res+=f[i-1][K-last-1]; //第i位放1
break; //第i位放大于1的数,不合要求,则break
}
else{ //第i位==1时,不能用组合数计算,继续枚举下一位
last++; //更新last
if(last>K) break; //1的个数多于K,则break
}
}
if(i==1 && last==K) res++; //特判,走到末位的情况
}
return res;
}
int main(){
init(); //预处理出组合数
int l, r;
cin>>l>>r>>K>>B;
cout<<dp(r)-dp(l-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\n",solve(y)-solve(x-1));
}
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=1010;
const int INF=0x3fffffff;
typedef long long LL;
//不降数这道题的记忆化搜索
int bit[15];
int dp[15][10];
int l,r,len;
int dfs(int pos,int pre,int limit){
if(pos>len) return 1;
if(!limit&&dp[pos][pre]!=-1) return dp[pos][pre];
int up=limit? bit[len-pos+1]:9;
int ans=0;
for(int i=pre;i<=up;i++){
ans+=dfs(pos+1,i,limit&&i==up);
}
if(!limit) dp[pos][pre]=ans;
return ans;
}
int chan(int x){
len=0;
while(x){
bit[++len]=x%10;
x/=10;
}
memset(dp,-1,sizeof(dp));
return dfs(1,0,1);
}
int main(){
while(~scanf("%lld %lld",&l,&r)){
printf("%lld\n",chan(r)-chan(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]第i高位,j填的数字
//记忆化搜索
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int f[10][10],a[10];//f[第i高位][填的数字]
int dfs(int len,int st,int ze,int lim)//ze是否有前导零 lim:是否达到上限
{
if (len==0) return 1;
if (lim==0&&f[len][st]!=-1) return f[len][st];
int ans=0,ed;
if (lim==1) ed=a[len]; else ed=9;
for (int i=0;i<=ed;i++)
{
if (ze==1)
{
int zz=0;
if (i==0) zz=1;
if (lim==1&&i==ed) ans+=dfs(len-1,i,zz,1);
else ans+=dfs(len-1,i,zz,0);
}
else if (abs(st-i)>=2)
{
if (lim==1&&i==ed) ans+=dfs(len-1,i,0,1);
else ans+=dfs(len-1,i,0,0);
}
}
if (lim==0&&st!=0) f[len][st]=ans;
return ans;
}
int solve(int x)
{
memset(a,0,sizeof(a));
int len=0;
while (x)
{
a[++len]=x%10;
x/=10;
}
return dfs(len,0,1,1);
}
int main()
{
memset(f,-1,sizeof(f));
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",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\n",solve(r)-solve(l-1));
}
return 0;
}
1590:恨 7 不成妻
好难TAT https://blog.csdn.net/deerly_/article/details/79930085
https://blog.csdn.net/ten_three/article/details/19698055
需要维护三个值 --->定义结构体,假定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\n",(ans%MOD+MOD)%MOD);
}
return 0;
}
这个也很好理解
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define LL long long
#define mod 1000000007
struct node
{
LL s,sum,sqrsum;//个数N 各数之和 各数平方和
node()
{
s=sum=sqrsum=-1;
}
}f[20][10][10];//f[位数][各数位之和mod7][这个数mod7]
int a[20];
LL pi[20];
node dfs(int len,int s,int qs,int lim)//数位 各数位之和mod7 这个数mod7 是否上限
{
if (len==0)
{
node tt;
tt.s=tt.sum=tt.sqrsum=0;
if (s!=0&&qs!=0) tt.s=1;
return tt;
}
if (lim==0&&f[len][s][qs].sqrsum!=-1) return f[len][s][qs];
node ans; ans.s=ans.sum=ans.sqrsum=0;
int ed=0;
if (lim==1) ed=a[len]; else ed=9;
for (int i=0;i<=ed;i++)
{
if (i==7) continue;
/*
第len位填i f1,f2,f2...fN记录前len位已经填好的数字
对答案的贡献为: (f1+ i*10^(len-1) )^2+(f2+ i*10^(len-1) )^2+...+(fN+ i*10^(len-1) )^2
将平方和展开 整理 提公因式:
N*(i*10^(len-1))^2+ f1^2+f2^2+...+fN^2+ 2*(i*10^(len-1))*(f1+f2+...fN)
其中 N即为要求的s 2*(i*10^(len-1))*(f1+f2+...fN) 即为要求的sum 整一个即为sqrsum
*/
node tt;
if (lim==1&&i==ed) tt=dfs(len-1,(s+i)%7,(qs*10+i)%7,1);
else tt=dfs(len-1,(s+i)%7,(qs*10+i)%7,0);
LL si=(i*pi[len-1])%mod;
ans.s=(ans.s+tt.s)%mod;
ans.sum=(ans.sum+(tt.sum+ (si*tt.s)%mod ) %mod )%mod;
ans.sqrsum=(ans.sqrsum+tt.sqrsum+( ((si*si)%mod*tt.s)%mod+ ((2*si)%mod *tt.sum) %mod )%mod )%mod;
}
if (lim==0) f[len][s][qs]=ans;
return ans;
}
LL solve(LL x)
{
memset(a,0,sizeof(a));
int len=0;
while (x)
{
a[++len]=x%10;
x/=10;
}
node ans=dfs(len,0,0,1);
return ans.sqrsum%mod;
}
int main()
{
pi[0]=1;
for (int i=1;i<=18;i++) pi[i]=pi[i-1]*10;
int t;
scanf("%d",&t);
while (t--)
{
LL x,y;
scanf("%lld%lld",&x,&y);
if (x>y) swap(x,y);
printf("%lld\n",(solve(y)-solve(x-1)+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;
}
posted on
浙公网安备 33010602011771号