数位dp

强烈推荐https://blog.csdn.net/wust_zzwh/article/details/52100392,写的真的太棒了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.hdu 2089 不要62

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

#include <bits/stdc++.h>
#define eps 0.000001
using 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.hdu 4734 F(x)

#include <bits/stdc++.h>
#define eps 0.000001
using 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<<pos),limit&&i==a[pos]);
    }
    if (!limit) f[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()
{
    scanf("%d",&T);
    memset(f,-1,sizeof(f));
    for (int ca=1;ca<=T;ca++)
    {
        scanf("%d%d",&A,&r);
        all=F(A);
        printf("Case #%d: %d\n",ca,solve(r));
    }
    return 0;
}
View Code

3.poj 3252 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 <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
//#include <bits/stdc++.h>
#define eps 0.000001
using 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.bzoj 1799 self同类分布

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

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

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
//#include <bits/stdc++.h>
#define eps 0.000001
#define ll long long
using 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

 

posted @ 2018-08-07 11:10  特特w  阅读(198)  评论(0编辑  收藏  举报