浅谈数位DP

浅谈数位DP

前言

李老师太巨啦!!!
带着一罐刚喝完的红牛的李老师走进教室
xaero:“红牛喝不喝"
李老师:“不喝不喝,再喝要猝死了”
于是李老师带走了两罐新的红牛的空罐子

李老师讲课的时候谈到了数位dp,然后发现好久没弄过了……于是去浅谈了一下。

数位DP是什么

一般数位DP是用于计数的DP,一般用于求 [ l , r ] [l,r] [l,r]之间满足某种规则(设规则为 g ( x ) g(x) g(x)),也就是满足 g ( x ) g(x) g(x)的数有多少个
数位的含义:一个数有个位、十位、百位、千位…数的每一位就是数位的意思
首先数位DP有两种实现形式,递推与记忆化搜索,当然选取于数据的组数。

数位DP的大概如何操作

因为我的水平有限,故只能给出“大概”
首先给出对于简单求上述模型,“一般用于求 [ l , r ] [l,r] [l,r]之间满足某种规则(设规则为 g ( x ) g(x) g(x)),也就是满足 g ( x ) g(x) g(x)的数有多少个”:

for(int i=l;i<=r;i++)  
        if(right(i)) ans++;  

然鹅这样的暴力是 O ( n ∗ r i g h t ) O(n*right) O(nright)的,

那么,我们加上记忆化搜索的枚举:
控制上届( l i m i t limit limit)枚举,从最高位向下枚举,用DP方程的形式也就是 f [ i ] [ j ] f[i][j] f[i][j]表示枚举到第 i i i位(从高位往低位)时,当前这一位的数字为 j j j,有多少个符合方案的数
那么,以数字243为例枚举,
1.当最高位是0时,对于这个回一个 f r o n t z e r o frontzero frontzero(判断前导0)的变量标记,(前导0对于计数的影响要看具体题目的,有比较大影响的比如说数字计数(ZJOI2010))
2.最高位是1时,后面自然可以枚举 0 0 0~ 9 9 9
3.最高位是2时,为了防止计了其他的数,第二位就有 l i m i t limit limit,也就是上限,第二位只能枚举到4,如果第二位枚举到4了,那么第三位只能枚举1以此类推。

一般数位DP的主程序如下,
就是到右边界所有的符合条件的数,减去到左边界-1符合条件的所有数(至于为什么不直接处理中间呢,是因为比较难以实现,要限制的东西太多 l e r ler ler

int main()  
{  
    long long l,r;  
    scanf("%lld%lld",&l,&r);
    printf("%lld\n",solve(r)-solve(l-1));  
}  

接下来给出数位DP的模板,讲解基本全在模板里了

#include 
using namespace std;
int f[12][10],n,m,a[12];
int dp(int len,int pre,bool limit,bool frontzero)//len是枚举的位,limit是上限,
//frontzero是
//前导0的判断,pre是前一位是什么
//数,也就是用来判断某些条件的,比如说不要六二,
//pre为6的时候,这一位就不能是2
{
	if (len==0) return 1;//枚举到个位的后一位那么直接可以退出了
	if (!frontzero&&!limit&&f[len][pre]!=-1) return f[len][pre];//如果
	//前导都不是0,
	//且也不受上限限制,那么f[len][pre]也有
	//值,那么就这个状态可以直接作为f[len+1][pre](即位数为len+1,
	//这一位的数字为pre)返回
	int p,ans=0,maxx=(limit?a[len]:9);//有上限时,只能是这一位原数字上
	//的数字,否则为9
	for (int i=0; i<=maxx; i++)
	  {
	  	if () continue;//视具体题目条件而定
	  	p=i;
	  	if (frontzero&&i==0) p=-10000;//如果一直是0,那么把p设置
	  	//成一个绝对为f[len-1][p]
	  	//绝对为-1的状态,让后面不能过继。
	  	ans+=dp(len-1,p,limit&&(i==maxx),(p==-10000));//把有没有受
	  	//上限控制,前导是不是都为0,
	  	//前导都为0的状态的话一定要单独
	  	//做
	  }
	if (!frontzero&&!limit) f[len][pre]=ans;//记忆化搜索
	return ans;
}
 

inline int solve(int x)
{
	int numx=0;
	memset(a,0,sizeof(a));
	while (x)
	  {
	  	a[++numx]=x%10;
	  	x/=10;
	  }//把x的位数及每一位上是什么记录下来
	memset(f,-1,sizeof(f));
	return dp(numx,-10000,1,1);
}
int main()
{
	scanf("%d%d",&n,&m);
	printf("%d",solve(m)-solve(n-1));
}

例题

T1

著名经典数位DP,不要62,求 [ l , r ] [l,r] [l,r]之间不包含62和4的数字个数

#include
#include
#include
#include
using namespace std;
typedef long long ll;
int a[20];
int dp[20][2];
int dfs(int pos,int pre,int sta,bool limit)
{
    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(pre==6 && i==2)continue;
        if(i==4) 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;
    }
    return dfs(pos-1,-1,0,true);
}
int main()
{
    int le,ri;
    while(~scanf("%d%d",&le,&ri) && le+ri)
    {
        memset(dp,-1,sizeof dp);
        printf("%d\n",solve(ri)-solve(le-1));
    }
    return 0;
}

T2

windy数
大概是相邻两位差不能小于等于2的数

#include 
using namespace std;

int f[12][10],n,m,a[12];

int dp(int len,int pre,bool limit,bool frontzero)
{
	if (len==0) return 1;
	if (!frontzero&&!limit&&f[len][pre]!=-1) return f[len][pre];
	int p,ans=0,maxx=(limit?a[len]:9);
	for (int i=0; i<=maxx; i++)
	  {
	  	if (abs(i-pre)<2) continue;
	  	p=i;
	  	if (frontzero&&i==0) p=-10000;
	  	ans+=dp(len-1,p,limit&&(i==maxx),(p==-10000));
	  }
	if (!frontzero&&!limit) f[len][pre]=ans;
	return ans;
}
 

inline int solve(int x)
{
	int numx=0;
	memset(a,0,sizeof(a));
	while (x)
	  {
	  	a[++numx]=x%10;
	  	x/=10;
	  }
	memset(f,-1,sizeof(f));
	return dp(numx,-10000,1,1);
}
int main()
{
	scanf("%d%d",&n,&m);
	printf("%d",solve(m)-solve(n-1));
}

T3

ZJOI 数字统计
注释放代码里 l e r ler ler

#include
using namespace std;
typedef long long ll;
ll f[15][2][15][2];
int num[N];  //num来存这个数每个位子上的数码
/*
记忆化搜索。
len是当前为从高到低第几位。
limited表示当前位是否和num[len]相等,
0是相等,1是不相等。
sum表示当前数字出现的次数。
frontzero表示之前是否是前导0。
d是当前在算的数码。
*/
ll dfs(int len, bool limited, int sum, bool frontzero, int d)
{
    ll ans=0;
    if (len==0) return sum;  //边界条件
    if (f[len][limited][sum][frontzero] != -1) return f[len][limited][sum][frontzero];  //记忆化
    for (int i = 0; i < 10; i ++)
	  {
        if (!limited && i > num[len]) break;
        /*
        由于我们是从高位到低位枚举的,
        所以如果之前一位的数码和最大数的数码相同,
        这一位就只能枚举到num[len];
        否则如果之前一位比最大数的数码小,
        那这一位就可以从0~9枚举了。
        */
        ans+=dfs(len-1,limited||(i<num[len]),sum+((!frontzero||i)&&(i==d)),frontzero&&(i==0),d);
        /*
        继续搜索,数位减一,
        limited的更新要看之前有没有相等,
        且这一位有没有相等;
        sum的更新要看之前是否为前导0或者这一位不是0;
        frontzero的更新就看之前是否为前导0且这一位继续为0;
        d继续传进去。
        */
      }
    f[len][limited][sum][frontzero] = ans;
    //记忆化,
    //把搜到的都记下来
    return ans;
}

ll solve(ll x, int d)
{
    int len = 0;
    while (x)
	  {
        num[++len]=x%10;
        x/=10;
      } 
    memset(f,-1,sizeof f); 
    return dfs(len, 0, 0, 1, d); 
	//开始在第len位上,
	//最高位只能枚举到num[len]所以limited是0,
	//sum=0,有前导0。
}

int main()
{
    ll a,b; 
    scanf("%lld%lld", &a, &b);
    for (int i=0;i<10;i++)
        printf("%lld ",solve(b,i)-solve(a-1,i));
}

总结:
大概是写完了?
等我模拟赛打完回来完善一下~

你可能感兴趣的:(新知识,个人观点)