【学时总结】 ◆学时·IV◆ 数位DP

【学时·IV】 数位DP


■基本策略■

说白了就是超时和不超时的区别 ;)

有一些特别的题与数位有关,但是用一般的枚举算法会超时。这时候就有人提出了——我们可以用动态规划!通过数字前一位和后一位之间的关系,逐渐推导出所有数位上的值作为初始化(也有些不是),实现大部分计数问题的高效解决。

主要题型大概就是求 Min~Max 之间满足条件 E() 的数的个数,这里使用了前缀和的思想,即 F[ij]=F[0 j]-F[0~(i-1)]。一般是将 F[] 初始化,但是针对某些特别题型,比如 E() 针对不同的数据不同,这时候需要对每一个数据单独求解。


■一般的计数问题■

在十进制里做DP ♪(´▽`)

◆入门练手◆ 不要62

这是一道基本上称得上“版题”的基础题。先给出m,n,也就是 Min,Max 。这里的 E(n) 则是 n不含4或62。
先给出一个最基本的结论:

若A < B,则A、B的十进制表示 {an...a2a1a0}、{bn...b2b1b0}必存在 ar=br(k < r < n)且 ak < bk

接下来是动态规划的初始化——令F[i][j]表示第i位是j的数的方案总数。当满足第i位(j)不为4,第i-1位(k)不为2或者i-1位为2但是第i位不为6:

若当前枚举到第i位,第i位数字为j,第i-1位数字为k

当且仅当j≠4且k≠2或者k=2但j≠6时,有dp[i][j]=sum(dp[i-1][k])

通过三层从小到大的循环完成i,j,k的枚举。

这是我们继续建立计算的基础——先用dig[]储存m、n的十进制表示,然后从高位开始枚举0~dig[i]-1,保证枚举出的数小于给出的数,且。设当前枚举位为j,如果满足 E() ,就在答案中加上 F[i][j]。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int F[15][15];
void GetDig(int num,int dig[])
{
    do{
		dig[++dig[0]]=num%10;
		num/=10;
	}while(num);
}
void GetF()
{
	F[0][0]=1;
	for(int i=1;i<=7;i++)
		for(int j=0;j<10;j++)
			for(int k=0;k<10;k++)
				if((j!=4) && !(j==6 && k==2))
					F[i][j]+=F[i-1][k];
}
int GetAns(int num,int dig[])
{
	int ret=0;
	for(int i=dig[0];i>0;i--)
	{
		for(int j=0;j<dig[i];j++)
		{
			if((j!=4) && !(j==2 && dig[i+1]==6))
				ret+=F[i][j];
		}
		if((dig[i]==4) || (dig[i]==2 && dig[i+1]==6))
				break;
	}
	return ret;
}
int main()
{
	GetF();
	int m,n;
	while(~scanf("%d%d",&n,&m) && n && m)
	{
		int dign[15]={},digm[15]={};
		GetDig(n,dign);GetDig(m+1,digm);
		int ansn=GetAns(n,dign),ansm=GetAns(m+1,digm);
		printf("%d
",ansm-ansn);
	}
	return 0;
}

◆有点意思◆ B-number

这道题做不做的出来,你心里难道就没有一点B数吗?

每个人心中都有一个B数(向yhn大佬致敬)。这道题其实是上一个题的升级版。

万事开头难,先看看状态定义:

dp[i][j][k] (满足0 ≤ i < len,0 ≤ j < 13,0 ≤ k < 3)

表示第i位时当前数模13余j数中,满足条件E(k)的数的个数;

E(k):k=0时,不存在13;k=1时,不存在13但是存在3;k=2时,不存在1或3

这里的第三维其实也可以用k表示当前数位的数字,但是显然可以更加优化!有时候只需要储存一些会对答案产生影响的条件作为一维。虽然减少了空间消耗,但是作为代价,思维的复杂程度和转移方程的错误率也相应增加,初学者还是不要尝试这种方法。(`・ω・´)

由于这道题有多组数据(o(゚Д゚)っ!),而且每一组数据相应的dp数组值不一样。这就意味着要针对每一组数据单独进行一次DP求解——这是数位DP中较特殊的题型。

与一般数位DP相同,我们仍需考虑n的数位。但是因为题目默认是1~n,就没有必要进行前缀和的操作了,我们可以同时考虑数位和DP转移,但是DP参数就会更复杂:

ll DP(int pos,int mod,int lst,bool lim,bool flg) //你没有看错,是long long
/*
pos: 现在枚举到的位置
mod: 当前数模13的余数
lst: 上一个数位的数字
lim: 是否达到数位限制*
flg: 当前是否已经存在13
*/

*:

什么叫数位限制?

我们令n的10进制表示为dig[0len-1]、枚举的数字m的10进制表示为fdig[0len-1],若我们现在从最高位枚举到第k位,满足dig[r]==fdig[r](k < r < len),则称第k数位的枚举受数位限制,此时的fdig[k]只能取 0dig[k];如果不受数位限制,则fdig[k]可取09!

总而言之,有了数位限制才能控制枚举出的数是小于等于n的数!

特别声明:数的最高位总受数位限制

于是我们发现,dp[][][] 只能储存当前位不受数位限制的答案,所以当前位受限制时不能将答案储存在dp里,同样,在记忆化搜索的记忆性返回的时候要保证现在不受数位限制。

接下来就是转移方程了:

令tot=sum(dp[pos-1][(mod*10+1)%mod][]),第三维由记忆化的返回值决定;

当flgtrue时,dp[pos][mod][0]=tot;
当flag
false且lst1时,dp[pos][mod][1]=tot;
当flag
false且lst!=1时,dp[pos][mod][2]=tot;

我知道看上面的这么多内容很难理解,所以下面放代码啦。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
#define MOD 13
ll dp[40][13][3];
//[i][j][k]:k=2 - no 1 or 3; k=1 - no 13 but 3; k=0 no 13
int num[15];
ll DP(int pos,int mod,int lst,bool lim,bool flg)
{
	if(pos<0)
	{
		if(flg && !mod) return 1ll;
		else return 0ll;
	}
	if(dp[pos][mod][0]!=-1 && !lim && flg) return dp[pos][mod][0];
	if(dp[pos][mod][1]!=-1 && !lim && lst==1 && !flg) return dp[pos][mod][1];
	if(dp[pos][mod][2]!=-1 && !lim && lst!=1 && !flg) return dp[pos][mod][2];
	int limit=lim? num[pos]:9;
	ll tot=0;
	for(int i=0;i<=limit;i++)
	{
		bool lflg=(flg||(lst==1 && i==3)),llim=(lim && (i==limit));
		tot+=DP(pos-1,(mod*10+i)%MOD,i,llim,lflg);
	}
	if(!lim)
	{
		if(flg) dp[pos][mod][0]=tot;
		if(!flg && lst==1) dp[pos][mod][1]=tot;
		if(!flg && lst!=1) dp[pos][mod][2]=tot;
	}
	return tot;
}
ll GetAns(int n)
{
	int len=0;
	memset(num,0,sizeof num);
	while(n)
		num[len++]=n%10,n/=10;
	return DP(len-1,0,0,true,false);
}
int main()
{
	memset(dp,-1,sizeof dp);
	int n;
	while(~scanf("%d",&n))
		printf("%lld
",GetAns(n));
	return 0;
}


■异进制的世界■

◆预备烧脑◆ Amount of Degrees

  • URAL - 1057
  • 【解析】
    其实就是求区间[X,Y]中有多少个数能表示成一个B进制的01数串,并且1的个数恰好为K(比如 B=3,K=2 时 101(3)=32+30=10(10))。
    求区间中的个数仍然是使用前缀和的思想。
    但是由于B进制并不方便思考,所以将B进制强制转换为2进制(重点:将除10进制外的所有进制按照2进制强制处理),这也是为什么题目给的Y是以2的幂为上限(Y≤2^31-1)。
    定义 F[i][j] 为满足条件的长度为i,包含j个1的二进制数。容易得到转移方程:

F[i][j]=F[i-1][j]+F[i-1][j-1]

之后就是处理答案。也就是GetAns(),其实和dig[]的思路一样,只是因为用2进制储存,还要比十进制更简单——只需要判断当前位是0还是1就可以了。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int F[40][40];
void init()
{
	F[0][0]=1;
	for(int i=1;i<40;i++)
	{
		F[i][0]=F[i-1][0];
		for(int j=1;j<=i;j++)
			F[i][j]=F[i-1][j]+F[i-1][j-1];
	}
}
int GetAns(int num,int mul,int mod)
{
	int sit[40]={},siz=1,ret=0;
	while(num) sit[siz++]=num%mod,num/=mod;
	for(int i=siz-1;i>0 && mul>=0;i--)
	{
		if(sit[i]>1) {ret+=F[i-1][mul-1]+F[i-1][mul];break;}
		if(sit[i]==1) ret+=F[i-1][mul],mul--;
	}
	return ret;
}
int main()
{
	init();
	int x,y,k,b;
	scanf("%d%d%d%d",&x,&y,&k,&b);
    int ans1=GetAns(y+1,k,b),ans2=GetAns(x,k,b);
    printf("%d
",ans1-ans2);
	return 0;
}

The End

Thanks for reading!

-Lucky_Glass

欢迎转载٩(๑❛ᴗ❛๑)۶,请在转载文章末尾附上原博文网址~
原文地址:https://www.cnblogs.com/LuckyGlass-blog/p/9121151.html