数位DP--由一道微软笔试题引起

前天晚上,一位研三的学长突然跑到我们宿舍,问我们一道微软笔试题。给你一个整数n,求出1到n这个区间范围内包含数字0的个数,例如当n=10的时候就只有10包含0,输出1,n=90就输出9。唯一的要求是此题不能用遍历来实现,时间负责度要比O(n)小,要是遍历显然谁都会做。

初看此题,似乎能找到规律,应该是排列组合的思想,下面是我认识的一个学数学的同学提供的思路:




用数学方法看起来应该能够解决,不过并没有尝试。在问了一个ACM大牛后,得到了一个名词“数位DP”,并且发现其实有很多与此类似的题目,都可以用数位DP的方法求解。

下面先给出数位DP的背景:

•在给定区间[A,B]内,找满足要求的数。
•要求一般和数大小无关,而与数的组成有关
•例如,递增的,1234, 2579…
•         双峰的,19280,26193…
•         含49的,49, 149, 1492… 
•         整除13的,26, 39…
•麻烦在于,规模大,位数> 100,不能枚举。
•区间往往不是整百整千,边界问题
•注意
–记忆化搜索思路清晰
–开适当空间
–寻找合适的状态,简化计算量

为了降低时间复杂度,可以借鉴传统DP中状态转换,打表这些思路,得到了数位DP:

F(A,B) = F(B,0)-F(A-1,0)

暴力+存储 = 记忆化搜索

•暴力:
•暴力枚举每一位(0..9),注意区间边界;与符号的匹配。

•dfs(i,j,k,flag)
•枚举第i位的数,匹配str[j],前一位是k,是否达到上限(flag=true,false)
•达到了上限则只能枚举0..num[i],否则可以枚举0..9
•存储
•dfs(i,j,k,flag)
•设状态与递归参数一致f[i][j][k][flag],表示当枚举到第i位的数,匹配str[j],前一位是k,是否达到上限(flag=true,false)时,满足要求的数字个数。
•dfs的过程,相当于在填充f,假设f的空间O(100*10*10*2),则dfs的时间O(20000)

针对上面几种类型的问题,数位DP解决方案如下:(具体可以看http://www.cppblog.com/Yuan/archive/2011/07/15/139299.html)

•整除13
•dfs(i, m, flag)
•枚举第i位数,前面枚举出的数模13的余数m,是否到达上限flag
•整除自身各位数CF55D
•dfs(i, m, l, flag)
•枚举第i位数,前面枚举出的数模LCM(0..9),LCM(前面枚举出的数),是否达到上限
•包含”49”
•dfs(i, k, find, flag)
•枚举第i位数,前一位是k,是否已包含”49”(find),是否达到上限
•分类讨论:前一位是否为4,当前是否已包含“49”

在这几种类型中,包含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;
}


你可能感兴趣的:(数位DP--由一道微软笔试题引起)