前天晚上,一位研三的学长突然跑到我们宿舍,问我们一道微软笔试题。给你一个整数n,求出1到n这个区间范围内包含数字0的个数,例如当n=10的时候就只有10包含0,输出1,n=90就输出9。唯一的要求是此题不能用遍历来实现,时间负责度要比O(n)小,要是遍历显然谁都会做。
初看此题,似乎能找到规律,应该是排列组合的思想,下面是我认识的一个学数学的同学提供的思路:
用数学方法看起来应该能够解决,不过并没有尝试。在问了一个ACM大牛后,得到了一个名词“数位DP”,并且发现其实有很多与此类似的题目,都可以用数位DP的方法求解。
下面先给出数位DP的背景:
为了降低时间复杂度,可以借鉴传统DP中状态转换,打表这些思路,得到了数位DP:
F(A,B) = F(B,0)-F(A-1,0)
暴力+存储 = 记忆化搜索
针对上面几种类型的问题,数位DP解决方案如下:(具体可以看http://www.cppblog.com/Yuan/archive/2011/07/15/139299.html)
在这几种类型中,包含49的与微软这道题最为相近,不过要注意的是运算过程中需要把前缀0的情况剔除,最终代码如下:
#include<iostream> #include<algorithm> #include<cstdlib> #include<cstring> using namespace std; typedef long long ll; #define mem(a,b) memset(a,b,sizeof(a)) const int L = 20, P = 1e9+7; struct RES { ll all, sum, cnt; RES() {} RES(int i,int j,int k):all(i),sum(j),cnt(k) {} } dp[L]; ll chkmod(ll x,ll p) { return (x%p+p)%p; } int d[L], n; RES dfs(int pos, int UP) { if(pos<0) { return RES(0,0,1); } if(!UP && ~dp[pos].all) { return dp[pos]; } RES ret(0,0,0); int up=UP?d[pos]:9; ret.all += dfs(pos-1, UP&&up==0).all; ret.all %= P; for(int i=1;i<=up;i++) { int nUP = UP&&i==up; for(int j=pos-1;j>=-1;j--) { ll tmp = dfs(j, nUP).sum + dfs(j, nUP).cnt * (pos - 1 - j); tmp %= P; ret.all += tmp; ret.all %= P; ret.sum += tmp; ret.sum %= P; ret.cnt += dfs(j, nUP).cnt; ret.cnt %= P; nUP = nUP && d[j]==0; // !!! } } if(!UP) { dp[pos] = ret; } return ret; } ll cal(ll x) { n=0; while(x) { d[n++]=x%10; x/=10; } return dfs(n-1,1).all; } int main() { mem(dp,-1); ll n; while(cin>>n) { cout<<cal(n)<<endl; } return 0; }