标签:搜索 限制 接下来 说明 sum void 一个 记录 mem
数位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了
标签:搜索 限制 接下来 说明 sum void 一个 记录 mem
原文地址:https://www.cnblogs.com/-ifrush/p/11787634.html