【学时·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. Murano Weekly Meeting 2016.08.02

    Meeting time: 2016.August.02 1:00~2:00 Chairperson:  Valerii Kovalchuk, from Mirantis Meeting summar ...

  2. 基于Python实现邮件发送

    import smtplibfrom email.mime.text import MIMETextemail_host = 'smtp.163.com' # 邮箱地址email_user = 'sz ...

  3. Castle.DynamicProxy的使用

    .Net平台AOP技术研究 简单实现 通过继承实现 public interface ICoding { void DoSth(); } public class Coding : ICoding { ...

  4. No module named Crypto--转

    https://blog.csdn.net/lacoucou/article/details/53152122 背景:win10+python 2.7 在python 中使用AES算法时,会报告上述错 ...

  5. QML 程序运行效率

    同样的程序,在 Windows 下面启动时非常慢,而在 Linux 上启动时很快,一方面是因为 qml 界面的创建耗时不同,另一方面是因为读取文件的用时相差太大导致. On Linux 在 Linux ...

  6. java学习第十五天

    1:对象数组(掌握) (1)数组既可以存储基本数据类型,也可以存储引用类型.它存储引用类型的时候的数组就叫对象数组. (2)案例: 用数组存储5个学生对象,并遍历数组. 2:集合(Collection ...

  7. 导出CSV

    public FileResult ExportExcel() { var sbHtml = new StringBuilder(); sbHtml.Append("<table bo ...

  8. 查看SQL Server中的锁表及解锁

    有时候系统很慢,有可能是SQL Server数据库中某些表被锁定 --查看被锁表(需查多几次,有些临时锁很快会自动解锁): SELECT request_session_id AS spid, OBJ ...

  9. Oracle数据库分割字符串function方法

    下面我直接上传一串代码源码, create or replace function strsplit(p_value varchar2, p_split varchar2 := ',') --usag ...

  10. intellijidea课程 intellijidea神器使用技巧 5-2 localhistory

    Ctrl shift A  ==>localhistory ==> show history 查看文件本地历史记录(idea每次修改在本地会生成历史记录) Ctrl shift A  == ...