title: 数位dp
数位dp
文中提到的引用均来自巨佬
- 数位dp,一般是求一段区间内满足给定条件的数的个数,数位:顾名思义,就是按照一个数的位数进行dp,比如一个三位数,就按照百位、十位、个位进行。
- 但是我感觉,有些带记忆的dfs内味了
数位dp实质还是一种暴力枚举的方法
对于区间 [l,r] 求满足条件的数的个数,最简单的暴力做法如下:
for(int i = l; l <= r; i++){
if(fair(i)) ans++;
}
但是这样如果l 是1而r是1e9,这种暴力的做法就直接没了,而数位dp由于是按照位数进行枚举每一种可能,这样就降低了每一次判断的复杂度(个人理解)其次就是记忆化搜索(这个是最重要的一点),使枚举的数字都满足了dp的性质 满足递推公式 dp[pos][j] = sum(dp[pos - 1][k]) j是指某个状态下(这个可以先略过,重点是公式)
这是我引用的数位dp枚举的一个小过程 说好的自己写呢?
新的枚举:控制上界枚举,从最高位开始往下枚举,例如:ri=213,那么我们从百位开始枚举:百位可能的情况有0,1,2(觉得这里枚举0有问题的继续看)
然后每一位枚举都不能让枚举的这个数超过上界213(下界就是0或者1,这个次要),当百位枚举了1,那么十位枚举就是从0到9,因为百位1已经比上界2小了,后面数位枚举什么都不可能超过上界。所以问题就在于:当高位枚举刚好达到上界是,那么紧接着的一位枚举就有上界限制了。具体的这里如果百位枚举了2,那么十位的枚举情况就是0到1,如果前两位枚举了21,最后一位之是0到3(这一点正好对于代码模板里的一个变量limit 专门用来判断枚举范围)。最后一个问题:最高位枚举0:百位枚举0,相当于此时我枚举的这个数最多是两位数,如果十位继续枚举0,那么我枚举的就是以为数咯,因为我们要枚举的是小于等于ri的所以数,当然不能少了位数比ri小的咯!(这样枚举是为了无遗漏的枚举,不过可能会带来一个问题,就是前导零的问题,模板里用lead变量表示,不过这个不是每个题目都是会有影响的,可能前导零不会影响我们计数,具体要看题目)
上面的枚举只规定了上限,即区间的上边界,那么我们该如果去求得 [l,r] 区间满足条件的个数呢?我们上面求的既然是 [1,r] 那么我们再求一遍 [1 , l -1],这样用 num( [1,r] ) - num([1, l - 1]) 就可以求出 [l, r] 的满足条件的数量
这里我直接引用一个数位dp的模板(套路),把里面一些我自己的理解说一说
typedef long long ll;
int a[20];
ll dp[20][state];//不同题目状态不同
ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
//递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
//第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
/*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/
int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
ll ans=0;
//开始计数
for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
{
if() ...
else if()...
ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的
/*这里还算比较灵活,不过做几个题就觉得这里也是套路了
大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
}
//计算完,记录状态
if(!limit && !lead) dp[pos][state]=ans;
/*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
return ans;
}
ll solve(ll x)
{
int pos=0;
while(x)//把数位都分解出来
{
a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
x/=10;
}
return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
ll le,ri;
while(~scanf("%lld%lld",&le,&ri))
{
//初始化dp数组为-1,这里还有更加优美的优化,后面讲
printf("%lld\n",solve(ri)-solve(le-1));
}
}
对于
-
1、 limit && i==a[pos]
因为我们要判断当前位数前一位是不是到达上界了,只有&后面的条件是不可以的
比如 213 如果 百位枚举1,十位枚举1,那么要枚举个位的时候,十位到达上界了吗?显然没有,因为十位可以枚举到10,所以我们要往前判断两步,一步是枚举上一位数是不是与上边界的相同位的数字相等,以及上上位数是不是处于那一位的边界状态。 -
2、在减枝和记忆化中的 if(!limit) 首先看 return dp[pos][sta] 如果,由于dp[pos][sta]是当前状态和当前位数下所有满足条件的和,比如213 ,dp[1][sta]是后两位所有满足条件的和,也就是说十位的枚举是用0 - 9里面找满足的数字,但是如果没有前面的限制条件,我们在已知百位枚举到2的时候,我们十位可以枚举 0 -9里面满足的数吗?显然不能因为由于前面百位的上界的限制,肯定不能枚举0 -9,所以直接返回会返回比正确答案多的数量,同理在记忆化的时候,由于它枚举的总和小于我们规定的dp[pos][sta]的值,如果这时候记忆化了,就导致dp数组的值要比正确的值小,在后面直接取记忆化的值时,会造成错误
这句引用的话也说的很好
limit为true的数并不多,一个个枚举不会很浪费时间,所以我们记录下! limit的状态解决了不少子问题重叠。
例题:
要求数位上不能有4,且62不能连续出现,这样枚举的时候,遇见4直接跳过,遇到6先记下来,然后在枚举下一位的时候,如果有2就跳过,这里开两个状态记录分别记录,dp[pos][sta] pos是当前位数,sta是上一位是不是6 (0不是,1是)
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
#include <map>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 7;
const int Mod = 1e9 + 7;
int a[20];
int dp[20][2];
//第二维 0是第 i 位前一位不为6时满足条件的个数,1则是前一位为6时满足条件的个数
int dfs(int pos , int pre , int sta, bool limit)
{
//递归终点,到终点的数一定是满足条件的,所以直接计数+1
if(pos == -1) return 1;
if(!limit && dp[pos][sta] != -1) return dp[pos][sta];
int up = limit? a[pos] : 9;
int tmp = 0;
for(int i = 0; i <= up; i++){
if(i == 4) continue;
if(pre == 6 && i == 2) continue;
tmp += dfs(pos - 1 , i , i == 6 , limit && i == a[pos]);
}
if(!limit) dp[pos][sta] = tmp;
return tmp;
}
int Solve(int x)
{
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
int ans = dfs(pos - 1 , -1 ,0 ,true);
return ans;
}
int main()
{
int l,r;
while(scanf("%d%d",&l,&r) && l + r){
memset(dp, - 1, sizeof dp);
printf("%d\n", Solve(r) - Solve(l - 1));
}
return 0;
}
数位dp的优化:
例hdu 4734
题意
题目给了个f(x)的定义:F(x) = An * 2n-1 + An-1 * 2n-2 + … + A2 * 2 + A1 * 1,Ai是十进制数位,然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。
这个题,如果你选择最朴素的数位dp的做法的话,因为输入一次a的值,就要重新清空一次dp数组,由于这个题时间卡的很死,所以把memset放在循环里, 会时间超限
或者按照这种思路但是内存就爆了
如果要memset优化就要加一维存f(a)的不同取值,那就是dp[10][4600][4600],这显然不合法。,所以这里优
思路:
- 那我们就可以选择利用减法,每一步都减去当前位置的权值,那么当走到最后一位的时候,如果sum >= 0 ,说明f(a) >= f(i)的,相反如果sum在某一位的时候 < 0就说明一定f(i) < f(a),就不用继续向下找了,起到了剪枝的作用
所以这样dp状态就和f(a)没有关系了,那么我们只需要在循环外memset一次就可以了
因为我们的终点状态是跟f(a)没有关系的,这样就算我们遇到了上次循环用到的dp数组空间,那么也没有关系,因为上一次的dp[pos][rsum]难道和这一次到这个状态的值不一样吗?显然是一样的,如过没有遇到上一次的状态,那我们也可以当作清空了dp数组,这样来说,清一次dp数组就够了.
TLE 代码
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
#include <map>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 7;
const int Mod = 1e9 + 7;
const int Sum = 5000;
int a[20] , p[11];
int dp[20][Sum];
int cmp;
int dfs(int pos , int sum , bool limit)
{
//递归终点,到终点的数一定是满足条件的,所以直接计数+1
if(pos == -1) return sum <= cmp;
if(!limit && dp[pos][sum] != -1) return dp[pos][sum];
int up = limit? a[pos] : 9;
int tmp = 0;
for(int i = 0; i <= up; i++){
tmp += dfs(pos - 1 ,sum + i * p[pos], limit && i == a[pos]);
}
if(!limit) dp[pos][sum] = tmp;
return tmp;
}
int Solve(int x)
{
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
int ans = dfs(pos - 1, 0,true);
return ans;
}
int f(int x)
{
int l = 0,fsum = 0;
while(x){
int d = x % 10;
x = x / 10;
fsum = fsum + d * p[l];
l++;
}
return fsum;
}
int main()
{
int a,b,t;
scanf("%d",&t);
p[0] = 1;
for(int i = 1; i <= 10; i++) p[i] = p[i - 1] * 2;
//memset(dp, - 1, sizeof dp);
int Case = 0;
while(t--){
memset(dp, - 1, sizeof dp);
Case++;
scanf("%d%d",&a,&b);
cmp = f(a);
printf("Case #%d: %d\n",Case, Solve(b) );
}
return 0;
}
ac代码
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
#include <map>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 7;
const int Mod = 1e9 + 7;
const int Sum = 5000;
int a[20] , p[11];
int dp[20][Sum];
int cmp;
int dfs(int pos , int sum ,bool lead , bool limit)
{
//递归终点,到终点的数一定是满足条件的,所以直接计数+1
if(pos == -1) return sum >= 0;
if(sum < 0) return 0;//剪枝
if(!limit && dp[pos][sum] != -1) return dp[pos][sum];
int up = limit? a[pos] : 9;
int tmp = 0 ,num1;
for(int i = 0; i <= up; i++){
if( lead && !i ) num1 = sum;
else num1 = sum - i * p[pos];
tmp += dfs(pos - 1 ,num1,lead && i == 0, limit && i == a[pos]);
}
if(!limit) dp[pos][sum] = tmp;
return tmp;
}
int Solve(int x)
{
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
int ans = dfs(pos - 1, cmp, true , true);
//前导0 , 上界
return ans;
}
int f(int x)
{
int l = 0,fsum = 0;
while(x){
int d = x % 10;
x = x / 10;
fsum = fsum + d * p[l];
l++;
}
return fsum;
}
int main()
{
int a,b,t;
scanf("%d",&t);
p[0] = 1;
for(int i = 1; i <= 10; i++) p[i] = p[i - 1] * 2;
memset(dp, - 1, sizeof dp);
int Case = 0;
while(t--){
//memset(dp, - 1, sizeof dp);
Case++;
scanf("%d%d",&a,&b);
cmp = f(a);
printf("Case #%d: %d\n",Case, Solve(b) );
}
return 0;
}