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 
#include 
#include 
#include 
#include <set>
#include 
#include 
#include <string>
#include 
#include 
#include 
#include 
#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 pairint> 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),逐位相减,如果在dp转移到最后一位差值仍大于等于零则说明f(i)<=f(a),对于不同的a,如果减到某一位后他们的状态:与目标值的差值相同,则接下来的位数中满足相减完毕后结果大于等于0的方案数也相同 ,dp是可以复用的

#include 
#include 
#include 
#include 
#include <set>
#include 
#include 
#include <string>
#include 
#include 
#include 
#include 
#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 pairint> 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 
#include 
#include 
#include 
#include <set>
#include 
#include 
#include <string>
#include 
#include 
#include 
#include 
#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 pairint> 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<
     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 
#include 
#include 
#include 
#include <set>
#include 
#include 
#include <string>
#include 
#include 
#include 
//#include 
#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 pairint> 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 
#include 
#include 
#include 
#include <set>
#include 
#include 
#include <string>
#include 
#include 
#include 
#include 
#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 pairint> 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了

你可能感兴趣的:(11 .3 数位dp)