浅析数位DP

目录

数位dp的模板

数位dp的思想

数位dp的例题


模板:

话不多说,直接上模板,刚开始实在搞不懂的话也没问题,会套模板就行(手动滑稽):

typedef long long ll;
int temp[pos];
ll dp[pos][sta]
//如果需要处理前导零,还需加入一个参数bool lead判断是否为前导零
ll dfs ( int pos , int sta , bool limit ){
	    if(pos==-1) return (根据sta的状况(题意)确定返回值);
	    if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
	    ll ans=0;
        //不受限的话就能从0搜到9,否则只能搜到temp[pos]
	    int end=limit ? temp[pos] : 9;
	    for(int i=0 ; i<=end ; ++i){
            int newsta;
            if(题目的要求) 更新sta的状态,即newsta;
 		    ans+=dfs(pos-1,newsta,limit&&i==end);//状态转移,若上一位的数值全部枚举完成,下一位将处于受限状态。
    }
    //只在非受限时才记录,因为受限时后面枚举的数字发生了变化,其状态是不确定的,不能直接返回,需要重新枚举确定
    if(!limit) dp[pos][sta]=ans;
    return ans;
}
ll solve(int num){
	memset(dp,-1,sizeof(dp));
	int pos=0;//别忘了赋初值哟~
	while(num){
		temp[pos++]=num%10;
		num/=10;
	}
        //limit的初始值始终赋1.
	return dfs(pos-1,0,1);//这里注意sta的初始值是赋1还是0,要依题意而定。
}

思想:

OK,除了会套模板之外,还是要稍微了解一下数位DP的原理的嘛~下面就简单的介绍一下:

数位DP本质上是一个记忆化搜索,它的搜索顺序是这样的(以一个数256为例):

首先获得256的数位是3位,然后获得它每一位上的数字为2、 5、 6,接着我们可以做出以下的表格:

212
10123456789
00123456789
-1returnreturnreturnreturnreturnreturnreturnreturnreturnreturn

为什么要画这样一个表格呢?因为数位DP的搜索顺序就是从高位出发,即表格的首行出发,然后向表格下一行递归搜索,也就是说数位DP是先搜索000,然后是001、002······009,然后才是010~019、020~029······以此类推直到256(请务必弄清这个搜索顺序!)。由于最高位是处于受限状态的(我们一般都是从高位向低位搜索,最早搜索的是最高位,而最高位一定是受限的,所以现在理解为什么limit的初始值一定是1了吧),只能搜到它在该位上对应的数字temp[pos]=2,也就是我们在这个位置上只能搜索0~2,对于这个位置上搜到的数,都是作为最高位的,比如搜到1,那么它代表的范围就是100~199。继续向下一个位搜索来到10位,对应的数字是5,但是要注意,该位现在并不一定处于受限状态,什么时候才是受限的呢?如果上一位搜到的是0,或者1,很显然它们代表的范围是0~99和100~199,在10位上可以从0搜到9,但如果上一位搜到的是2,它代表的范围是200~256,10位还能从0搜到9吗,显然不能,此时就是受限状态,只能搜到5,同理对于个位的搜索也是一样(看到这里,应该理解了模板的状态转移方程中limit&&i==end的含义了吧)。从个位再往下,已经没有了可搜索的值,此时应该返回满足条件的数了,那么返回多少?这得根据题目来决定,现在假设你搜索到的值是025,而题目的要求是含有2,那么此时就应该返回1,而搜到145则要返回0。

但是到目前为止,似乎这就是换了一个方式的暴力搜索啊,数位DP快速的优势在哪里?我们一开始就讲过,数位DP的搜索不是普通搜索而是记忆化搜索,既然是记忆化搜索当然要有记忆的步骤。当我们搜索完一轮后(比如从000~009),这个时候会需要将当前搜索到的符合条件的数记忆在dp数组中,还是上述的假设,题目的要求是含有2,所以你搜索完000~009后发现了一个合要求的数字002,那么就令dp[pos][sta]=1(pos是当前的数位,sta是当前的状态),那么此后当程序再次搜索到这个范围内时(当然还要求状态sta与记录时的sta一致),比如010~019,程序就不会一个一个的搜索而是直接返回1,由此实现了时间的节省,提高了搜索的效率。最后如果当该数位处于受限状态时,它能搜索的范围就减少了,可能搜不到dp记下的值(不过我举的假设碰巧能搜到,尴尬~),因此不能直接返回,而要重新全部搜索一遍。

例题:

好了,讲了这么多,相信你一定迫不及待想做一两道题来证明自己,ok,满足你的愿望,下面是两道极简单的例题:

1.求从1到n中,只含有0和1的数的个数,n<=10^{9} (如1~11中,只有1,10,11三个数合要求,答案就为3)

解数位DP题,二话不说,直接模板摆出来,需要我们考虑的只有如何实现sta到newsta之间状态的转移。

对于本题,我们只需要考虑一个数是否只含有0和1,那么就只有两种状态,一种是只含有0和1,记该状态为sta=1,一种是含有其它的数,记该状态为sta=0,由于10^{9} 的数位不超过10,可以开出数位数组temp[10],记忆化数组dp[10][2](当然为保险起见,数组可以开大一点)。

下面是状态的转移,开始搜索时假设该数字合要求,sta=1,向下搜索,当发现一个数字既不是0也不是1时,状态改变为newsta=0,一直到pos==-1就搜索完了一个数字,如果此时的sta仍然为1,则说明该数字是合要求的,返回1,否则返回0.

分析到这里,程序也就出来了,核心代码如下:

int temp[11];
int dp[11][2]; 
int dfs(int pos,int sta,bool limit){
        //sta==1的意思是当sta=1时就返回1,否则就返回0
		if(pos==-1) return sta==1;
		if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
		int ans=0;
		int end=limit?temp[pos]:9;
		int newsta;
		for(int i=0;i<=end;++i){
            //状态的转移,只有当当前搜索到的数字是0或1并且前面搜索到数字都合要求时,新状态才能是1 
			if(i<=1&&sta==1) newsta=1;
            //一旦发现某个数字不合要求,新状态就置0
			else newsta=0;
			ans+=dfs(pos-1,newsta,limit&&i==end);
		}
		if(!limit) dp[pos][sta]=ans;
		return ans;
}
int solve(int num){
	    memset(dp,-1,sizeof(dp));
	    int pos=0;
	    while(num){
		    temp[pos++]=num%10;
		    num/=10;
	    }
        //开始时假定每个数都是合要求的,sta赋初值为1
	    return dfs(pos-1,1,1);
}

2.求从1到n中,共包含了多少个数字7,n<=10^{9}(如1~17中,7包含一个7,17包含一个7,答案就为2)

和上面一样,继续分析状态的表示,这次我们不是要求满足某个要求的数有多少,而是求一个数字出现的次数有多少,但是核心的思想仍然没有变,既然统计的是7出现的次数,那么sta就记录一个数字中7的个数,注意是一个数字而不是1~n里的所有数字。因为n不超过10^{9},所以一个数字中出现7的个数不会超过777777777中7的个数,即9个,所以我们开出数位数组temp[11],记忆化数组dp[11][10].

而sta到newsta之间的状态转移就很明显了,只要新搜到了一个数字7,就将原有的sta+1,即newsta=sta+1,否则不变,当pos==-1搜索完一个数字后,直接返回该数字中7的个数即sta的值。核心代码如下:

int temp[11];
int dp[11][10]; 
int dfs(int pos,int sta,bool limit){
		if(pos==-1) return sta;
		if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
		int ans=0;
		int end=limit? temp[pos]:9;
        int newsta;
		for(int i=0;i<=end;++i){
            //此处一定要记得每次循环前都初始化,否则i循环到7后会对后面循环的数字产生影响(newsta++之后若不初始化,i==7之后循环到的所有数字的newsta都被加了1)
			newsta=sta;
			if(i==7) newsta++;//记录一次搜索中出现的7的个数
			ans+=dfs(pos-1,newsta,limit&&i==end);
		}
		if(!limit) dp[pos][sta]=ans;
		return ans;
}
int solve(int num){
	    memset(dp,-1,sizeof(dp));
	    int pos=0;
	    while(num){
		    temp[pos++]=num%10;
		    num/=10;
	    }
        //开始时是一个7都没搜到的,所以sta赋初值为0
	    return dfs(pos-1,0,1);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值