ICPC20上海C——小白也能秒懂的数位dp讲解

dp定义有很多种。采用这个最直观的:添加链接描述

i&j=0,意味着相加不会进位。log2(i+j)+1向下取整,意味着它等于i、j中的最长len值。

接下来为了解决原问题,我们定义一个更为简单的子问题为:
∑ i = 0 X ∑ j = [ i = 0 ] Y [ i & j = 0 ] \sum_{i=0}^X \sum_{j=[i=0]}^Y [i\&j=0] i=0Xj=[i=0]Y[i&j=0]
为什么做此定义?以up1=up2=1(注:对应已知的数位dp模板的up变量,比如hdu2089,见下文“附”)为例:枚举i和j的最高位(使得最高位固定),当两者最高位均为0,则取原问题。否则
[ l o g 2 ( i + j ) + 1 ] [log_2(i+j)+1] [log2(i+j)+1]
已经确定(“[]”表示向下取整),在我的代码里就是idx+1,于是可以把它提到求和式外面,这样就构造出上述更为简单的子问题。

所以我们定义dp[idx,zero]为:当前考虑到下标为idx的位,求的是原问题/子问题(zero=1/0)。之所以变量名叫zero,只是因为一个巧合:该变量恰好和表示i和j的更高位是否全都是0的变量是同一个。

因为原问题的转移方程里有可以提取的公因子,所以我们定义一个权值weight,它等于1或idx+1。下面看weight什么时候等于idx+1。由定义可以知道,如果zero=0,则无论如何权值都只乘1;如果zero=1,表示求解原问题,则i和j不都等于0时,weight=idx+1。

伪代码

    LL ans = 0;
    int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
    rep(i,0,up1){
        rep(j,0,up2){
            if(i & j) continue;
            int weight = zero && (i || j) ? idx+1 : 1;
            (ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,NXTZERO) * weight % mod) %= mod;
        }
    }

接下来确定NXTZERO。由以上讨论已经知道,zero=1(求解原问题)且i和j都为0时,NXTZERO=1。其他情况NXTZERO=0。

NXTZERO = zero && !(i || j)
  • 因为dp定义与test case无关,所以memset只需要在最开始做1次,这一技巧在hdu2089中也用到。
  • 需要减1,因为数位dp的代码把i == 0 且 j == 0的情况也算进去了。

参考已有数位dp模板,有如下代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 40;
const int mod = 1e9 + 7;

int x[N],y[N];
LL dp[N][2];

void dbg(){puts("");}
template<typename T, typename... R>void dbg(const T &f, const R &... r) {
    cout << f << " ";
    dbg(r...);
}
template<typename Type>inline void read(Type &xx){
    Type f = 1;char ch;xx = 0;
    for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
    xx *= f;
}

LL dfs(int idx,int f1,int f2,bool zero){
    if(idx == -1) return 1;
    LL &d = dp[idx][zero];
    if(~d && !f1 && !f2) return d;
    LL ans = 0;
    int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
    rep(i,0,up1){
        rep(j,0,up2){
            if(i & j) continue;
            int weight = zero && (i || j) ? idx+1 : 1;
            (ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,zero && !(i || j)) * weight % mod) %= mod;
        }
    }
    if(!f1 && !f2) d = ans;
    return ans;
}

LL solve(int u,int v){
    int l1 = 0,l2 = 0;
    memset(x,0,sizeof x);memset(y,0,sizeof y);
    for(;u;u >>= 1) x[l1++] = u & 1;
    for(;v;v >>= 1) y[l2++] = v & 1;
    return (dfs(max(l1,l2)-1,1,1,true) + mod-1) % mod;
}

int main(int argc, char** argv) {
    memset(dp,-1,sizeof dp);
    int T;read(T);
    while(T--){
        int u,v;read(u);read(v);
        printf("%lld\n",solve(u,v));
    }
    return 0;
}

可惜它TLE了。这就引出一个优化:定义dp[idx,f1,f2,zero],其中f1和f2是以上代码的f1和f2。它们不实际参与决策,但放在状态里。这是一个空间换时间的技巧。那么

if(~d && !f1 && !f2) return d;

if(!f1 && !f2) d = ans;

就变成了

if(~d) return d;

return d = ans;

相应地,必须在每个case都memset一次。
这就得到我们在参考链接中看到的4维dp,本文指出它本质上是2维的dp+空间换时间技巧。这样就得到一个性能更优(可以AC)的代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 40;
const int mod = 1e9 + 7;

int x[N],y[N];
LL dp[N][2][2][2];

void dbg(){puts("");}
template<typename T, typename... R>void dbg(const T &f, const R &... r) {
    cout << f << " ";
    dbg(r...);
}
template<typename Type>inline void read(Type &xx){
    Type f = 1;char ch;xx = 0;
    for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
    xx *= f;
}

LL dfs(int idx,int f1,int f2,bool zero){
    if(idx == -1) return 1;
    LL &d = dp[idx][f1][f2][zero];
    if(~d) return d;
    LL ans = 0;
    int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
    rep(i,0,up1){
        rep(j,0,up2){
            if(i & j) continue;
            int weight = zero && (i || j) ? idx+1 : 1;
            (ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,zero && !(i || j)) * weight % mod) %= mod;
        }
    }
    return d = ans;
}

LL solve(int u,int v){
    int l1 = 0,l2 = 0;
    memset(x,0,sizeof x);memset(y,0,sizeof y);memset(dp,-1,sizeof dp);
    for(;u;u >>= 1) x[l1++] = u & 1;
    for(;v;v >>= 1) y[l2++] = v & 1;
    return (dfs(max(l1,l2)-1,1,1,true) + mod-1) % mod;
}

int main(int argc, char** argv) {
    int T;read(T);
    while(T--){
        int u,v;read(u);read(v);
        printf("%lld\n",solve(u,v));
    }
    return 0;
}

附:hdu2089的AC代码

2维且memset只做1次的版本

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int SZ = 20;

int dl,d[SZ],dp[SZ][2];

template<typename Type>inline void read(Type &xx){
    Type f = 1;char ch;xx = 0;
    for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
    xx *= f;
}

int dfs(int idx,bool is6,bool isup){
    if(idx == -1) return 1;
    if(!isup && ~dp[idx][is6]) return dp[idx][is6];
    int up = isup ? d[idx] : 9;
    int ans = 0;
    rep(i,0,up){
        if(i == 4) continue;
        if(is6 && i == 2) continue;
        ans += dfs(idx - 1,i == 6,isup && i == up);
    }
    if(!isup) dp[idx][is6] = ans;
    return ans;
}

int solve(int x){
    dl = 0;
    for(;x;x /= 10) d[dl++] = x % 10;
    return dfs(dl - 1,false,true);
}

int main(int argc, char** argv) {
    memset(dp,-1,sizeof dp);
    int a,b;
    while(~scanf("%d%d",&a,&b) && (a || b)){
        printf("%d\n",solve(b) - solve(a - 1));
    }
    return 0;
}

3维且memset做多次的版本

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int SZ = 20;

int dl,d[SZ],dp[SZ][2][2];

template<typename Type>inline void read(Type &xx){
    Type f = 1;char ch;xx = 0;
    for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
    xx *= f;
}

int dfs(int idx,bool is6,bool isup){
    if(idx == -1) return 1;
    int &v = dp[idx][isup][is6];
    if(~v) return v;
    int up = isup ? d[idx] : 9;
    int ans = 0;
    rep(i,0,up){
        if(i == 4) continue;
        if(is6 && i == 2) continue;
        ans += dfs(idx - 1,i == 6,isup && i == up);
    }
    return v = ans;
}

int solve(int x){
    dl = 0;
    for(;x;x /= 10) d[dl++] = x % 10;
    memset(dp,-1,sizeof dp);
    return dfs(dl - 1,false,true);
}

int main(int argc, char** argv) {
    int a,b;
    while(~scanf("%d%d",&a,&b) && (a || b)){
        printf("%d\n",solve(b) - solve(a - 1));
    }
    return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值