这几天在看数位dp,总算稍微有一丢丢的看明白了,所以就来浅浅的分享一下吧!!!
概况
数位dp主要是来求一个区间满足条件的个数的
比如 :从10到20之间求1的个数。显然有11个,但是在做题的时候范围会很大像是1e18 之类的 ,所以通常的方法就很容易超范围所以数位dp是一个不错的选择。
基本思想
假设范围是n~m,我们可以通过dp求出1到n的满足条件的个数,在求出1到m满足条件的个数。两者相减就是范围内满足条件的个数。
long long int n,m;
cin>>n>>m;
cout<<solve(m)-solve(n-1)<<endl;
大体的思想就是,我们通过n分解成每一位数,从最高位开始枚举。
比如:234;
如果最高位枚举2的话十位则枚举的范围是(0~3)
如果最高位枚举的是(0 ~ 1)时,十位则枚举范围是(0 ~ 9)之后随着位数以此类推
2 3 4
2 3 (0~4)
2 (0~2) (0~9)
(0~1) (0~9) (0~9)
采用这个枚举方式进行记忆化搜索;
模板
数位dp感觉还是蛮容易想的如果你知道模板
ll dp[20][20]; // 为了记忆化搜索储存
ll arr[100]; //为了统计每一位数的值,所以数组大小不用太大。
//pos为位数,就是dfs执行到一个数哪一位。
//sta是状态,状态不同的题状态不一样
// land为前导0 比如从0到12345 则1就表示成00001,这些0就是前导0
//limit是为了记录上一位的值是不是最大值,好为了判断这一位值的范围 比如123 十位如果执行到2 则各位范围只能是0~3,如果十位是(0~1)则各位可以是(0~9)
ll dfs(int pos,int sta,int land,int limit)
{ // land为1则这一位是前导0
// limit为1则表示执行到这个数这一位的最大值
if(pos==-1)return 1;//表示一个数执行完成了 返回值不一定是1,根据题目会有所改变。
if(!limit&&!land&&dp[pos][sta]!=-1)return dp[pos][sta];
//记忆化存储 枚举到当前位置 pos,状态为 sta 的数量,dp 值保存的是满足条件数的个数
int up=limit?arr[pos]:9;//枚举这一位的上界
long long int ret=0;
for(int i=0;i<=up;i++)
{
if()
。。。。。
if(....)
。。。。。
ret+=dfs(pos-1,sta(/*状态的变形*/),land&&i==0,limit&&i==arr[pos]);
//进行搜索
}
if(!land&&!limit) dp[pos][sta]=ret;//搜索完成储存数目
return ret;
}
ll solve(ll x)
{
int pos=0;
while(x)//分解位数
{
arr[pos++]=x%10; //储存上界的每一位
x/=10;
}
return dfs(pos-1,0,1,1);
}
int main()
{
ll n,m;cin>>n>>m;
memset(dp ,-1,sizeof dp); //进行初始化
cout<<solve(m)-solve(n-1)<<" ";
cout<<endl;
}
注意:
(1)前导0的使用方法
当初看的时候就是感觉这个前导0好像在这串代码里没啥用的样子。但是确实有用(hhhh) 上面说了我们给每一个数都在前面加了前导0让他和最大数的位数相同。这里前导0有可能扰乱我们的基数
比如:我们来计数(1~123)的0的个数
1则就相当于001,1前面的前导0 就会扰乱我们对0的计数。
(2)我们的状态需要自己定义,这还是一个难点对我来说,还有就是那个pos==-1返回的值不是一成不变的根据题意稍作修改。
(3)还要注意dp数组还有里面的个别变量会超过int类型的最大值。所以需要把它们设置成 long long
例题
(1)P4999 烦人的数学作业
求给定区间内每个数的每一位数之和的和;
比如 123~124=1+2+3+1+2+4
代码:
#include<bits/stdc++.h>
#define INF 0x7f7f7f7f
#define MAXN 100005
using namespace std;
long long int n,m,f[408][408],arr[1000109],c[100009],sum[409],dp[408][408];
long long int mod=1e9+7;
long long dfs(long long int pos,long long int sum,int limit)
{
if(pos==-1)return sum;
if(!limit&&f[pos][sum]>=0)return f[pos][sum];
int up=limit?arr[pos]:9;
int temp=0;
for(int i=0;i<=up;i++)temp=(temp+dfs(pos-1,sum+i,limit&&i==up))%mod;
if(!limit)f[pos][sum]=temp;
return temp;
}
long long int solve(long long int x)
{
long long int pos=0;
while(x)
{
arr[pos++]=x%10;
x/=10;
}
return dfs(pos-1,0,1)%mod;
}
int main()
{
int t;
cin>>t;
memset(f,-1,sizeof f);
while(t--)
{
long long int n,m;
cin>>n>>m;
cout<<(solve(m)-solve(n-1)+mod)%mod<<endl;
}
}
(2)P2602 [ZJOI2010]数字计数
给定范围内出现(0~9)的次数
代码:
#include<bits/stdc++.h>
#define INF 0x7f7f7f7f
#define MAXN 100005
#define ll long long
using namespace std;
ll dp[20][20][20];
ll arr[10000];
ll dfs(int pos,int sta,int land,int limit,int number)
{
if(pos==-1)return sta;
if(!limit&&!land&&dp[pos][number][sta]!=-1)return dp[pos][number][sta];
int up=limit?arr[pos]:9;
long long int ret=0;
for(int i=0;i<=up;i++)
{
if(land&&i==0)
ret+=dfs(pos-1,sta,land&&i==0,limit&&i==arr[pos],number);
else
ret+=dfs(pos-1,sta+(number==i),land&&i==0,limit&&i==arr[pos],number);
}
if(!land&&!limit) dp[pos][number][sta]=ret;
return ret;
}
ll solve(ll x,int number)
{
int pos=0;
while(x)
{
arr[pos++]=x%10;
x/=10;
}
return dfs(pos-1,0,1,1,number);
}
int main()
{
ll n,m;cin>>n>>m;
memset(dp ,-1,sizeof dp);
for(int i=0;i<10;i++)
cout<<solve(m,i)-solve(n-1,i)<<" ";
cout<<endl;
}
这两个题一个使用了前导0,一个 没有使用,细细品味!!!