数位 DP 问题往往都是这样的题型,给定一个闭区间 ,让你求这个区间中满足 某种条件 的数的总数。首先我们将问题转化成更加简单的形式。设 表示在区间 中满足条件的数的数量,那么所求的答案就是 。
个人感觉数位DP终点是找状态,不重不漏,找相邻几位的关系,进行状态的转移,因为要记忆化就要正确找准状态。至于找状态就是找变量?
数位DP的 函数般都有 :从高位开始的位置, :一个标志,用来判断给定数位上的上界, :就是状态(比如记录前一位的数是多少),其余的流程一般都差不多,所以只要找好状态就和套板子差不多。
优化
1、memset 优化 将memset提到外面(limit值与所设状态无关)
给定不同
值,可增加一维存储状态
,或者改变dp策略(如下)
2、减法优化(改变dp策略,以便可以使用memset优化)
1、hdu2089 不要62(基础入门)
题意:求
中没有62和4的数的个数。
这个题目状态的转移只和前一位是否是6有关。
是枚举到第
位并且前一位不是6的数的个数
是枚举到第
位并且前一位是6的数的个数
int a[20];
int dp[20][2];
int dfs(int pos,int pre,bool 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(i == 4 ) continue;
if(pre == 6 && i == 2) continue;
tmp += dfs(pos-1,i,i==6,limit&&i == a[pos]);
}
if(!limit) return dp[pos][sta] = tmp;
else return tmp;
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos - 1,-1,0,1);
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n,m;
while(cin >> n >> m&&n + m){
memset(dp,-1,sizeof dp);
cout << solve(m) - solve(n-1)<<endl;
}
return 0;
}
hdu4734 F(x)(减法优化)
求
中小于
的个数。
是到第在第i位后小于等于j的数的个数。
int dp[30][N/10];
int a[20];
int all = 0;
int dfs(int pos,int sum,bool limit){
if(pos == -1) return sum <= all;
if(sum > all) return 0;
if(!limit&&dp[pos][all - sum]!=-1) return dp[pos][all - sum ];
int up = limit?a[pos]:9;
int tmp = 0;
for(int i = 0;i <= up;++i){
tmp += dfs(pos-1,sum + i*(1<<pos) ,limit &&i == a[pos]);
}
if(!limit) dp[pos][all - sum] = tmp;
return tmp;
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos - 1,0 ,1);
}
int main(){
int t;
scanf("%d",&t);
memset(dp,-1,sizeof dp);
for(int p = 1;p <= t;++p){
all = 0;
//memset(dp,-1,sizeof dp);
int n,m;
scanf("%d%d",&n,&m);
int s = 0;
int b = 1;
while(n){
all += n % 10 * b;
n /= 10;
b *= 2;
}
printf("Case #%d: %d\n",p,solve(m));
}
}
poj3522 Round Numbers
求
的数中二进制0的个数大于等于1的个数。
表示第
位
后满足条件数的个数。
因为
的个数可能为负,所以加上个40.
int dp[40][80];
int a[40];
ll dfs(int pos,int sta,int lead,int limit){
if(pos == -1) return sta >= 40;
if(!lead&&!limit&&dp[pos][sta] != -1 ) return dp[pos][sta];
int up = limit?a[pos]:1;
ll tmp = 0;
for(int i = 0;i <= up;++i){
if(lead&&i == 0) tmp += dfs(pos-1,sta ,lead,limit&&i == a[pos]);
else tmp += dfs(pos - 1,sta+(i == 0?1:-1),lead&&i == 0,limit&&i == a[pos]);
}
if(!limit&&!lead) dp[pos][sta] = tmp;
return tmp;
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x % 2;
x /= 2;
}
return dfs(pos - 1,40,1,1);
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
memset(dp,-1,sizeof dp);
ll n,m;
while(cin >> n >> m){
printf("%lld\n",solve(m) - solve(n-1));
}
return 0;
}
hdu3652 B-number
求
中含有数字13并且可以被13整除的数的数目。
pos表示第几位(高位开始)
sta:1代表着前一位为1,2代表着在前几位中已经存在13这个数字了,0表示前一位不是1.
mod 代表着前几位%13的余数。
要开三维,因为紧靠pos和mod不能确定完整的状态,因为sta也是可变的
int a[20];
int dp[20][3][13];
int t(int x,int y,int z){
if(z == 2) return 2;
if(x == 1&&y == 3) return 2;
if(y == 1) return 1;
return 0;
}
int dfs(int pos,int pre,int mod,int sta, bool limit){
if(pos == -1) return mod == 0 && sta == 2;
if(!limit&&dp[pos][sta][mod] != -1) return dp[pos][sta][mod];
int up = limit?a[pos]:9;
int ans = 0;
for(int i = 0;i <= up;++i){
ans += dfs(pos-1,i,(mod*10 + i)%13,t(pre,i,sta),limit&&i == a[pos]);
}
if(!limit) dp[pos][sta][mod] = ans;
return ans;
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos - 1,-1,0,0,1);
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n;
memset(dp,-1,sizeof dp);
while(cin >> n){
printf("%d\n",solve(n));
}
return 0;
}
hdu4507吉哥系列故事——恨7不成妻
题意:
首先定义与7有关的数
如果一个整数符合下面3个条件之一,那么我们就说这个整数和7有关——
1、整数中某一位是7;
2、整数的每一位加起来的和是7的整数倍;
3、这个整数是7的整数倍;
现在问题来了:吉哥想知道在一定区间内和7无关的数字的平方和。
思路:
如果单存的考计数就是数位dp的模板题了。
现在是求
内与7无关数字的平方和。
我们定义
表示第pos位,余数为Mod,数字和为sum的状态
每个状态有
:当前状态下符合要求的数的个数,数的和,数的平方和
假设我们第pos位为
,
然后
表示从
位开始的数,比如有
个
因为我们要统计这些数平方的和
那么上式就变为了
表示下个状态符合要求的数,
表示下个状态符合要求的数的和,
就是下个状态的
。这就是
中的状态转移吧,变成了子问题。这题感觉好棒啊!
const int N = 1e5 + 10;
const ll mod = 1e9 + 7;
using namespace std;
int a[20];
ll p[20];
struct node{
ll cnt;
ll sum;
ll sqsum;
node(ll a,ll b,ll c):cnt(a),sum(b),sqsum(c){}
node(){cnt = -1;sum = 0;sqsum = 0;}
}dp[20][7][7];
node dfs(int pos,int Mod,ll sum,bool limit){
if(pos == -1) return node(Mod!=0&&sum!=0,0,0);
if(!limit&&dp[pos][Mod][sum].cnt != -1) return dp[pos][Mod][sum];
int up = limit?a[pos]:9;
node tmp(0,0,0),ans(0,0,0);
for(int i = 0;i <= up;++i){
if(i == 7) continue;
tmp = dfs(pos-1,(Mod*10+i)%7,(sum+i)%7,limit&&i == a[pos]);
ans.cnt = (ans.cnt + tmp.cnt) % mod;
ans.sum = (ans.sum + tmp.cnt%mod*i*p[pos]%mod + tmp.sum)%mod;
ans.sqsum = (ans.sqsum + tmp.sqsum % mod + 2LL*i*p[pos]%mod*tmp.sum) % mod;
ans.sqsum = (ans.sqsum + i*i*p[pos]%mod*p[pos]%mod*tmp.cnt%mod) % mod;
}
if(!limit) dp[pos][Mod][sum] = ans;
return ans;
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,0,1).sqsum;
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int t;
p[0] = 1;
for(int i = 1;i <= 18;++i) p[i] = p[i-1] * 10 % mod;
scanf("%d",&t);
while(t--){
ll l,r;
scanf("%lld%lld",&l,&r);
printf("%lld\n",((solve(r) - solve(l-1)%mod+mod))%mod);
}
return 0;
}
#10164. 「一本通 5.3 例 2」数字游戏
题意:
让你求
内不降的数字,如123445。
思路:
满足区间减法。我们只需求
这个状态比较简单,因为这一位只和上一位有关,所以我们设
表示当前pos位前一位是sta的状态保存的非降数字数目。剩下的就是模板稍微改下即可。
int a[20];
int dp[20][10];
int dfs(int pos,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;
if(up < sta) return 0;
int ans = 0;
for(int i = sta;i <= up;++i){
ans += dfs(pos-1,i,limit&&i == a[pos]);
}
if(!limit) dp[pos][sta] = ans;
return ans;
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,true);
}
int main(){
memset(dp,-1,sizeof dp);
int l,r;
while(cin >> l >> r)
cout << solve(r) - solve(l-1) << endl;
}
「一本通 5.3 例 3」Windy 数
题意:
让你求
的
数的个数。
所谓windy数是相邻两个数字的差值不能小于2。
我们设
状态为第
位,前一位为
的符合要求的数字个数。
当前位的数只和上一位有关,注意前导零即可。
int a[20];
int dp[20][12];
int dfs(int pos,int pre,bool lead,bool limit){
if(pos == -1) return 1;
if(!lead&&!limit&&dp[pos][pre]!=-1) return dp[pos][pre];
int up = limit?a[pos]:9;
int tmp = 0;
for(int i = 0;i <= up;++i){
if(lead) tmp += dfs(pos-1,i,lead&&i == 0,limit&&i == a[pos]);
else if(abs(i-pre) >= 2) tmp += dfs(pos-1,i,lead,limit&&i == a[pos]);
}
if(!lead&&!limit) dp[pos][pre] = tmp;
return tmp;
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,true,true);
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int l,r;
cin >> l >> r;
memset(dp,-1,sizeof dp);
cout << solve(r) - solve(l-1)<<endl;
return 0;
}
P3413 SAC#1 - 萌数
题意:
让你找
之间萌数的个数。萌数定义如下:
只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。
现在SOL想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。
思路:
如果萌数存在,则至少存在一位置上的数字,他和前一位或者前2位相等。
所以我们定义
保存状态
第
位
位上的数字
位上的数字
是否已经存在回文
调了很长时间。。。。
char s[N],t[N];
int a[1005];
ll dp[1005][65][65][2];
ll dfs(int pos,int sta1,int sta2,bool sta,bool lead,bool limit){
if(pos == -1) return lead == 0&&sta == 1;
if(!lead&&!limit&&dp[pos][sta1][sta2][sta]!=-1) return dp[pos][sta1][sta2][sta];
int up = limit?a[pos]:9;
ll ans = 0;
for(int i = 0;i <= up;++i){
if(lead&&i == 0) ans = (ans + dfs(pos-1,sta1,sta2,false,true,limit&&i == a[pos])) % mod;
else ans =(ans + dfs(pos-1,sta2,i,sta||(i == sta2||i==sta1),false,limit&&i == a[pos])) % mod;
}
if(!lead&&!limit) dp[pos][sta1][sta2][sta] = ans;
return ans;
}
ll solve(char str[]){
int l = strlen(str);
int pos = 0;
int down = (str[0] == '0'?1:0);
for(int i = l - 1;i >= down;--i){
a[pos++] = str[i] - '0';
}
return dfs(pos-1,11,12,false,true,true);
}
int main(){
cin >> s >> t;
memset(dp,-1,sizeof dp);
int l = strlen(s);
s[l-1]--;
for(int i = l-1;i >= 0;--i){
if(s[i] < '0') s[i-1] --,s[i] += 10;
}
cout << ((solve(t) - solve(s))%mod + mod) % mod;
}
2020牛客寒假算法基础集训营3 牛牛的随机数
答案是
关于 考虑按位贡献,
即对于每
位来说,对答案产生的贡献为:
中0的个数
中1的个数
中1的个数
中0的个数
所以我们可以统计
和
中1的个数。
由于满足区间减法,只需统计
中第
为1的个数的树即可。到这儿很显然就是数位DP了。
不过记得dfs的时候dp数组也要取模,debug真的好累。。。。
int a[100];
ll dp[100][2][100];
inline ll mul(ll a, ll b, ll Mod)
{
ll c = a*b-(ll)((long double)a*b/Mod+0.5)*Mod;
return c<0 ? c+Mod : c; //就是算的a*b%Mod;
}
ll dfs(int pos,bool sta,int k,bool limit){
if(pos == -1) return sta == 1;
if(!limit&&dp[pos][sta][k]!=-1) return dp[pos][sta][k];
int up = limit?a[pos]:1;
ll ans = 0;
for(int i = 0;i <= up;++i) ans += dfs(pos-1,sta||(pos == k&&i),k,limit&&i == a[pos]),ans %= mod;
if(!limit) dp[pos][sta][k] = ans;
return ans;
}
ll solve(ll x,int k){
int pos = 0;
while(x){
a[pos++] = x % 2;
x >>= 1;
}
return dfs(pos-1,0,k,true);
}
long long inv(long long x,long long mod)
{
long long k=mod-2,ans=1;
while(k)
{
if (k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
int main(){
int t;
memset(dp,-1,sizeof dp);
scanf("%d",&t);
while(t--){
ll l1,r1,l2,r2;
scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
ll n1 = r1 - l1 + 1;
ll n2 = r2 - l2 + 1;
ll ans = 0;
for(int i = 0;i <= 60;i++){
ll cnt1 = solve(r1,i) - solve(l1 - 1,i);
ll cnt2 = solve(r2,i) - solve(l2 - 1,i);
ans += mul(mul(cnt1,n2-cnt2,mod),(1LL<<i)%mod,mod);
ans %= mod;
ans += mul(mul(n1-cnt1,cnt2,mod),(1LL<<i)%mod,mod);
ans %= mod;
}
ans = (ans % mod + mod ) % mod;
ans=ans*inv((n1%mod)*(n2%mod)%mod,mod)%mod;
printf("%lld\n",ans);
}
}
数位类统计问题
思路:从高位到低位逐步确定。然后看情况组合。
void init() //预处理组合数
{
C[0][0] = 1;
for (int i = 1; i <= 31; ++i)
{
C[i][0] = C[i - 1][0];
for (int j = 1; j <= i; ++j)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
}
P2518 [HAOI2010]计数
题意:
你有一组非零数字(不一定唯一),你可以在其中插入任意个0,这样就可以产生无限个数。比如说给定{1,2},那么可以生成数字12,21,102,120,201,210,1002,1020,等等。
现在给定一个数,问在这个数之前有多少个数。(注意这个数不会有前导0)
思路:
这个题目是数位DP思想,按位确定状态,然后计数。
比如1020,从高位开始,首先找< 1的数字有没有,有的话,放在这里,然后其余的数字多重集排列计数,然后留这儿原来的数字,继续下一位。
多重集排列:
已知
=
long long C(int x,int y){
long long tot=1;
for(int i=x;i>=x-y+1;i--) tot*=i;
for(int i=y;i>=2;i--) tot/=i;
return tot;
}
ll calAns(ll n){
ll ans = 1;
for(int i = 0;i <= 9;++i)if(b[i]){
ans *= C(n,b[i]);
n -= b[i];
}
return ans;
}
int main(){
cin >> s;
int l = strlen(s);
for(int i = 0;i < l;++i){
a[i] = s[i] - '0';
b[a[i]] ++;
}
ll ans = 0;
for(int i = 0;i < l;++i){
for(int j = 0;j < a[i];++j){
if(b[j]) {b[j]--;ans += calAns(l -i - 1);b[j]++;}
}
b[a[i]] --;
}
cout << ans << endl;
}
#10163. 「一本通 5.3 例 1」Amount of Degrees
题意:
让你求
内满足
的数的个数。
思路:
上述问题满足区间减法。所以我们只需求
满足的条件的数的个数。
把
分解为
进制数。
然后从高位开始
如果当前为是1,先填0,后面的位数可以填
个1,组合数计算累加。然后填1,往下一位走。
如果是0,直接往下位走。
如果大于1,则本位以及以后的位都可以填 1,组合数跳出。(说明我们填的所有数都比
小)
最后注意:可能包含 的情况即可。
int k,b;
int a[50];
int C[32][32];
void init() //预处理组合数
{
C[0][0] = 1;
for (int i = 1; i <= 31; ++i)
{
C[i][0] = C[i - 1][0];
for (int j = 1; j <= i; ++j)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
}
int solve(int x){
int pos = 0;
while(x){
a[pos++] = x % b;
x /= b;
}
int cnt = k;
ll ans = 0;
bool r = 1;
for(int i = pos - 1;i >= 0;i --){
if(a[i] !=0){
if(a[i] == 1) ans += C[i][cnt--];
else {ans += C[i+1][cnt];break;}
}
if(cnt == 0){ans ++; break;}//包含x的情况
}
return ans;
}
int main(){
init();
int l,r;
cin >> l >> r >> k >> b;
cout << solve(r) - solve(l - 1);
}
注:本文是吾看了其他大神的讲解学习数位dp怕有所遗忘所留,如有重复,实为借鉴。