模板 - 动态规划 - 数位dp

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[20];
ll dp[20][20/*可能需要的状态1*/][20/*可能需要的状态2*/];//不同题目状态不同
ll dfs(int pos,int state1/*可能需要的状态1*/,int state2/*可能需要的状态2*/,bool lead/*这一位的前面是否为零*/,bool limit/*这一位是否取值被限制(也就是上一位没有解除限制)*/)
//不是每个题都要处理前导零
{
    //递归边界,最低位是0,那么pos==-1说明这个数枚举完了
    if(pos==-1)
        return 1;/*这里返回1,表示枚举的这个数是合法的,那么这里就需要在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。 */
    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
    if(!limit && !lead && dp[pos][state1][state2]!=-1)
        return dp[pos][state1][state2];
    /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态对应*/
    int up=limit?a[pos]:9;//根据limit判断枚举的上界up
    ll ans=0;
    //开始计数
    for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
    {
        int new_state1=???;
        int new_state2=???;
        /*
        计数的时候用continue跳过不合法的状态,不再搜索
        */

        //合法的状态向下搜索
        ans+=dfs(pos-1,new_state1,new_state2,lead && i==0,limit && i==a[pos]);//最后两个变量传参都是这样写的
    }
    //计算完,记录状态
    if(!limit && !lead)
        dp[pos][state1][state2]=ans;
    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    return ans;
}

ll solve(ll x)
{
    //可能需要特殊处理0或者-1
    if(x<=0)
        return ???;

    int pos=0;
    while(x)//把数位分解
    {
        a[pos++]=x%10;//编号为[0,pos),注意数位边界
        x/=10;
    }

    return dfs(pos-1/*从最高位开始枚举*/,0/*可能需要的状态1*/,0/*可能需要的状态2*/,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}

int main()
{
    memset(dp,-1,sizeof(dp));
    //一定要初始化为-1

    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri))
    {
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}

其实另一种计数写法对别的题目有一定的启发性,需要特别注意的是,无论哪种写法的dp结果中存的数字都是和le与ri无关的。所以在数位受限时不能取用计算过的dp值,也不能更新dp值,不受限的情况可以重复利用。

无注释版:

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[20];
ll dp[20][MAXS1][MAXS2];
ll dfs(int pos,int s1,int s2,bool lead,bool limit) {
    if(pos==-1) {
        return ?;
    }
    if(!limit && !lead && dp[pos][s1][s2]!=-1)
        return dp[pos][s1][s2];
    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        int ns1=op1(s1);
        int ns2=op2(s2);
        ans+=dfs(pos-1,ns1,ns2,lead && i==0,limit && i==a[pos]);
    }
    if(!limit && !lead)
        dp[pos][s1][s2]=ans;
    return ans;
}

ll solve(ll x) {
    if(x<=0)
        return ?;

    int pos=0;
    while(x) {
        a[pos++]=x%10;
        x/=10;
    }

    return dfs(pos-1,INITS1,INITS2,true,true);
}

int main() {
    memset(dp,-1,sizeof(dp));

    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri)) {
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}

 


 

一个更简单的模板,去掉了很多奇奇怪怪的东西,比如前导0,前导0的确应该特殊考虑而不能一概而论。

int dfs(int i, int s, bool e) {
    if (i==-1) return s==target_s;
    if (!e && ~f[i][s]) return f[i][s];
    int res = 0;
    int u = e?num[i]:9;
    for (int d = first?1:0; d <= u; ++d)
        res += dfs(i-1, new_s(s, d), e&&d==u);
    return e?res:f[i][s]=res;
}

看起来清爽多了,其中:

f为记忆化数组;

i为当前处理串的第i位(权重表示法,也即后面剩下i+1位待填数);

s为之前数字的状态(如果要求后面的数满足什么状态,也可以再记一个目标状态t之类,for的时候枚举下t);

e表示之前的数是否是上界的前缀(即后面的数能否任意填)。

for循环枚举数字时,要注意是否能枚举0,以及0对于状态的影响,有的题目前导0和中间的0是等价的,但有的不是,对于后者可以在dfs时再加一个状态变量z,表示前面是否全部是前导0,也可以看是否是首位,然后外面统计时候枚举一下位数。

注意:

不满足区间减法性质的话,不能用solve(r)-solve(l-1)。


看了学长的部分博客之后发现其实使用f[i][j][st]表示以j开头的i位数满足条件st的数的个数也是可以的。待更新。

posted @ 2019-03-14 00:01  韵意  阅读(269)  评论(0编辑  收藏  举报