数位DP

数位DP,常常用来求解对于给定区间中符合约束条件的数的个数。

例:对于区间[1,100],试求其中不含两个连续1的数个数。

首先想到的是在该区间内枚举,然后判断记录,很明显,这样在数据很大或者约束更复杂的情况下,一定会超时。

那么,下面介绍DP的方法:

我们可以按照位来枚举区间两端点的值,在枚举的过程中要保证当前产生的数不超过区间的右端点,最后再用前缀和的思想维护一下即可。例如100的话,我们从百位开始枚举(一般题目中都是由高位向低位),在不超过区间右端点的情况下,有两种选法,分别是1和0。假设我们枚举了0,那么接下来的十位有9种选法;而如果我们选了1,那么十位就只有0可以选了。由十位到个位也是相同的道理。由于是按照位来枚举,处理各种约束便十分得简单了。

枚举完100的所有情况后,我们等于是得到了[0,100]这个区间的答案,显然要想求[1,100],只需再减去[0,1-1]的答案即可,即用前缀和的思想来维护。

模板:

int dfs(int pos, bool limit, bool lead) {//pos为当前搜索到的位置或位,limit为当前有无限制,lead为有无前导零,除此之外,还可以根据需要添加其它限制 
	if (pos == 0) return 1;//搜索完毕,(一般来说)返回1,用来表示已经找到了一个合法的数 
	if (!limit && !lead && dp[pos][][]) return dp[pos][][];//dp数组还可以添加其它的状态
	//记忆化,这里的limit和lead都是一些防止发生状态冲突的量,与下面相对应
	int i, sum = 0, up = limit?a[pos]:9;//确定枚举上界 
	for (i = 0; i <= up; i++)
		sum += dfs(pos-1, limit&&i==a[pos], lead&&i==0);//枚举每一种情况,统计到答案里 
	if (!limit && !lead) dp[pos][][] = sum;//对应上面的记忆化,即我们只记忆无限制和无前导零的状态 
	return sum;
}

a数组的初始化:

int solve(int x) {
	memset(a, 0, sizeof(a));
	memset(dp, 0, sizeof(dp));
	int cnt = 0;
	while (x) {//分离每一位
		a[++cnt] = x%10;
		x /= 10;
	}
	return dfs(cnt, true, true);//可添加其它限制 
}

主函数:

int main() {
	int l, r;
	cin >> l >> r; 
	cout << solve(r)-solve(l-1);//前缀和
	return 0;
}

状态冲突:即我们已经记忆过的状态与当前的状态发生冲突。也就是对于一个dp[pos][][],我们已经记忆过一次了,然后在搜索的时候又再次访问到了,由于我们的状态有限,所以难免会发生本次dp[pos][][]和以往的不同,但这次我们却返回了以往的dp[pos][][],所以会导致答案出现错误。

解决的方法:可以少记忆一点,在牺牲时间的前提下获取正确率;或者从数组中再多开几维,即增加状态,牺牲空间换取时间和正确率。

例题:

https://www.luogu.org/problemnew/show/P3413#sub

http://acm.hdu.edu.cn/showproblem.php?pid=2089

想深入了解的读者可以看下这篇博客:

https://blog.csdn.net/wust_zzwh/article/details/52100392

 

 

 

你可能感兴趣的:(算法竞赛)