数位dp学习小结

数位dp学习小结:

数位dp解决的基本问题:给定区间[l,r],求出某些满足条件限制的的的数字总数。这些数字并不一定是10进制下,也有可能在26进制下进行。
举个栗子:
1)给定区间[l,r]统计区间内数字数位上没有连续的 62 和 4的数字个数。例如621,40都是非法数字。
2)26进制数字给出区间[l,r]统计数字上有连续的SHU出现的数字个数。

解题核心:dp状态的建立和递推方程的构建。
理论上状态的增加可以通过增加dp数组的维数来表示。
下面的一道道例题中自行体会。

给出个模板框架(我也是学长手中传下来的 )

#include<bits/stdc++.h>

typedef long long LL;
int a[20];
LL dp[20][state];
LL dfs(int pos,/*state变量*/,bool limit/*数位上界变量*/)
{
    if(pos==-1) return 1;
    if(!limit && dp[pos][state]!=-1) return dp[pos][state];
    int up=limit?a[pos]:9;
    LL ans=0;
    for(int i=0;i<=up;i++)
    {
        if() ...
        else if()...
        ans+=dfs(pos-1,/*状态转移*/,limit && i==a[pos])
    }
    if(!limit ) dp[pos][state]=ans;
    return ans;
}

LL solve(LL x)
{
    int pos=0;
    while(x)
    {
        a[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1/*从最高位枚举*/, /*一系列状态 */ ,true);
}

int main()
{
    LL le,ri;
    memset(dp,-1,sizeof dp);
    while(~scanf("%lld%lld",&le,&ri))
    {
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}

看不懂模板没有关系,看一道例题分析分析:
HDU 2089
题意:
给定区间[l,r]统计区间内数字数位上没有连续的 62 和 4的数字个数。
思路:
1)先转化成求0~n中满足条件的的数字个数solve(n),则ans=solve ( r ) -solve(l-1)
2)首先对数字x某个数位pos来说如果不考虑限制:则该pos位可取的数字为0~9,考虑大小限制则为0 ~ a[pos] (x的第pos位数字是什么)
3)这题可划分三种情况:
pos位下一位填4,直接跳过不枚举
pos位填了6,下一位填2也应当跳过不枚举
其他情况合法。
4)dp数组维数的确定,第一维:数位,第二位:0,1代表枚举到当前数位的前一位是否含有数字6.
AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[20]; //数位数组
ll dp[20][2]; //位数+state数位限制
//state 表示i == 6 该数字是否包含数位6
ll dfs(int pos,int state,bool limit){ //数位,限制条件,数位限制变量
    if(pos==-1) return 1;
    if(!limit&&dp[pos][state]!=-1){ //记忆化搜索答案在dp数组之中如果已经搜索过不需要再搜索
        return dp[pos][state];
    }
    int up = limit?a[pos]:9;//数位限制
    ll ans = 0;
    for(int i=0;i<=up;++i){
        if((state&&i==2)||i==4) continue;
        /*三种情况
        1)本位有6下一位为2跳过
        2)下一位直接为4跳过
        3)符合题意
        */
        ans += dfs(pos-1,i==6,limit&&i==a[pos]);
    }
    if(!limit){
        dp[pos][state] = ans;
    }
    return ans;
}

ll solve(ll x){
    int pos = 0;
    //将数字的每一位放入数组之中
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,true); //
}

int main(){
    memset(dp,-1,sizeof(dp));
    ll l,r;
    while(~scanf("%lld%lld",&l,&r)){
        if(l+r==0) break;
        printf("%lld\n",solve(r)-solve(l-1));
    }
    return 0;
}

HDU 3652
题意:给出数字n,统计小于n是13的倍数且数位中含有13的数字
思路:
dp数组开三维
第一维: 数位
第二维state: state=0 表示没出现过13 state=1表示上一位是1 ,state=2表示前面出现过13
第三维mod: 前i位数字 mod 13 的余数
其他同模板。

AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[30];
ll dp[30][3][14];
ll dfs(int pos,int state,int mod,bool limit){
    if(pos==-1){
        if(state==2&&mod==0) return 1;
        return 0;
    }
    if(!limit&&dp[pos][state][mod]!=-1){
        return dp[pos][state][mod];
    }
    int up = limit?a[pos]:9;
    ll ans = 0;
    for(int i=0;i<=up;++i){
        int sta = 0;
        if(state==2||(state==1&&i==3)) sta = 2;
        else if(i==1) sta = 1;
        ans += dfs(pos-1,sta,(mod*10+i)%13,limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][state][mod]=ans);
}

ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,0,true);
}
int main(){
    memset(dp,-1,sizeof(dp));
    ll n;
    while(~scanf("%lld",&n)){
        printf("%lld\n",solve(n));
    }
    return 0;
}

POJ 3252

题意:求区间[a,b]中数字转化为2进制后 0的个数>=1的个数的数字的个数
思路:与前面略有不同:
1)a数组中存的的0和1
2)dfs中需要增加一个bool变量lead判断是否含有前导零,lead=0表示非前导零。
dp数组三维
第一维: 数位
第二维:存储zero的个数
第三维:存储非前导零one的个数

AC code:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
ll a[40];
ll dp[40][40][40];
ll dfs(int pos,int zero,int one,bool lead,bool limit){
    if(pos==-1){
        if(zero>=one) return 1;
        return 0;
    }
    if(!limit&&dp[pos][zero][one]!=-1){
        return dp[pos][zero][one];
    }
    ll ans = 0;
    int up = limit?a[pos]:1;
    for(int i=0;i<=up;++i){
        int z=zero,o=one;
        if(i) ++o,lead = 0;
        //非前导零
        if(!lead&&!i) ++z;
        ans += dfs(pos-1,z,o,lead,limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][zero][one]=ans);
}

ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++] = x%2;
        x >>= 1;
    }
    return dfs(pos-1,0,0,1,1);
}
int main(){
    ll a,b;
    memset(dp,-1,sizeof(dp));
    while(~scanf("%lld%lld",&a,&b)){
        if(a>b) swap(a,b);
        printf("%lld\n",solve(b)-solve(a-1));
    }
    return 0;
}

Windy数
题意:找出[l,r]之间不含前导零且相邻两个数字之差至少为2的正整数的数字的个数。注意:1~9这九个数字也是windy数字
思路: 这题和模板也差不多,首先dp数组显然是开二维,第一维:数位,第二维:记录差值。 判断添加abs(i-state)<2的都是应该跳过的,但应当注意state初值的传入.即改变state初值的传入,见代码注释。

AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a[40];
ll dp[40][12];

ll dfs(int pos,int state,bool limit){
    if(pos==-1){
        return 1;
    }
    if(!limit&&dp[pos][state]!=-1&&state>=0){
        return dp[pos][state];
    }
    ll ans = 0;
    int up = limit?a[pos]:9;
    for(int i=0;i<=up;++i){
        if(abs(i-state)<2) continue;
        int sta = i;
        if(i==0&&state==-10){
            sta = state;
        }
        ans += dfs(pos-1,sta,limit&&i==a[pos]);
    }
    if(!limit&&state>=0){
        dp[pos][state] = ans;
    }
    return ans;
}

ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++]=x%10;
        x /= 10;
    }
    return dfs(pos-1,-10,1);
    //-10不是必要只要0-state>2就可以 可以-7,-3
    //-10作为开始可以枚举出比次高位小的数字
    //1298->0298
    //不传入-10直接传入0会导致枚举缺少部分windy数字
}

int main(){
    memset(dp,-1,sizeof(dp));
    ll l,r;
    while(~scanf("%lld%lld",&l,&r)){
        printf("%lld\n",solve(r)-solve(l-1));
    }
    return 0;
}

Shu oj水题
题意:题意:给一个数n,求0~n内有多少个数满足其二进制形式不存在相邻的1,前导零不影响数字统计
思路:将数字n转转换成二进制形式,二维dp数组,第二维度表示其二进制数字是否存在相邻的1。

AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

ll a[70];
ll dp[70][2];
ll dfs(int pos,int state,bool limit){
    if(pos==-1){
        return 1;
    }
    if(!limit&&dp[pos][state]!=-1){
        return dp[pos][state];
    }
    ll ans= 0;
    int up = limit?a[pos]:1;
    for(int i=0;i<=up;++i){
        if(state&&i) continue; //前一位是1当前位也是1,跳过
        ans += dfs(pos-1,i,limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][state]=ans);
}

ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++] = x%2;
        x >>= 1;
    }
    return dfs(pos-1,0,1);
}

int main(){
    memset(dp,-1,sizeof(dp));
    ll n;
    while(~scanf("%lld",&n)){
        printf("%lld\n",solve(n));
    }
    return 0;
}

明7暗7加强版
题意:1≤M,N≤100000000, 给出M,N;找出M之后第N个数位中含有7或者是7倍数的数字。
思路:
数位dp是解决统计一类数位中符合某些规律的数字的方法, 那么我们可以用一次数位dp找出对于数字x,在0-x内其符合题意的数字有多少个。
这里我们利用反向统计的方法,即统计数位中不包括7,不是7倍数的数字,这样可以降低搜索的复杂度 f(x) = x - solve(x); 表示0-x中数位中含有7或是7倍数的数字的个数,这一部分就是裸的数位dp.
那么找M之后第N个数字怎么查找呢? 精确查找是不太可能的,但注意M,N<=1e8所以我们可以直接取一个上界r = 3e9计算[M,r]中查找符合f( r ) - f(M) ==N的数字的个数, 二分查找缩小区间[M,r]出满足题意且最小的数字x即为答案.

AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 3e9;
int a[12];
ll dp[12][8];
ll dfs(int pos,int state,bool limit){
    if(pos==-1){
        if(!state) return 0;
        return 1;
    }
    if(!limit&&dp[pos][state]!=-1){
        return dp[pos][state];
    }
    ll ans = 0;
    int up = limit?a[pos]:9;
    for(int i=0;i<=up;++i){
        if(i==7) continue;
        ans += dfs(pos-1,(state*10+i)%7,limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][state]=ans);
}

ll solve(int x){
    int pos=0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,1);
}

int main(){
    memset(dp,-1,sizeof(dp));
    ll M,N;
    while(~scanf("%lld%lld",&M,&N)){
        ll t = M - solve(M);
        ll l = M,r = INF,mid,ans,tmp;
        while(l<=r){
            mid = (l+r)>>1;
            tmp = mid - solve(mid) - t;
            if(tmp>=N){
                r = mid - 1;
                ans = mid;
            }
            else{
                l = mid + 1;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

HDU 3709

题意:
定义平衡数字:从该数字上某一位做轴,该位跳过不计算,分成左右两半,权值相等.
eg:4139 从第三位开始数字进行划分: 42 + 11 = 91.
eg:2317 第三位开始划分两半:2
2 + 31 = 71。
eg:0,1,2,3,4,5,6,7,8,9也是平衡数字.
给定[l,r]求出其中的平衡数字个数。

思路:枚举平衡中轴,假设中轴位mid,当前数位pos上数字为i,左右权值相等可以转换成: sigma(i*(pos-mid)) = 0; 三维dp数数组,并注意0的处理,因为每一次枚举中轴都会算一次0.

AC code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[20];
ll dp[20][20][1800];
ll dfs(int pos,int state,int sum,bool limit){
    if(pos==-1){
        return sum==0;
    }
    if(!limit&&dp[pos][state][sum]!=-1){
        return dp[pos][state][sum];
    }
    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0;i<=up;++i){
        ans += dfs(pos-1,state,sum+i*(pos-state),limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][state][sum]=ans);
}
ll solve(ll x){
    if(x==-1) return 0;
    int pos=0;
    while(x){
        a[pos++]=x%10;
        x/=10;
    }
    ll ans=0;
    for(int i=pos-1;i>=0;--i){
        ans += dfs(pos-1,i,0,1);
    }
    return (ans-pos+1);
}

int main(){
    int T;
    ll l,r;
    memset(dp,-1,sizeof(dp));
    cin>>T;
    while(T--){
        cin>>l>>r;
        cout<<solve(r)-solve(l-1)<<endl;
    }
    return 0;
}

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的个数。
    思路:
    求出sum = f(x),枚举数数字权值,构造当前数位的前缀和state,
    统计sum-state>=0的数字个数
    其他同模板
*/

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 10000+5;
int a[15];
ll dp[15][maxn];
int sum;

ll dfs(int pos,int state,bool limit){
    if(pos==-1){
        return state<=sum;
    }
    if(sum<state) return 0;
    if(!limit&&dp[pos][sum-state]!=-1){
        return dp[pos][sum-state];
    }
    ll ans = 0;
    int up = limit?a[pos]:9;
    for(int i=0;i<=up;++i){
        ans += dfs(pos-1,state+i*(1<<pos),limit&&i==a[pos]);
    }
    return limit?ans:(dp[pos][sum-state]=ans);
}


ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,1);
}

int getsum(int x){
    int a[20],pos=0;
    while(x){
        a[pos++] = x%10;
        x /= 10;
    }
    int ans = 0;
    for(int i=pos-1;i>=0;--i){
        ans = ans * 2 + a[i];
    }
    return ans;
}

int main(){
    memset(dp,-1,sizeof(dp));
    int T,kase=0,x,b;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&x,&b);
        sum = getsum(x);
        printf("Case #%d: %lld\n",++kase,solve(b));
    }
    return 0;
}

从计数转向求和:
HDU 4507
题意:
求区间[L,R]中满足下面三个条件数字的的平方和:
1)数字中不带有7
2)数字数位之和不能是7的倍数
3)该数字不能是7的倍数
思路:

数位dp显然,但是数位dp通常计,这里需要将计数转换为求和
    假设用cnt表示满足条件的小于X的数字个数,sum表示cnt个数的数字之和,sqsum表示cnt个数字平方之和
    可开一个结构体变量Node记录三个变量值,ans表示当前pos位满足条件,nex表示pos位之后满足条件的结构体变量
    1)cnt求解基础数位dp:
    开三维Node dp数组,第一维:pos,第二维:sta1,前pos个数字之和mod7的余数
    第三维:sta2,前pos位数字组成的数字mod7的余数 (sta2*10+i)%mod;
    2)sum解决:
    ans.sum = (ans.sum + i*10^pos*nex.cnt+nex.sum) i表示第pos位填充的数字
    i*10^pos*nex.cnt满足前pos条件下数字有nex.cnt个,得该位填充数字对求和的总贡献
    3)sqsum解决:
    假设当前状态下表示的某个数字(i*10^pos+xj) x是pos位之后填充的完整数字
    当前状态的某个数字 sqsum = (i*10^pos+xj)^2 = (i*10^pos+2*10^pos*xj + xj^2;
    当前状态下所有满足条件数字(共计nex.cnt个)的平方和:
    ans.sqsum = ans.sqsum + (i*10^pos)*nex.cnt + 2*i^10^pos*sigma(xj)+sigma(xj^2)
    sigma(xj) = nex.sum;
    sigma(xj^2) = nex.sqsum;
    ans.sqsum = ans.sqsum + (i*10^pos)*nex.cnt + 2*i^10^pos*nex.sum+nex.sqsum;
    到此思路完成
    最后切记取模!!!乘法取模,疯狂取模,(solve(r)-solve(l-1)+mod)%mod!!!!

AC code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
struct Node{
    ll cnt,sum,sqsum;
    Node(){};
    Node(ll c,ll s,ll sq):cnt(c),sum(s),sqsum(sq){};
};
int a[20];
ll p[20]; //10^pos
Node dp[20][8][8];

void init(){
    p[0] = 1;
    for(int i=1;i<20;++i){
        p[i] = p[i-1]*10LL%mod;
    }
    for(int i=0;i<20;++i){
        for(int j=0;j<8;++j){
            for(int k=0;k<8;++k){
                dp[i][j][k].cnt = -1;
            }
        }
    }
}
Node dfs(int pos,int sta1,int sta2,bool limit){
    if(pos==-1){
        return Node(sta1!=0&&sta2!=0,0,0);
    }
    if(!limit&&dp[pos][sta1][sta2].cnt!=-1){
        return dp[pos][sta1][sta2];
    }
    int up = limit?a[pos]:9;
    Node ans(0,0,0),nex(0,0,0);
    for(int i=0;i<=up;++i){
        if(i==7) continue;
        nex = dfs(pos-1,(sta1+i)%7,(sta2*10+i)%7,limit&&i==a[pos]);
        ll A = i*p[pos]%mod;
        ans.cnt = (ans.cnt + nex.cnt)%mod;
        ans.sum = (ans.sum+(A*nex.cnt)%mod+nex.sum)%mod;
        ans.sqsum = (ans.sqsum+(A*A%mod*nex.cnt%mod+2*A*nex.sum%mod+nex.sqsum%mod)%mod)%mod;
    }
    return limit?ans:(dp[pos][sta1][sta2]=ans);
}

ll solve(ll x){
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,0,1).sqsum%mod;
}

int main(){
    init();
    ll l,r;
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%lld%lld",&l,&r);
        printf("%lld\n",((solve(r)-solve(l-1))+mod)%mod);
    }
    return 0;
}

暂且写到这里,以后碰到新的继续放入,QAQ还是太弱了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值