一、题意
给定一个区间[a, b](注意输入的时候可能a > b,所以,在数据输入后,要先比较a和b,如果a > b,交换a和b的值),统计这个区间里面,数位上有多少个0、多少个1、……、多少个9。
二、思路
第一种:数位DP。dfs函数的参数列表为:
int pos:当前处理的数位所在的位置;
int val:当前统计的数值(0——9);
int amt:从最高位开始到当前位置的前一个位置(因为当前位置还没开始统计),val的数量。
bool lead:是否有前导0。因为这题要统计0的个数,前导0对结果是有影响的,所以需要做限制。
bool limit:当前数位的最大值是否受前一位限制。
dp[15][10][15]:记忆化当前从当前状态出发往下搜索得到的结果。dp[pos][val][amt]表示:从 ”从最高位到当前位pos的前一个位置为止,待统计数值为val,已统计的val的个数为amt(是从最高位到当前位置pos的前一个位置已统计的val的个数)“ 的这种状态开始往下搜索所能得到的结果(这个结果就是从pos位置开始搜索到最后一个位置所能得到的val的个数。注意,是搜索,而不是对某一个特定的数)。没明白的再重新看一遍这段话,再次强调,我们的dp数组、amt等变量做的统计都是从最高位到当前位的前一个位置之间的。因为当前位置pos的这个数位上的值还没有开始统计。
关于上述的dp数组的设计,我之前看过别人的一篇博客,还和博主讨论一番,当时也是迷迷糊糊似懂非懂的。今天再次做这个题,终于明白了为什么这样设计是正确的。当limit == false && lead == false时(这是dp记忆化的前提),不管你前面(从最高位到当前位的前一个位置之间)的amt个val值怎么排列,只要你到当前位置pos时limit == false而且lead
== false,那么,从当前位置pos一直到最低位之间的数值都可以进行任意的排列组合,自然搜索得到的结果的相同的,所以可以用记忆化最开始第一次搜索得到的结果。比如:当前pos在最高位的后三个位置,统计的数值是0,不管你的前缀是110……还是101……,只要你的limit == false而且lead == false,那后面的所有数位就可以任意排列组合,那么,当我第一次已110……前缀往下搜索得到结果以后,使用dp数组记忆化一下,再当我用101……前缀去往下搜索时,得到的结果肯定和以110……前缀往下搜索得到的结果相同,所以可以直接去dp数组里面取出结果。
第二种:网上最为流行的,按权来统计数位。
三、源代码
第一种:
#include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #include<cctype> #include<algorithm> #include<utility> #include<vector> #include<set> #include<map> #include<stack> #include<queue> using namespace std; typedef __int64 LL; int digit[15], dn; LL dp[15][10][15], ans[2][10]; LL dfs(int pos, int val, int amt, bool lead, bool limit) { if(pos == 0)return amt; if(!limit && !lead && dp[pos][val][amt] != -1)return dp[pos][val][amt]; int top = limit ? digit[pos] : 9, t = 0; LL ans = 0; for(int i = 0; i <= top; ++i) { if(val != i)t = amt; else { if(val == 0) { if(lead)t = 0; else t = amt + 1; } else t = amt + 1; } ans += dfs(pos - 1, val, t, lead && i == 0, limit && i == top); } if(!limit && !lead)dp[pos][val][amt] = ans; return ans; } void solve(LL x, int idx) { if(x == 0)return; memset(digit, 0, sizeof(digit)); dn = 0; for(; x > 0; x /= 10) digit[++dn] = x % 10; for(int i = 0; i < 10; ++i) ans[idx][i] = dfs(dn, i, 0, true, true); } int main() { #ifndef ONLINE_JUDGE freopen("input.txt", "r", stdin); #endif // ONLINE_JUDGE memset(dp, -1, sizeof(dp)); LL a, b; while(scanf("%I64d%I64d", &a, &b), a != 0 || b != 0) { if(a > b)swap(a, b); memset(ans, 0, sizeof(ans)); solve(a - 1, 0), solve(b, 1); for(int i = 0; i < 10; ++i)printf("%I64d%c", ans[1][i] - ans[0][i], i < 9 ? ' ' : '\n'); } return 0; }
第二种思路的代码暂时先不贴上来。
四、对数位dp使用dfs方式的思考与总结
使用dfs方式搜索结果,实际上就是一棵十叉树,每一条从根节点到叶子节点的路径都是一个具体的数值。每一层节点都有一个层号,也就是我们的pos。当dfs的参数给定时,可以确定这棵树上的一个具体的节点,如果我们已经使用dp数组记忆了从这个节点开始往下搜索能得到的结果,那么,下次我们再到达这个节点时,直接返回dp数组的结果就好了。在最坏的情况下,这种记忆化的dfs会产生一棵”大十叉树“,也就是说,对于每一个给定的数值,都能在这棵树上找到一条路径。那么在这种情况下,当最顶上的根节点数值又很大时(不大的话直接用循环暴力解决得了),容易发生栈溢出以及超时(因为和暴力循环没多大区别)。所以,为了防止栈溢出,就必须做记忆化工作。