李老师太巨啦!!!
带着一罐刚喝完的红牛的李老师走进教室
xaero:“红牛喝不喝"
李老师:“不喝不喝,再喝要猝死了”
于是李老师带走了两罐新的红牛的空罐子
李老师讲课的时候谈到了数位dp,然后发现好久没弄过了……于是去浅谈了一下。
一般数位DP是用于计数的DP,一般用于求 [ l , r ] [l,r] [l,r]之间满足某种规则(设规则为 g ( x ) g(x) g(x)),也就是满足 g ( x ) g(x) g(x)的数有多少个
数位的含义:一个数有个位、十位、百位、千位…数的每一位就是数位的意思
首先数位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(n∗right)的,
那么,我们加上记忆化搜索的枚举:
控制上届( 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));
}
著名经典数位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;
}
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));
}
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));
}
总结:
大概是写完了?
等我模拟赛打完回来完善一下~