博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
数位dp
阅读量:4626 次
发布时间:2019-06-09

本文共 5896 字,大约阅读时间需要 19 分钟。

强烈推荐,写的真的太棒了Orz,(所以下面的都引自这篇博客咳咳

一、模板

数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数。所谓数位dp,是在数位上进行dp,实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。

如果是暴力枚举:

for(int i=le;i<=ri;i++)        if(right(i)) ans++;

而数位dp的话,是控制上界枚举,从最高位开始往下枚举,例如:ri=213,那么我们从百位开始枚举:百位可能的情况有0,1,2(觉得这里枚举0有问题的继续看)

然后每一位枚举都不能让枚举的这个数超过上界213(下界就是0或者1,这个次要),当百位枚举了1,那么十位枚举就是从0到9,因为百位1已经比上界2小了,后面数位枚举什么都不可能超过上界。所以问题就在于:当高位枚举刚好达到上界是,那么紧接着的一位枚举就有上界限制了。具体的这里如果百位枚举了2,那么十位的枚举情况就是0到1,如果前两位枚举了21,最后一位之是0到3(这一点正好对于代码模板里的一个变量limit 专门用来判断枚举范围)。最后一个问题:最高位枚举0:百位枚举0,相当于此时我枚举的这个数最多是两位数,如果十位继续枚举0,那么我枚举的就是个位数咯,因为我们要枚举的是小于等于ri的所以数,当然不能少了位数比ri小的咯!(这样枚举是为了无遗漏的枚举,不过可能会带来一个问题,就是前导零的问题,模板里用lead变量表示,不过这个不是每个题目都是会有影响的,可能前导零不会影响我们计数,具体要看题目)

然后是大神的模板

typedef long long ll;int a[20];ll dp[20][state];//不同题目状态不同ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零{    //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了    if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)    if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];    /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/    int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了    ll ans=0;    //开始计数    for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了    {        if() ...        else if()...        ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的        /*这里还算比较灵活,不过做几个题就觉得这里也是套路了        大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论        去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目        要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,        前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/    }    //计算完,记录状态    if(!limit && !lead) dp[pos][state]=ans;    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/    return ans;}ll solve(ll x){    int pos=0;    while(x)//把数位都分解出来    {        a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行        x/=10;    }    return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛}int main(){    ll le,ri;    while(~scanf("%lld%lld",&le,&ri))    {        //初始化dp数组为-1,这里还有更加优美的优化,后面讲        printf("%lld\n",solve(ri)-solve(le-1));    }}

然后有些优化什么的还是去看原博客吧qwq

二、例题

1.不要62

不要连续的62,所以只需要记录上一位是不是6就可以了,f[pos][sta]表示第pos位,上一位是不是6

#include 
#define eps 0.000001using namespace std;int l,r;int f[20][2],a[20];int dfs(int pos,int pre,int sta,bool limit){ if (pos==-1) return 1; if (!limit && f[pos][sta]!=-1) return f[pos][sta]; int up; if (limit) up=a[pos]; else up=9; int tmp=0; for (int i=0;i<=up;i++) { if (pre==6&&i==2|| i==4) continue; tmp+=dfs(pos-1,i,i==6,limit&&i==a[pos]); } if (!limit) f[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,1);}int main(){ while (scanf("%d%d",&l,&r)&&l+r) { memset(f,-1,sizeof(f)); printf("%d\n",solve(r)-solve(l-1)); } return 0;}
View Code

2. F(x)

#include 
#define eps 0.000001using namespace std;const int N=1e4+50;int T,r,all,A;int f[20][N],a[20];int F(int x){ if (x==0) return 0; int ans=F(x/10); return ans*2+(x%10);}int dfs(int pos,int sum,bool limit){ if (pos==-1) return sum<=all; if (sum>all) return 0; if (!limit && f[pos][all-sum]!=-1) return f[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<
View Code

3.Round Numbers

二进制中0的数量要不少于1的数量的数的个数,f[pos][num],到当前数位pos,0的数量减去1的数量不少于num的方案数,一个简单的问题,中间某个pos位上num可能为负数(这不一定是非法的,因为我还没枚举完嘛,只要最终的num>=0j就可以),因为最小就-32吧,直接加上32,把32当0用。这题主要是要想讲一下lead的用法,显然我要统计0的数量,前导零是有影响的,所以!lead&&!limit才能dp。

#include 
#include
#include
#include
#include
//#include
#define eps 0.000001using namespace std;const int N=1e4+50;int l,r;int f[35][70],a[35];int dfs(int pos,int sta,bool lead,bool limit){ if (pos==-1) return sta>=32; if (!lead && !limit && f[pos][sta]!=-1) return f[pos][sta]; int up= limit? a[pos] :1; int 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 (!lead && !limit) f[pos][sta]=tmp; return tmp;}int solve(int x){ int pos=0; while (x) { a[pos++]=x&1; x>>=1; } return dfs(pos-1,32,1,1);}int main(){ memset(f,-1,sizeof(f)); scanf("%d%d",&l,&r); printf("%d\n",solve(r)-solve(l-1)); return 0;}
View Code

4. self同类分布

一个数是它自己数位和的倍数。

枚举数位和,因为总就162,然后问题就变成了一个数%mod=0,mod是枚举的,想想状态:dp[pos][sum][val],当前pos位上数位和是sum,val就是在算这个数%mod,(从高位算  *10+i),因为我们枚举的数要保证数位和等于mod,还要保证这个数是mod的倍数,很自然就能找到这些状态,显然对于每一个mod,val不能保证状态唯一,所以直接对每一个mod,memset一次。

#include 
#include
#include
#include
#include
//#include
#define eps 0.000001#define ll long longusing namespace std;const int N=1e4+50;int T;ll l,r;ll f[20][170][170];int a[20];ll dfs(int pos,int sum,int val,int mod,bool limit){ if (sum-9*pos-9>0) return 0; if (pos==-1) return sum==0 && val==0; if (!limit && f[pos][sum][val]!=-1) return f[pos][sum][val]; int up= limit? a[pos] :9; ll tmp=0; for (int i=0;i<=up;i++) { if (sum-i<0) break; tmp+=dfs(pos-1,sum-i,(val*10+i)%mod,mod,limit && i==a[pos]); } if (!limit) f[pos][sum][val]=tmp; return tmp;}ll solve(ll x){ int pos=0; while (x) { a[pos++]=x%10; x/=10; } ll ans=0; for (int i=1;i<=pos*9;i++) { memset(f,-1,sizeof(f)); ll tmp=dfs(pos-1,i,0,i,1); ans+=tmp; } return ans;}int main(){ /* scanf("%d",&T); for (int ca=1;ca<=T;ca++) { scanf("%lld",&r); printf("Case %d: %lld\n",ca,solve(r)); }*/ scanf("%lld%lld",&l,&r); printf("%lld\n",solve(r)-solve(l-1)); return 0;}
View Code

 

转载于:https://www.cnblogs.com/tetew/p/9435832.html

你可能感兴趣的文章
极其蛋疼的if else 中的break用法
查看>>
Map集合
查看>>
C#/java 执行oracle package
查看>>
程序面试试题
查看>>
Wall POJ - 1113 凸包模板
查看>>
leetcode算法: Find Bottom Left Tree Value
查看>>
python opencv3 grabcut前景检测
查看>>
内容安全策略(CSP)_防御_XSS_攻击的好助手
查看>>
获取URL中的参数
查看>>
宝塔面板安装swoole扩展
查看>>
HDOJ_1061_Rightmost Digit
查看>>
【小笨鸟看JDK1.7集合源码之三】LinkedList源码剖析
查看>>
bfs,dfs区别
查看>>
Javascript端加密java服务端解密
查看>>
xml文件中引号如何处理
查看>>
Centos 下 Jenkins2.6 + Git + Maven Shell一件部署与备份
查看>>
MVC原理
查看>>
Java中堆内存和栈内存详解
查看>>
网络编程
查看>>
C# 访问USB(HID)设备方法 (转)
查看>>