【学时·IV】 数位DP


■基本策略■

说白了就是超时和不超时的区别 ;)

有一些特别的题与数位有关,但是用一般的枚举算法会超时。这时候就有人提出了——我们可以用动态规划!通过数字前一位和后一位之间的关系,逐渐推导出所有数位上的值作为初始化(也有些不是),实现大部分计数问题的高效解决。

主要题型大概就是求 Min~Max 之间满足条件 E() 的数的个数,这里使用了前缀和的思想,即 F[ij]=F[0 j]-F[0~(i-1)]。一般是将 F[] 初始化,但是针对某些特别题型,比如 E() 针对不同的数据不同,这时候需要对每一个数据单独求解。


■一般的计数问题■

在十进制里做DP ♪(´▽`)

◆入门练手◆ 不要62

这是一道基本上称得上“版题”的基础题。先给出m,n,也就是 Min,Max 。这里的 E(n) 则是 n不含4或62。

先给出一个最基本的结论:

若A < B,则A、B的十进制表示 {an...a2a1a0}、{bn...b2b1b0}必存在 ar=br(k < r < n)且 ak < bk

接下来是动态规划的初始化——令F[i][j]表示第i位是j的数的方案总数。当满足第i位(j)不为4,第i-1位(k)不为2或者i-1位为2但是第i位不为6:

若当前枚举到第i位,第i位数字为j,第i-1位数字为k

当且仅当j≠4且k≠2或者k=2但j≠6时,有dp[i][j]=sum(dp[i-1][k])

通过三层从小到大的循环完成i,j,k的枚举。

这是我们继续建立计算的基础——先用dig[]储存m、n的十进制表示,然后从高位开始枚举0~dig[i]-1,保证枚举出的数小于给出的数,且。设当前枚举位为j,如果满足 E() ,就在答案中加上 F[i][j]。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int F[15][15];
void GetDig(int num,int dig[])
{
do{
dig[++dig[0]]=num%10;
num/=10;
}while(num);
}
void GetF()
{
F[0][0]=1;
for(int i=1;i<=7;i++)
for(int j=0;j<10;j++)
for(int k=0;k<10;k++)
if((j!=4) && !(j==6 && k==2))
F[i][j]+=F[i-1][k];
}
int GetAns(int num,int dig[])
{
int ret=0;
for(int i=dig[0];i>0;i--)
{
for(int j=0;j<dig[i];j++)
{
if((j!=4) && !(j==2 && dig[i+1]==6))
ret+=F[i][j];
}
if((dig[i]==4) || (dig[i]==2 && dig[i+1]==6))
break;
}
return ret;
}
int main()
{
GetF();
int m,n;
while(~scanf("%d%d",&n,&m) && n && m)
{
int dign[15]={},digm[15]={};
GetDig(n,dign);GetDig(m+1,digm);
int ansn=GetAns(n,dign),ansm=GetAns(m+1,digm);
printf("%d\n",ansm-ansn);
}
return 0;
}

◆有点意思◆ B-number

这道题做不做的出来,你心里难道就没有一点B数吗?

每个人心中都有一个B数(向yhn大佬致敬)。这道题其实是上一个题的升级版。

万事开头难,先看看状态定义:

dp[i][j][k] (满足0 ≤ i < len,0 ≤ j < 13,0 ≤ k < 3)

表示第i位时当前数模13余j数中,满足条件E(k)的数的个数;

E(k):k=0时,不存在13;k=1时,不存在13但是存在3;k=2时,不存在1或3

这里的第三维其实也可以用k表示当前数位的数字,但是显然可以更加优化!有时候只需要储存一些会对答案产生影响的条件作为一维。虽然减少了空间消耗,但是作为代价,思维的复杂程度和转移方程的错误率也相应增加,初学者还是不要尝试这种方法。(`・ω・´)

由于这道题有多组数据(o(゚Д゚)っ!),而且每一组数据相应的dp数组值不一样。这就意味着要针对每一组数据单独进行一次DP求解——这是数位DP中较特殊的题型。

与一般数位DP相同,我们仍需考虑n的数位。但是因为题目默认是1~n,就没有必要进行前缀和的操作了,我们可以同时考虑数位和DP转移,但是DP参数就会更复杂:

ll DP(int pos,int mod,int lst,bool lim,bool flg) //你没有看错,是long long
/*
pos: 现在枚举到的位置
mod: 当前数模13的余数
lst: 上一个数位的数字
lim: 是否达到数位限制*
flg: 当前是否已经存在13
*/

*:

什么叫数位限制?

我们令n的10进制表示为dig[0len-1]、枚举的数字m的10进制表示为fdig[0len-1],若我们现在从最高位枚举到第k位,满足dig[r]==fdig[r](k < r < len),则称第k数位的枚举受数位限制,此时的fdig[k]只能取 0dig[k];如果不受数位限制,则fdig[k]可取09!

总而言之,有了数位限制才能控制枚举出的数是小于等于n的数!

特别声明:数的最高位总受数位限制

于是我们发现,dp[][][] 只能储存当前位不受数位限制的答案,所以当前位受限制时不能将答案储存在dp里,同样,在记忆化搜索的记忆性返回的时候要保证现在不受数位限制。

接下来就是转移方程了:

令tot=sum(dp[pos-1][(mod*10+1)%mod][]),第三维由记忆化的返回值决定;

当flgtrue时,dp[pos][mod][0]=tot;

当flagfalse且lst1时,dp[pos][mod][1]=tot;

当flagfalse且lst!=1时,dp[pos][mod][2]=tot;

我知道看上面的这么多内容很难理解,所以下面放代码啦。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
#define MOD 13
ll dp[40][13][3];
//[i][j][k]:k=2 - no 1 or 3; k=1 - no 13 but 3; k=0 no 13
int num[15];
ll DP(int pos,int mod,int lst,bool lim,bool flg)
{
if(pos<0)
{
if(flg && !mod) return 1ll;
else return 0ll;
}
if(dp[pos][mod][0]!=-1 && !lim && flg) return dp[pos][mod][0];
if(dp[pos][mod][1]!=-1 && !lim && lst==1 && !flg) return dp[pos][mod][1];
if(dp[pos][mod][2]!=-1 && !lim && lst!=1 && !flg) return dp[pos][mod][2];
int limit=lim? num[pos]:9;
ll tot=0;
for(int i=0;i<=limit;i++)
{
bool lflg=(flg||(lst==1 && i==3)),llim=(lim && (i==limit));
tot+=DP(pos-1,(mod*10+i)%MOD,i,llim,lflg);
}
if(!lim)
{
if(flg) dp[pos][mod][0]=tot;
if(!flg && lst==1) dp[pos][mod][1]=tot;
if(!flg && lst!=1) dp[pos][mod][2]=tot;
}
return tot;
}
ll GetAns(int n)
{
int len=0;
memset(num,0,sizeof num);
while(n)
num[len++]=n%10,n/=10;
return DP(len-1,0,0,true,false);
}
int main()
{
memset(dp,-1,sizeof dp);
int n;
while(~scanf("%d",&n))
printf("%lld\n",GetAns(n));
return 0;
}

■异进制的世界■

◆预备烧脑◆ Amount of Degrees

  • URAL - 1057
  • 【解析】

    其实就是求区间[X,Y]中有多少个数能表示成一个B进制的01数串,并且1的个数恰好为K(比如 B=3,K=2 时 101(3)=32+30=10(10))。

    求区间中的个数仍然是使用前缀和的思想。

    但是由于B进制并不方便思考,所以将B进制强制转换为2进制(重点:将除10进制外的所有进制按照2进制强制处理),这也是为什么题目给的Y是以2的幂为上限(Y≤2^31-1)。

    定义 F[i][j] 为满足条件的长度为i,包含j个1的二进制数。容易得到转移方程:

F[i][j]=F[i-1][j]+F[i-1][j-1]

之后就是处理答案。也就是GetAns(),其实和dig[]的思路一样,只是因为用2进制储存,还要比十进制更简单——只需要判断当前位是0还是1就可以了。

  • 【源代码】
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int F[40][40];
void init()
{
F[0][0]=1;
for(int i=1;i<40;i++)
{
F[i][0]=F[i-1][0];
for(int j=1;j<=i;j++)
F[i][j]=F[i-1][j]+F[i-1][j-1];
}
}
int GetAns(int num,int mul,int mod)
{
int sit[40]={},siz=1,ret=0;
while(num) sit[siz++]=num%mod,num/=mod;
for(int i=siz-1;i>0 && mul>=0;i--)
{
if(sit[i]>1) {ret+=F[i-1][mul-1]+F[i-1][mul];break;}
if(sit[i]==1) ret+=F[i-1][mul],mul--;
}
return ret;
}
int main()
{
init();
int x,y,k,b;
scanf("%d%d%d%d",&x,&y,&k,&b);
int ans1=GetAns(y+1,k,b),ans2=GetAns(x,k,b);
printf("%d\n",ans1-ans2);
return 0;
}

The End

Thanks for reading!

-Lucky_Glass

【学时总结】 ◆学时·IV◆ 数位DP的更多相关文章

  1. 数位dp——牛客多校H

    /* x[1,A] y[1,B] x^y<C 或 x&y>C 把ABC拆成二进制后按位进行数位dp dp[pos][s1][s2][f1][f2] 表示从高到低第pos位,条件一状 ...

  2. 【BZOJ1662】[Usaco2006 Nov]Round Numbers 圆环数 数位DP

    [BZOJ1662][Usaco2006 Nov]Round Numbers 圆环数 Description 正如你所知,奶牛们没有手指以至于不能玩"石头剪刀布"来任意地决定例如谁 ...

  3. bzoj1026数位dp

    基础的数位dp 但是ce了一发,(abs难道不是cmath里的吗?改成bits/stdc++.h就过了) #include <bits/stdc++.h> using namespace ...

  4. uva12063数位dp

    辣鸡军训毁我青春!!! 因为在军训,导致很长时间都只能看书yy题目,而不能溜到机房鏼题 于是在猫大的帮助下我发现这道习题是数位dp 然后想起之前讲dp的时候一直在补作业所以没怎么写,然后就试了试 果然 ...

  5. HDU2089 不要62[数位DP]

    不要62 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  6. 数位DP GYM 100827 E Hill Number

    题目链接 题意:判断小于n的数字中,数位从高到低成上升再下降的趋势的数字的个数 分析:简单的数位DP,保存前一位的数字,注意临界点的处理,都是套路. #include <bits/stdc++. ...

  7. 数位dp总结

    由简单到稍微难点. 从网上搜了10到数位dp的题目,有几道还是很难想到的,前几道基本都是模板题,供入门用. 点开即可看题解. hdu3555 Bomb hdu3652 B-number hdu2089 ...

  8. 数位DP入门

    HDU 2089 不要62 DESC: 问l, r范围内的没有4和相邻62的数有多少个. #include <stdio.h> #include <string.h> #inc ...

  9. 数位DP之奥义

    恩是的没错数位DP的奥义就是一个简练的dfs模板 int dfs(int position, int condition, bool boundary) { ) return (condition ? ...

随机推荐

  1. 硬盘和显卡的访问与控制(二)——《x86汇编语言:从实模式到保护模式》读书笔记02

    上一篇博文我们讲了如何看到实验结果,这篇博文我们着重分析源代码. 书中作者为了说明原理,约定了一种比较简单地用户程序头部格式,示意图如下(我参考原书图8-15绘制的,左边的数字表示偏移地址): 所以, ...

  2. Java学习笔记--继承和多态(下)

    1.通过继承来开发超类(superclass) 2.使用super 关键词唤起超类的构造方法 3.在超类中覆盖方法 4.区分override和overload 5.在Object类中探索toStrin ...

  3. 数据库存储的数据(如果是<p>数据</p>或者<img src="" />等格式)(asp.net)

    1.用方法@Html.Raw() @Html.Raw(数据库提取的数据放在这里)

  4. Mysql 求时间 between 昨天 and 上个月的今天 等时间函数

    问题: Mysql表中一列create_time,类型datetime(YYYY-MM-DD HH:MM:SS),想获取上个月今天到昨天的数据. select * from 表名 where date ...

  5. 动态LINQ(Lambda表达式)构建

    using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; us ...

  6. PHP中函数的定义与使用

    函数是什么? 函数是一个被命名的.独立的代码段,它执行特定的任务,并可能给调用它的程序返回一个值. 函数是被命名的,每个函数都有唯一的名称. 函数是独立的,无需程序其他部分干预,函数便能执行自己的任务 ...

  7. JavaSE集合基础总览

    Java集合 Java集合,也称之为容器.基本上你写所有的Java程序,都必须要用到一个包.该API基本都位于java.util工具类包中,是JavaSE中的重中之重.简单可以总结为“1136”,分别 ...

  8. HTML头部元素实例

    HTML head 元素 标签 描述 <head> 定义了文档的信息 <title> 定义了文档的标题 <base> 定义了页面链接标签的默认链接地址 <li ...

  9. 在Git上创建新分支(实用性高!!!)

    在github上创建仓库: Create a new repository on the command line touch README.md git init git add README.md ...

  10. Android OS Startup

    OS puts emphases on how to provide interfaces to user's APPs for using hardware device in the conven ...