蒟蒻的笔记本一、数位dp

本博客搬自本人的洛谷博客 diker

蒟蒻的我学了一天终于稍微明白一点原理了orz。

1.对数位dp的一些理解

真如其名,数位dp就是在数位上做动态规划。
数位dp一般应用于:

求出在给定区间[A,B]内,符合条件P的数的个数。
条件P一般与数的大小无关,而与数的组成有关(也就是与这个数每个数位上的数有关)。因为在考虑数位时比较方便dp。

给个例题(这也是我开始学数位dp的万恶之源…):
https://www.nowcoder.com/acm/contest/190/D
求数n后面第m个是7的倍数或者含有数字7的数。
这题的题解后面加上,从简单的题开始。

2.关于含特定数字的题

题1:求区间[a,b]含数字7的数的个数。(1<=a,b<=2000000000)

求含7的数的个数和求不含7的数的个数是等价的。这里先写求不含7的数的个数。

首先总的想法是分别求出[0,a-1]和[0,b]上不含7的数的个数,然后相减。
然后是求[0,b]是考虑该如何dp,在i位数内满足条件的数的个数能否由i-1位数满足条件的个数转移得到呢。

【求不含7的数的个数】dp[i]代表i位数内不含7的数的个数,状态转移就是:

  • dp[i]=9*dp[i-1];
数位dp似乎往往记忆化搜索比递推好,因为在递推下每一位达到上界的情况不好处理。
#include <iostream>
using namespace std;
int dp[20],num[20],n,in;
int dfs(int ind,bool sg){//ind当前位数,sg是否到达上界
 if(ind==0) return 1;
    if((!sg&&dp[ind])) return dp[ind];//已经遍历过,直接返回结果
    int ret=0,ma=(sg?num[ind]:9);//如果上一位取到上界,当前位的数不能超上界
    for(int i=0;i<=ma;i++){
        if(i!=7) ret+=dfs(ind-1,sg&&i==num[ind]);//只有高位到达上界才会对低位构成限制
    }
    if(!sg) dp[ind]=ret;//在没有限制下才记忆化
    return ret;
}
int solve(int sn){
    in=0;
    while(sn){
        num[++in]=sn%10;//每一位的上界
        sn/=10;
    }
 return dfs(in,1);
}
int main(){
    int a,b;
    cin>>a>>b;
    cout<<b-a+1-(solve(b)-solve(a-1));//总数减去不含7的个数就是含7的个数
    return 0;
}

【直接求含7的数的个数】
用dp[i][0]代表i位数以内的数的个数,dp[i][1]代表i位数内第i位为7的满足条件的数的个数,dp[i][2]代表i位数内第i位不为7的满足条件的数的个数,状态转移为:

  • dp[i][0]=10*dp[i-1][0];
  • dp[i][1]=dp[i-1][0];
  • dp[i][2]=9dp[i-1][1]+9dp[i-1][2];
#include <iostream>
using namespace std;
int dp[20][3],num[20],n,in;
int dfs(int ind,int k,bool sg){//ind当前位数,k状态,sg是否到达上限
 if(ind==0) return k==0?1:0;
    if((!sg&&dp[ind][k])) return dp[ind][k];
    int ret=0,ma=(sg?num[ind]:9);
    for(int i=0;i<=ma;i++){//状态转移
        if(k==0) ret+=dfs(ind-1,0,sg&&i==num[ind]);
        if(k==1&&i==7) ret+=dfs(ind-1,0,sg&&i==num[ind]);
        else if(k==2&&i!=7) ret+=dfs(ind-1,1,sg&&i==num[ind])+dfs(ind-1,2,sg&&i==num[ind]);
    }
    return sg?ret:dp[ind][k]=ret;
}
int solve(int sn){
    in=0;
    while(sn){
        num[++in]=sn%10;
        sn/=10;
    }
 return dfs(in,1,1)+dfs(in,2,1);
}
int main(){
    int a,b;
    cin>>a>>b;
    cout<<solve(b)-solve(a-1);
    return 0;
}

题2:求区间[a,b]不含27的数的个数。(1<=a,b<=2000000000)
比不含7多了一位,那么在判断当前位是否为7时还应该考虑上一位是否为2,也就是说dfs函数还应该加一个参数用来判断上一位是否为2。

#include <iostream>
using namespace std;
int dp[20][2],num[20],n,in;
int dfs(int ind,bool ist,bool sg){//上一位是否为2
 if(ind==0) return 1;
    if((!sg&&dp[ind][ist])) return dp[ind][ist];
    int ret=0,ma=(sg?num[ind]:9);
    for(int i=0;i<=ma;i++){
     if(ist&&i==7) continue;//上一位为2当前位为7的情况跳过
        ret+=dfs(ind-1,i==2,sg&&i==num[ind]);
    }
    return sg?ret:dp[ind][ist]=ret;
}
int solve(int sn){
    in=0;
    while(sn){
        num[++in]=sn%10;
        sn/=10;
    }
 return dfs(in,1);
}
int main(){
    int a,b;
    cin>>a>>b;
    cout<<solve(b)-solve(a-1);
    return 0;
}

特定的数不止2位怎么办 关于特定的数数位在2位以上或位数不确定的情况,可以写一个类似与kmp里面的next数组,那么就只需要在dfs函数里加一个参数j表示匹配到特定数的第几位了。

3.关于能被特定的数整除

题3:求区间[a,b]不能被7整除且不含7的数的数的个数。(1<=a,b<=2000000000)
比不含7多了一个不被7整除的条件,在dfs函数里加一个参数c表示当前数余7的情况。

#include <iostream>
using namespace std;
int dp[20][10],num[20],n,in;
int dfs(int ind,int c,bool sg){
 if(ind==0) return c==0?0:1;
    if((!sg&&dp[ind][c])) return dp[ind][c];
    int ret=0,ma=(sg?num[ind]:9);
    for(int i=0;i<=ma;i++){
        if(i!=7) ret+=dfs(ind-1,(c*10+i)%7,sg&&i==num[ind]);
    }
    return sg?ret:dp[ind][c]=ret;
}
int solve(int sn){
    in=0;
    while(sn){
        num[++in]=sn%10;
        sn/=10;
    }
 return dfs(in,0,1);
}
int main(){
    int a,b;
    cin>>a>>b;
    cout<<solve(b)-solve(a-1);
    return 0;
}

4.第k个满足条件的数

【回到开头的题】
题4:
链接:https://www.nowcoder.com/acm/contest/190/D
来源:牛客网

题目描述

今天是个特殊的日子,CSL和他的小伙伴们围坐在一张桌子上玩起了明七暗七的游戏。
游戏规则是这样的: 一个人报出一个起始数,接下来按照逆时针的顺序轮流报数,如果碰到数是7的倍数或含有7,则拍手,下一个人接着报数。直到有一个人报错了数字或者没有及时拍手为止。
玩游戏嘛,当然得有惩罚。这么简单的游戏对CSL的学霸小伙伴而言实在是太无脑了,轻轻松松数到上万根本不在话下。
但是对于数学是体育老师教的CSL来说,实在是太难了。快帮他算算什么时候应该拍手吧。

输入描述:

输入两个整数m和n。(1 ≤ m, n ≤ 1012)

输出描述:

输出一个整数,表示m以后第n个需要拍手的数字。
分析:显然这是一道数位dp题,数位dp能统计出一个区间内满足条件的数的个数,可是我们要求第k个,怎么办呢?考虑区间[0,a]与区间内满足条件的数的个数f(a)之间的关系,随着a的增大,f(a)也是增大的,显然是具有二分的性质,转化为经典的二分问题,我们二分当前数的大小,看它是第几大的,就可以了.

#include <iostream>
#define LL long long
using namespace std;
int num[20],digit;
LL dp[20][10];
LL dfs(int ind,int c,int sg){//题3的dfs函数
    if(ind==0) return c==0?0:1;
    if(!sg&&dp[ind][c]) return dp[ind][c];
    LL ret=0;int ma=(sg?num[ind]:9);
    for(int i=0;i<=ma;i++){
        if(i!=7) ret+=dfs(ind-1,(c*10+i)%7,sg&&i==num[ind]);
    }
    return sg?ret:dp[ind][c]=ret;
}
LL solve(LL nu){
    digit=0;
    LL tm=nu;
    while(nu){
        num[++digit]=nu%10;
        nu/=10;
    }
    return tm+1-dfs(digit,0,1);//总数减去不满足条件的数
}
int main(){
    LL m,n,l,r,mid;
    cin>>m>>n;
    l=m;r=m+7*n;//上下边界,ans最大肯定不超m+7*n
    m=solve(m);
    while(l<r){
        mid=(l+r)>>1;
        if(solve(mid)-m>=n) r=mid;//等于的情况要取区间左边,因为需要找的是第一个等于n的数
        else l=mid+1;
    }
    cout<<r;
    return 0;
}
tbc…
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值