【洛谷】 P4127 [AHOI2009] 同类分布

2 篇文章 0 订阅

这是一道 2018.7.25 第一网教考试题,考试的时候秒想出了是数位Dp,却被卡在了Dp状态的定义上,最后没有做出来,乖乖打了暴力

 

顺便扯一下什么叫数位Dp,识别方法有两种:看数据范围,看需要求的答案

一般的常识数据范围是1e12~1e18,而且让你无法线性求得,一般求得是某一段区间中满足某一些条件的数的个数

 

Dp肯定需要Dp数组,那么数位Dp的Dp数组究竟长什么样子呢?

其实从某种意义上说,数位Dp就是暴搜(加了一个记忆化的数组),而且搜的很有技巧;没错,Dp数组其实就是那个记忆化数组

 

为什么可以记忆化,首先要了解数位Dp的大体框架

数位Dp是将一个数字拆成若干(cnt)位,那么我们得到了一个长度为cnt的数组dig [ ],这个dig从1~cnt位是某一个数从低位到高位的映射

如果要求得1~R中满足我们常常把R拆位,得到R的dig [ ] 数组,这个R的各个位有什么用呢?

由于我们需要求得答案一定是小于R的某些数组成的贡献,那么这个R的dig [ ] 数组其实给了我们一个界限

 

那么问题来了,什么时候可以超过dig [ ] 中的某一位呢?想一下,我们平常是如何判定两个数的大小的,如果两个数的位数是一样的,是不是先看最高位的大小关系,如果最高位相等,那么再看次高位,否则就可以确定两个数的大小关系啦

这对数位Dp有什么启发呢?其实就是一个判断上一位是否顶了最高位的上界,这一位可以取到的值是0~dig [ 某一位 ] 还是 0~9,一定是由上一位是否顶上界决定的,举个例子:12345这个数字,在进行到3这一位时,如果上一位是1,那么没有顶2的上界,则3这一位随便取0~9中的数,但是如果是2的话,就要看2的上一位是否顶上界,原则一致,如果都顶上界,那么3这一位只能选0~3中的数了,因为如果选了4,就会最终形成124xx的形式,超越了12345,这样会统计到超过R的数,完蛋了

解决的方法很简单,就是开一个变量lim,lim=1表示之前的所有位都顶了上界,而lim=0表示有一位没有顶上界,如果lim是0的话表示有一位已经小于了那一位的上界,那么其位后面的所有数不管选择什么数都无所谓了,不会越过R的界限。

 

解决了关于如何合法进行Dp的步骤,那么接下来需要如何进行记忆化搜索呢?首先记忆化的条件是什么,是不是后来进行的Dfs的过程和之前某一次Dfs的过程会完全一致,那么得到的对答案贡献一定也是一样的,所以没有必要再去Dfs一遍,这样大大减小了遍历相同解答子树的次数,从而减小了时间复杂度,也是一个Dp过程

 

那么怎样合理的进行记忆化呢?首先要判断该状态一定会转移到特定的状态且保证再次来到这种状态的时候还是会按照之前转移的方式来转移的,这样的话是合理的,否则不能对该数位Dp状态记忆化;还有就是定义记忆化的Dp数组的方法,首先是有确切的思路,那么就可以按照状态转移的特定性定义啦,还有就是玄学乱猜,一般记忆化Dp数组的维度都是很小的(不是Mod就是Sum),反正很多变,需要积累经验

 

没错这就顺理成章引出了如何做这道题了,我不是被卡在了状态定义上吗?因为本题求出满足要求的数必须要知道这个数是什么,才能知道是否能够整除,这样的状态肯定开不下啦(一维就是1e18)!所以啊,换个思路,我们想想,如果各个位上的数字和能够整除原数,那么是不是 原数 % 各位数字和 == 0 就好啦?所以可以转化为:求满足原数x % 一个特定的数y,并且保证x的各位数字等于y,那么这个数是不是满足要求啊?计入答案;那么我们只需要枚举这个特定的数y就好啦,这个y最大会有多大?想一想每一位最大是9,一共有18位,所以,最多可能达到18*9=162,区区小数……

 

那么这道题就完了,其实还有一个数位Dp的细节:返回一个已经Dfs过的状态必须要判断前面的位是否全部顶上界,如果没有顶上界才能记忆化地返回Dp值,但是如果顶上界则不能返回,因为这样会将“非合法”状态的Dp值返回(非合法状态其实在某种意义上来说就是某些时候是合法的,但是有些时候是不合法的,当且仅当它代表的状态所统计的数超过了R时不合法)

 

Ac代码:

 

#include <bits/stdc++.h>

long long  dp [ 20 ] [ 200 ] [ 200 ] ;
long long  dig [ 20 ] ;
long long  lx , rx ;
int  cnt , mod ;

long long  dfs ( int  dep , int  res , int  sum , int  lim ) {
	if ( dep == 0 )  return  ( res == 0  &&  sum == mod ? 1 : 0 ) ;
	if ( ( ! lim  )  &&  dp [ dep ] [ res ] [ sum ] != - 1 )  return  dp [ dep ] [ res ] [ sum ] ;
	int  top = lim ? dig [ dep ] : 9 ;
	long long  tmp = 0 ;
	for ( int  i = top ; i >= 0 ; i -- ) 
		tmp += dfs ( dep - 1 , ( res * 10 + i ) % mod , sum + i , lim  &&  top == i ) ;
	if ( ! lim )  dp [ dep ] [ res ] [ sum ] = tmp ;
	return  tmp ;
}

long long  solve ( long long  x ) {
	cnt = 0 ;
	long long  ans = 0 ;
	memset ( dp , - 1 , sizeof ( dp ) ) ;
	while ( x ) {
		dig [ ++ cnt ] = x % 10 ;
		x /= 10 ;
	}
	for ( mod = 1 ; mod <= 9 * cnt ; mod ++ ) {
		ans += dfs ( cnt , 0 , 0 , 1 ) ;
		memset ( dp , - 1 , sizeof ( dp ) ) ;
	}
	return  ans ;
}

int  main ( ) {
	std :: cin >> lx >> rx ;
	std :: cout << solve ( rx ) - solve ( lx - 1 ) << std :: endl ;
	return  0 ; 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值