11 .3 数位dp

数位dp是以数位上的关系为转移关系而进行的一种计数dp,题目基本类型是给定区间[l ,r] ,求l到r之间满足要求的数字的个数 .

dp状态的转移方式通常是用 递归+记忆化搜索 ,转移顺序一般是由高数位转移到底数位 ,其中就是记忆化搜索保证了数位dp的高效率

例如千位2到百位转移要枚举0,1,2,3 ...(2000,2100,2200,2300...) ,而千位3也是同样的(3000,3100,3200,3300...),其进行的都是对三位数000~999的统计,所以低位统计过程只用进行一次就可将结果应用于所有高位状态上,减少了重复过程的进行.

结果的输出形式是 0~r 之间的dp 与 0~l之间的dp 进行相减 来求 l到r 之间的 dp.

        printf("%lld\n",solve(r)-solve(l-1));

值得注意的点是边界 l和r 不能进行记忆化搜索 ,比如 dp[2][sta] 记录的是 000~999(三位数) 中满足条件的数字的个数 ,而对于l = 2250 ,其在2000之后的三位数只有 100~250 ,所以这时候如果直接记忆化返回 dp[2][sta] 就会出现多记.

有的题目对前导零有要求,有的没有,做的时候随机应变。

例题:

HDU 2089 不要62

题解:转移状态很清晰明了的题目,主要通过此题了解 递归+记忆化的转移方式,对题目要求如何在dfs函数中进行处理 ,lim边界标记的使用和传递的方式.

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <map>
#include <unordered_map>
#define mem( a ,x ) memset( a , x ,sizeof(a) )
#define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
#define lson l ,mid ,pos<<1
#define rson mid+1 ,r ,pos<<1|1
#define Fi first
#define Se second

using namespace std;
typedef long long ll ;
typedef pair<int ,int> pii;
typedef pair<ll ,int> pli;
const ll inf = 0x3f3f3f3f;
const int N = 1e5+5;
const ll mod = 1e9+7;

ll dp[10][2];
int a[10] ,pos;

ll dfs( int pos ,int pre ,int sta ,int lim ){
    //pos 当前数位 , pre 前一位的数字 ,sta 当前状态:前一位是否是6 ,lim 是否是在需要特判的边界上
   if( pos==-1 )return 1;
   if( !lim && dp[pos][sta]!=-1 )return dp[pos][sta];
   //在边界则不能进行记忆化
   int tmp = lim ? a[pos] : 9;
   ll now = 0;
   rep( i ,0 ,tmp ){
       if( i==4 )continue;
       //数位为4则不进行计数
       if( pre==6 && i==2 )continue;
       //前一位为6则不进行计数
       now += dfs(pos-1 ,i ,i==6 ,lim&&i==tmp);
       //
   }
   if( !lim )dp[pos][sta] = now;
   //在边界则不能进行记忆化
   return now;
}

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

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

常用优化:

1.就算面对不同询问,数位的dp状态也往往是相同的,因此在约束条件普适于所有数字的条件下不用每次都用mem对dp进行初始化

如果面对不同询问,在约束条件下产生的状态不普适的条件下(如 一个数是它自己数位和的倍数),可以将dp增加一维dp[pos][state][limit],或者直接每次都初始化dp

2.状态的表示方法不同也会影响dp的适用范围,一种常见的状态表示是将和式的状态表示为 当前数位和 与 目标值 所需要拼凑的差值

例如:HDU 4734

F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,Ai是十进制数位,给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。

如果正面思考,用数位和sum作为状态,逐位拼凑到a,则状态dp[pos][sum]无法复用,因为随着a的变化,sum<f(a)的数字个数也是不同的,状态增加一维表示a后才能覆盖所有状态,所需要空间是dp[10][4600][4600]

不如反面思考,将初始与目标值的差值作为状态,初始状态为f(a),逐位相减,如果在dp转移到最后一位差值仍大于等于零则说明f(i)<=f(a),对于不同的a,如果减到某一位后他们的状态:与目标值的差值相同,则接下来的位数中满足相减完毕后结果大于等于0的方案数也相同 ,dp是可以复用的

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <map>
#include <unordered_map>
#define mem( a ,x ) memset( a , x ,sizeof(a) )
#define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
#define lson l ,mid ,pos<<1
#define rson mid+1 ,r ,pos<<1|1
#define Fi first
#define Se second

using namespace std;
typedef long long ll ;
typedef pair<int ,int> pii;
typedef pair<ll ,int> pli;
const ll inf = 0x3f3f3f3f;
const int N = 1e5+5;
const ll mod = 1e9+7;

ll dp[10][10500];
int num[10] ,cnt;

void div( ll x ){
    cnt = 0;
    while( x ){
        num[cnt++] = x%10;
        x /= 10;
    }
}

int f( ll x ){
    int tmp = 0 ,ans = 0;
    while( x ){
        ans += (x%10)*(1<<tmp);
        tmp++; x/=10;
    }
    return ans;
}

int dfs( int pos ,int sta ,bool lim ){
    //sta 是与目标值的所需差值
    if( sta < 0)return 0;
    if( pos < 0 )return sta >= 0;
    if( !lim && dp[pos][sta] != -1 )return dp[pos][sta];

    int ans = 0 ,sum = sta;
    int tmp = lim ? num[pos]:9;
    rep( i ,0 ,tmp ){
       sta = sum - i*(1<<pos);
       if( sta < 0)break;
       ans += dfs( pos-1 ,sta ,lim && i==tmp );
    }

    if( !lim )dp[pos][sum] = ans;
    return ans;
}

int main( ){
     int t ,fa ,ks = 0;
     ll a ,b;
     scanf("%d" ,&t);
     mem( dp ,-1);
     while( t-- ){
         scanf("%lld %lld" ,&a ,&b);
         fa = f(a);
         div(b);
         printf("Case #%d: %d\n", ++ks ,dfs(cnt-1, fa ,1) );
     }

     return 0;
}

其他题目:

HDU 3709 这题就是要枚举中轴,然后数位dp

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <map>
#include <unordered_map>
#define mem( a ,x ) memset( a , x ,sizeof(a) )
#define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
#define lson l ,mid ,pos<<1
#define rson mid+1 ,r ,pos<<1|1
#define Fi first
#define Se second

using namespace std;
typedef long long ll ;
typedef pair<int ,int> pii;
typedef pair<ll ,int> pli;
const ll inf = 0x3f3f3f3f;
const int N = 1e5+5;
const ll mod = 1e9+7;

int num[20] ,cnt;
ll dp[20][20][2000];

void div( ll x ){
    cnt = 0;
    while(x){
        num[cnt++] = x%10;
        x /= 10;
    }
}

ll dfs( int pos ,ll sum, int sta ,bool lim ){
     if( pos < 0 || sum < 0 )return sum == 0;
     if( !lim && dp[pos][sta][sum] != -1 )return dp[pos][sta][sum];

     int tmp = lim ? num[pos] : 9;
     ll ans = 0;
     rep( i ,0 ,tmp ){
         if( pos >= sta )ans += dfs( pos-1 ,sum + i*(pos-sta) ,sta ,lim&&i==tmp );
         if( pos < sta )ans += dfs( pos-1 ,sum - i*(sta-pos) ,sta ,lim&&i==tmp );
        }
     if( !lim )dp[pos][sta][sum] = ans;
     //cout<<pos<<" "<<sum<<" "<<sta<<" "<<ans<<endl;
     return ans;
}

ll sol( ll x ){
   div(x);
   ll ans = 0;
   rep( i ,0 ,cnt-1 ){
       ans += dfs( cnt-1 ,0 ,i ,1 );
   }
   //注意枚举中轴过程中0的重复计数
   return x >= 0 ? ans - cnt + 1 : 0;
}

int main( ){
     mem( dp ,-1 );
     int t;
     ll s ,e;
     scanf("%d" ,&t);
     while( t-- ){
         scanf("%lld%lld" ,&s ,&e );
         printf("%lld\n" ,sol(e) - sol(s-1) );
     }
     return 0;
}

这题需要注意的是在枚举中轴的过程中,对于数字0,无论中轴在哪一位都能满足,因此最后要减去0的重复计数

HYSBZ - 1799给出a,b,求出[a,b]中各位数字之和能整除原数的数的个数。

数位和相较于原来的a,b较小,最大只有9*18 == 162 ,也就是说在a~b之间有很多数的数位和是相同的,可以直接枚举除数mod

状态 dp[pos][val][mod],其中val是枚举到某一位的余数

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <map>
//#include <unordered_map>
#define mem( a ,x ) memset( a , x ,sizeof(a) )
#define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
#define lson l ,mid ,pos<<1
#define rson mid+1 ,r ,pos<<1|1
#define Fi first
#define Se second

using namespace std;
typedef long long ll ;
typedef pair<int ,int> pii;
typedef pair<ll ,int> pli;
const ll inf = 0x3f3f3f3f;
const int N = 1e5+5;
//const ll mod = 1e9+7;

ll dp[20][200][200];
int num[20] ,cnt;

ll dfs( int pos ,int sum ,int val ,int mod ,bool lim ){
   if( sum - 9*(pos+1) > 0 )return 0;
   if( pos == -1 )return sum == 0 && val == 0;
   if( !lim && dp[pos][sum][val] != -1 )return dp[pos][sum][val];
   int up = lim ? num[pos] : 9;
   ll ans = 0;
   rep( i ,0 ,up ){
       if( sum - i < 0 )break;
       ans += dfs(pos-1 ,sum-i ,(val*10+i)%mod ,mod ,lim&&i==up );
   }
   if( !lim )dp[pos][sum][val] = ans;
   return ans;
}

void div( ll x ){
    cnt = 0;
    while( x ){
        num[cnt++] = x%10;
        x /= 10;
    }
}

ll solve( ll x ){
    div(x);
    ll ans = 0;

    rep( i ,1 ,cnt*9 ){
        mem( dp ,-1 );
        ans += dfs( cnt-1 ,i ,0 ,i ,1 );
    }

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

进阶:dp所求不是a~b之间数字个数的情况,例如求a~b之间满足条件数字的和,求a~b之间满足条件数字的平方和

处理方法是在转移过程中考虑各位为dp结果产生的贡献,写出dp的转移方程

和 : sum[pos] = sum[pos-1][0~9] + cnt[pos-1][0~9]*i*10^pos

平方和:i为当前位上的数 ,假设b为低位上余下的数值,则将表达式展开(i*10^pos + b)^2 = (i*10^pos)^2 + 2*b*i*10^pos + b^2

故转移方程 :

sum_sq[pos] = cnt[pos-1][0~9]*(i*10^pos)^2 + 2*i*10^pos * sum[pos-1][0~9]  + sum_sq[pos-1][0~9];
     sum[pos] = sum[pos-1][0~9] + cnt[pos-1][0~9]*i*10^pos

例题:HDU 4507

简单的约束条件,但所求结果不是计数而是平方和

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <map>
#include <unordered_map>
#define mem( a ,x ) memset( a , x ,sizeof(a) )
#define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
#define lson l ,mid ,pos<<1
#define rson mid+1 ,r ,pos<<1|1
#define Fi first
#define Se second

using namespace std;
typedef long long ll ;
typedef pair<int ,int> pii;
typedef pair<ll ,int> pli;
const ll inf = 0x3f3f3f3f;
const int N = 1e5+5;
const ll mod = 1e9+7;

ll ten[20];
ll dp[20][10][10]; // dp[pos][digsum][num]
ll sum[20][10][10] ,sum_sq[20][10][10];
int num[20] ,cnt;
ll sq (ll x){ return (x%mod)*(x%mod)%mod; }

void init( ){
   ten[0] = 1;
   rep( i ,1 ,18 )ten[i] = 10ll*ten[i-1]%mod;
   mem( dp ,-1 );
}

ll dfs( int pos ,int sum7 ,int dig7 ,ll &s,ll &s_sq ,bool lim ){
   if( pos < 0 ){
     return (sum7) && (dig7);
   }

   if( !lim && dp[pos][sum7][dig7]!=-1 ){
     s = ( sum[pos][sum7][dig7])%mod;
     s_sq = ( sum_sq[pos][sum7][dig7])%mod;
     return dp[pos][sum7][dig7];
   }

   ll ans=0;
   ll up = lim ? num[pos] : 9;
   for( ll i = 0; i <= up; i++ ){
     if( i==7 )continue;
     ll cnt ;
     ll ans_s=0 ,ans_sq=0;
     cnt = dfs( pos-1 ,(sum7*10+i)%7 ,(dig7+i)%7 ,ans_s ,ans_sq , lim&&i==up )%mod;
     s_sq = (s_sq + cnt*sq(i*ten[pos])%mod + 2*i*ten[pos]%mod*ans_s%mod + ans_sq%mod)%mod;
     s = (s + cnt%mod * (i*ten[pos])%mod + ans_s%mod )%mod;
     ans = (ans + cnt)%mod;
   }

   if( !lim ){
        dp[pos][sum7][dig7] = ans%mod;
        sum[pos][sum7][dig7] = s%mod;
        sum_sq[pos][sum7][dig7] = s_sq%mod;
   }
   return ans%mod;
}

void div( ll x ){
    cnt = 0;
    while( x ){
        num[cnt++] = x%10;
        x /= 10;
    }
}

ll solve( ll x ){
    div( x );
    ll ans_sq = 0 ,ans_s = 0;
    dfs( cnt-1 ,0 ,0 ,ans_s ,ans_sq ,1 );
    return ans_sq%mod;
}

int main( ){
     int T;
     ll l ,r;
     //freopen( "Hdu 4507.in" ,"r" ,stdin );
     //freopen( "my - hdu 4507.txt" ,"w" ,stdout );

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

方法就是考虑每一位的贡献,转移方程就是上面推的

复制下别人的题解:

关于数字7的限制其实不难实现,难的是要求平方和,考虑2XX这个数,也就是百位为2的数,它满足限制条件的平方和是多少?
假设满足条件的数有234,245,266,那么 234^2 + 245^2 + 266^2 = (200 + 34)^2 + (200 + 45)^2 + (200 + 66)^2
= 3*200^2 + 2*200*(34+45+66) + (34^2 + 35^2 + 66^2),因此在枚举到2的时候,表达式里只有3,(34 + 45 + 66),(34^2 + 35^2 + 66^2)不知道,
因此我们可以定义dfs1(i)为求平方和,dfs2(i)为求和,dfs3(i)为求有多少个满足条件的数(也就是上述的3)。

另外注意算的时候每一步都取模,不要爆longlong了

原文地址:https://www.cnblogs.com/-ifrush/p/11787634.html

时间: 2024-08-10 11:53:16

11 .3 数位dp的相关文章

51Nod 1009 数字1的个数 | 数位DP

题意: 小于等于n的所有数中1的出现次数 分析: 数位DP 预处理dp[i][j]存 从1~以j开头的i位数中有几个1,那么转移方程为: if(j == 1) dp[i][j] = dp[i-1][9]*2+pow(10,i-1);else dp[i][j] = dp[i-1][9]+dp[i][j-1]; 然后注意下对于每个询问统计的时候如果当前位为1需要额外加上他后面所有位数的个数,就是n%pow(10,i-1); 这样总复杂度log(n)*10 #include <bits/stdc++.

51nod1043(数位dp)

题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1043 题意:中文题诶- 思路:数位dp 我们用dp[i][j]来存储长度为2*i且一半和为j的所有情况(包括前导0的情况),为了方便我们现在只讨论其一半的和的情况,因为如果包括前导0的话其两边的情况是一样的: 我们假设再长度为i-1的数字最前面加1位数字k,0<=k<=9(这位数字加在哪里并不影响答案,因为我们在计算i-1长度的时候已经计算了所有组合情况,

数位dp

1.[hdu3709]Balanced Number 1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<string> 5 #include<cstdlib> 6 #include<algorithm> 7 #include<ctime> 8 #include<cmath> 9 #include<queue>

【HDU 3652】 B-number (数位DP)

B-number Problem Description A wqb-number, or B-number for short, is a non-negative integer whose decimal form contains the sub- string "13" and can be divided by 13. For example, 130 and 2613 are wqb-numbers, but 143 and 2639 are not. Your task

hdu 5898 odd-even number 数位DP

odd-even number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Submission(s): 716    Accepted Submission(s): 385 Problem Description For a number,if the length of continuous odd digits is even and the length

LightOJ1068 Investigation(数位DP)

这题要求区间有多少个模K且各位数之和模K都等于0的数字. 注意到[1,231]这些数最大的各位数之和不会超过90左右,而如果K大于90那么模K的结果肯定不是0,因此K大于90就没有解. 考虑到数据规模,数据组数,这题状态这么表示: dp[i][j][k]:位数为i模K结果为j且各位数之和模K结果为k的数字个数 然后就是转移方程,最后就是统计.. 统计部分好棘手...半乱搞下AC的..还是对数位DP的这一部分太不熟悉了. 1 #include<cstdio> 2 #include<cstr

hdu 4734 数位dp

http://acm.hdu.edu.cn/showproblem.php?pid=4734 Problem Description For a decimal number x with n digits (AnAn-1An-2 ... A2A1), we define its weight as F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1. Now you are given two numbers A and B, plea

HDU 4389 X mod f(x) (数位DP)

题目链接  HDU4389 题意  给出T个区间[L, R],统计L到R中有多少个满足条件的数. 限制条件为该数能被这个数的各位数字之和整除. 数据范围$1 <= L <= R <= 10^{9}$ 考虑数位DP 注意到f(x)最大为81,所以对1-81每一个和做一遍数位DP即可. f[pos][mod][sum][x] 表示当前处理到第pos位,当前的数位和对x取模的结果,当前的数位和,以及当前正在求的x = f(x) #include <bits/stdc++.h> us

bzoj1026: [SCOI2009]windy数 数位dp

题目: http://www.lydsy.com/JudgeOnline/problem.php?id=1026 题意: Description windy定义了一种windy数.不含前导零且相邻两个数字之差至少为2的正整数被称为windy数. windy想知道, 在A和B之间,包括A和B,总共有多少个windy数? Input 包含两个整数,A B. Output 一个整数 思路: 数位dp,记忆化搜索. 1 #include <bits/stdc++.h> 2 3 using namesp