【AcWing算法提高课】1.8数位DP

一、度的数量

1081.度的数量 题目链接

X X X 恰好等于 K K K 个互不相等的 B B B 的整数次幂之和即数 X X X B B B 进制表示下有 K K K 位是 1 1 1,其余位均为 0 0 0

数位DP的技巧:

  1. 题目要求区间 [ l , r ] [l,r] [l,r] 中满足性质的数的个数,用 d p ( n ) dp(n) dp(n) 求出区间 [ 0 , n ] [0,n] [0,n] 中满足性质的数的个数,则答案即为 d p ( r ) − d p ( l − 1 ) dp(r)-dp(l-1) dp(r)dp(l1)
  2. 按照树的形式,对区间 [ 0 , n ] [0,n] [0,n] 中的数从高到低位分解,一层一层处理 (如下图所示),一般每一层按照填 0 ~ a i − 1 0~a_i-1 0ai1 与填 a i a_i ai 分成两种情况,前者可以直接预处理求出结果,后者需要继续讨论下一位的选法。注意需要特判最后一层的右子节点是否满足性质。

设数 n n n B B B 进制表示下有 m m m 位,第 i   ( 0 ≤ i < m ) i\ (0\le ii (0i<m) 位是 a i a_i ai (从低到高分别为第 0 0 0 位、第 1 1 1 位…第 m − 1 m-1 m1 位),对于区间 [ 0 , n ] [0,n] [0,n] 中的数作以下分析:

当前处理到第 i i i 位时,且前所有位上共有 l a s t last last 1 1 1 时:

  1. a i > 0 a_i>0 ai>0,那么这一位可以填 0 0 0,剩下 i − 1 i-1 i1 位还能填 K − l a s t K-last Klast 1 1 1,其余为 0 0 0,这样的方案数有 C i − 1 K − l a s t C_{i-1}^{K-last} Ci1Klast
  2. a i > 1 a_i>1 ai>1,那么这一位可以填 1 1 1,剩下 i − 1 i-1 i1 位还能填 K − l a s t − 1 K-last-1 Klast1 1 1 1,其余为 0 0 0,这样的方案数有 C i − 1 K − l a s t − 1 C_{i-1}^{K-last-1} Ci1Klast1;此外,由于满足性质的数在 B B B 进制表示下只会有 0 0 0 1 1 1,因此这一位不能再填别的数了,直接返回
  3. a i = 1 a_i=1 ai=1,还需要对下一位继续讨论, l a s t + = 1 last+=1 last+=1

上述全部都是叶子节点为左子结点的情况,最后还要特判树的右支是否满足性质。

【AcWing算法提高课】1.8数位DP_第1张图片
对于组合数 C n m C_n^m Cnm,我们可以根据递推公式 C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1 ,通过两重循环预处理。

代码实现:

#include 
#include 
#include 
using namespace std;

const int N = 35;

int K, B;
int f[N][N];

void init(){  //预处理组合数
    for (int i = 0; i < N; i ++)
        for (int j = 0; j <= i; j ++){
            if (!j) f[i][j] = 1;
            else f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
        }
}

int dp(int n){
    if (!n) return 0;  //0不满足性质,返回0
    
    vector <int> nums;
    while (n) nums.push_back(n % B), n /= B;  //提取n在B进制表示下的每一位
    
    int res = 0, last = 0;  //res存储答案数,last存储在前所有位中1的个数
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        if (x){  //如果当前位不是0,则这一位一定可以填0,答案加上剩下i位中填K-last个1,剩下全为0的方案数
            res += f[i][K - last];
            if (x > 1){  //如果当前位大于1,则这一位一定可以填1,答案加上剩下i位中填K-last-1个1,剩下全为0的方案数
                if (K - last - 1 >= 0) res += f[i][K - last - 1];
                break;  //由于满足性质的数在B进制表示下不会出现除0,1以外的数,所以剩下数均不满足性质,直接返回
            }
            else{
                last ++;  //当前位是1,还要继续往下讨论,1的个数+1
                if (last > K) break;  //如果1的个数超过上限,返回
            }
        }
        
        if (!i && last == K) res ++;  //判断n是否满足性质
    }
    
    return res;
}

int main(){
    init();
    
    int l, r;
    scanf("%d %d %d %d", &l, &r, &K, &B);
    
    printf("%d", dp(r) - dp(l - 1));
    
    return 0;
}

二、数字游戏

1082.数字游戏 题目链接

先来考虑左支能够直接算出方案数的部分。
f [ i , j ] f[i,j] f[i,j] 表示共有 i i i 位,且最高位是 j j j 的“从左到右位数字呈非下降关系”的数的个数,有:
f [ i , j ] = ∑ k = j 9 f [ i − 1 , k ] f[i,j]=\sum\limits_{k=j}^9 f[i-1,k] f[i,j]=k=j9f[i1,k]

与上题的分析方法类似,当前处理到第 i i i 位,且上一位数字是 l a s t last last 时:

  1. a i < l a s t a_iai<last,之后每一位无论怎么选都不可能构成“非下降”关系,直接返回
  2. a i ≥ l a s t a_i\ge last ailast,这一位可以填 l a s t ~ a i last~a_i lastai,对于每一个数字 k k k 满足 l a s t ≤ k ≤ a i last\le k\le a_i lastkai,方案数累加上 f [ i , k ] f[i,k] f[i,k],同时记得更新 l a s t = a i last=a_i last=ai

代码实现:

#include 
#include 
#include 
using namespace std;

const int N = 15;

int f[N][N];

void init(){
    for (int i = 0; i <= 9; i ++) f[1][i] = 1;
    
    for (int i = 2; i < N; i ++)
        for (int j = 0; j <= 9; j ++)
            for (int k = j; k <= 9; k ++)
                f[i][j] += f[i - 1][k];
}

int dp(int n){
    if (!n) return 1;  //0也是满足性质的数
    
    vector <int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0, last = 0;
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        for (int j = last; j < x; j ++)
            res += f[i + 1][j];
            
        if (x < last) break;
        last = x;
        
        if (!i) res ++;
    }
    
    return res;
}

int main(){
    init();
    
    int l, r;
    while (~scanf("%d %d", &l, &r))
        printf("%d\n", dp(r) - dp(l - 1));
    
    return 0;
}

三、Windy数

1083.Windy数 题目链接

在前两道题中,一个数的前导零对该数是否满足性质没有影响。但在本题中,如果存在前导零,如果没有特判的话,会导致下一位只能从 2 2 2 开始填。因此本题中对有前导零的数单独求解,用树的结构求首位非零的所有满足方案的数的个数。

先来考虑所有左支的情况。设 f [ i , j ] f[i,j] f[i,j] 表示共有 i i i 位,且最高位是 j j j 的所有是Windy数的数的个数,有:
f [ i , j ] = ∑ k = 0 , 1 , . . . , 9 , ∣ k − j ∣ ≥ 2 f [ i − 1 , k ] f[i,j]=\sum\limits_{k=0,1,...,9,|k-j|\ge 2}f[i-1,k] f[i,j]=k=0,1,...,9,kj2f[i1,k]
注意初始化时要有 f [ 1 , 0 ] = 1 f[1,0]=1 f[1,0]=1,因为最后一位是 0 0 0 的方案数会从这个状态转移。

对于所有含有前导零的数,枚举数的位数和最高位,可以通过 f f f 数组直接得到方案数。

对于首位非零的数,按照树的结构,从高位到低位求解。当前处理到第 i i i 位,且上一位数字是 l a s t last last 时:
这一位可以填在 0 ~ a i − 1 0~a_i-1 0ai1之间 且与 l a s t last last 作差的绝对值不小于 2 2 2 的数,如果 ∣ a i − l a s t ∣ < 2 |a_i-last|<2 ailast<2,那么之后的所有数都不会是Windy数了,直接返回,否则继续枚举下一位,记得更新 l a s t = a i last=a_i last=ai l a s t last last 的初始值必须 ≤ − 1 \le -1 1 ≥ 11 \ge 11 11,为了确保第一位能取到 1 ~ 9 1~9 19

代码实现:

#include 
#include 
#include 
using namespace std;

const int N = 11;

int f[N][10];

void init(){
    for (int i = 0; i <= 9; i ++) f[1][i] = 1;
    
    for (int i = 2; i < N; i ++)
        for (int j = 0; j <= 9; j ++)
            for (int k = 0; k <= 9; k ++)
                if (abs(j - k) >= 2)
                    f[i][j] += f[i - 1][k];
}

int dp(int n){
    if (!n) return 0;
    
    vector <int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0, last = -2;
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        for (int j = i == nums.size() - 1; j < x; j ++)
            if (abs(j - last) >= 2)
                res += f[i + 1][j];
        
        if (abs(x - last) >= 2) last = x;
        else break;
        
        if (!i) res ++;
    }
    
    for (int i = 1; i < nums.size(); i ++)
        for (int j = 1; j <= 9; j ++)
            res += f[i][j];
    
    return res;
}

int main(){
    init();
    
    int l, r;
    scanf("%d %d", &l, &r);
    printf("%d", dp(r) - dp(l - 1));
    
    return 0;
}

四、数字游戏II

1084.数字游戏II 题目链接

题目要求各位数字之和 m o d   N mod\ N mod N 0 0 0,容易想到用额外一维状态表示当前各位数字之和 m o d   N mod\ N mod N 的结果。设 f [ i , j , k ] f[i,j,k] f[i,j,k] 表示共有 i i i 位,最高位为 j j j,且各位数字之和 m o d   N = k mod\ N=k mod N=k 的数的个数,有:
f [ i , j , k ] = ∑ x = 0 9 f [ i − 1 , x , ( k − x )   m o d   N ] f[i,j,k]=\sum\limits_{x=0}^9f[i-1,x,(k-x)\ mod\ N] f[i,j,k]=x=09f[i1,x,(kx) mod N]

当前处理到第 i i i 位,且之前各位数字之和为 l a s t last last 时:方案数加上 ∑ x = 0 a i f [ i , x , − l a s t   m o d   N ] \sum\limits_{x=0}^{a_i}f[i,x,-last\ mod\ N] x=0aif[i,x,last mod N],同时继续枚举下一位数, l a s t + = a i last+=a_i last+=ai。最后特判 n n n 是否满足性质。

代码实现:

#include 
#include 
#include 
using namespace std;

const int N = 11, M = 110;

int P;
int f[N][10][M];

int mod(int x, int y){  //保证取模后的结果是正数
    return (x % y + y) % y;
}

void init(){
    memset(f, 0, sizeof f);
    
    for (int i = 0; i <= 9; i ++)
        f[1][i][i % P] ++;
        
    for (int i = 2; i < N; i ++)
        for (int j = 0; j <= 9; j ++)
            for (int k = 0; k < P; k ++)
                for (int x = 0; x <= 9; x ++)
                    f[i][j][k] += f[i - 1][x][mod(k - j, P)];
}

int dp(int n){
    if (!n) return 1;
    
    vector <int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0, last = 0;
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        for (int j = 0; j < x; j ++)
            res += f[i + 1][j][mod(-last, P)];
        
        last += x;
        
        if (!i && last % P == 0) res ++;   
    }
    
    return res;
}

int main(){
    int l, r;
    while (~scanf("%d %d %d", &l, &r, &P)){
        init();
        
        printf("%d\n", dp(r) - dp(l - 1));
    }
        
    return 0;
}

五、不要62

1085.不要62 题目链接

代码实现:

#include 
#include 
#include 
using namespace std;

const int N = 11;

int f[N][10];

void init(){
    for (int i = 0; i <= 9; i ++)
        if (i != 4)
            f[1][i] = 1;
            
    for (int i = 2; i < N; i ++)
        for (int j = 0; j <= 9; j ++){
            if (j == 4) continue;
            for (int k = 0; k <= 9; k ++){
                if (k == 4 || j == 6 && k == 2) continue;
                f[i][j] += f[i - 1][k];
            }
        }
}

int dp(int n){
    if (!n) return 1;
    
    vector <int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0, last = 0;
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        for (int j = 0; j < x; j ++){
            if (j == 4 || last == 6 && j == 2) continue;
            res += f[i + 1][j];
        }
        
        if (x == 4 || last == 6 && x == 2) break;
        last = x;
        
        if (!i) res ++;
    }
    
    return res;
}

int main(){
    init();
    
    int l, r;
    while(scanf("%d %d", &l, &r), l && r)
        printf("%d\n", dp(r) - dp(l - 1));
        
    return 0;
}

六、恨7不成妻

1086.恨7不成妻 题目链接

本题的难点不在于数要满足三个性质,而在于题目要求所有满足性质的数的平方和

考虑在已知共有 i − 1 i-1 i1 位的所有满足性质的数的平方和时,如何转移至共有 i i i 位且最高位为 j j j 的所有满足性质的数的平方和。设有 i − 1 i-1 i1 位的满足性质的数有 t t t 个, A k   ( 1 ≤ k ≤ t ) A_k\ (1\le k\le t) Ak (1kt) 是一个有 i − 1 i-1 i1 位的满足性质的数,那么有:

∑ k = 1 t ( j A k ‾ ) 2 = ∑ k = 1 t ( j × 1 0 i − 1 + A k ) 2 = ∑ k = 1 t A k 2 + 2 × ∑ k = 1 t A k × ( j × 1 0 i − 1 ) + t × ( j × 1 0 i − 1 ) 2 \sum\limits_{k=1}^t(\overline{jA_k})^2=\sum\limits_{k=1}^t(j\times 10^{i-1}+A_k)^2=\sum\limits_{k=1}^tA_k^2+2\times \sum\limits_{k=1}^tA_k\times (j\times 10^{i-1})+t\times (j\times 10^{i-1})^2 k=1t(jAk)2=k=1t(j×10i1+Ak)2=k=1tAk2+2×k=1tAk×(j×10i1)+t×(j×10i1)2

其中已知 ∑ k = 1 t A k 2 \sum\limits_{k=1}^tA_k^2 k=1tAk2,但仅知道平方和是不够的,我们还需要 ∑ k = 1 t A k \sum\limits_{k=1}^tA_k k=1tAk t t t 才能完成状态转移。

基于上述分析,我们可以设计状态 f [ i , j , a , b , s ] f[i,j,a,b,s] f[i,j,a,b,s] 代表共有 i i i 位,最高位为 j j j,数模 7 7 7 a a a,各位数字之和模 7 7 7 b b b 的所有数的数据 ( s = 0 s=0 s=0 代表个数, s = 1 s=1 s=1 代表和, s = 2 s=2 s=2 代表平方和)。

代码实现:

#include 
#include 
using namespace std;

typedef long long LL;

const int N = 20, P = 1e9 + 7;

struct F{
    int s0, s1, s2;
}f[N][10][7][7];

int power7[N], power9[N];

int mod(LL x, int y){
    return (x % y + y) % y;
}

void init(){
    for (int i = 0; i <= 9; i ++){
        if (i == 7) continue;
        auto &v = f[1][i][i % 7][i % 7];
        v.s0 ++;
        v.s1 += i;
        v.s2 += i * i;
    }
    
    LL power = 10;
    for (int i = 2; i < N; i ++, power *= 10)
        for (int j = 0; j <= 9; j ++){
            if (j == 7) continue;
            for (int a = 0; a < 7; a ++)
                for (int b = 0; b < 7; b ++)
                    for (int k = 0; k <= 9; k ++){
                        if (k == 7) continue;
                        auto &v1 = f[i][j][a][b], 
                             &v2 = f[i - 1][k][mod(a - j * power, 7)][mod(b - j, 7)];
                        v1.s0 = mod(v1.s0 + v2.s0, P);
                        v1.s1 = mod(v1.s1 + v2.s1 + j * (power % P) % P * v2.s0, P);
                        v1.s2 = mod(v1.s2 + v2.s2 + 2 * j * (power % P) % P * v2.s1 + j * j * (power % P) % P * (power % P) % P * v2.s0, P);
                    }
        }
    
    power7[0] = power9[0] = 1;
    for (int i = 1; i < N; i ++){
        power7[i] = power7[i - 1] * 10 % 7;
        power9[i] = power9[i - 1] * 10ll % P;
    }
}

F get(int i, int j, int a, int b){
    int s0 = 0, s1 = 0, s2 = 0;
    for (int x = 0; x < 7; x ++)
        for (int y = 0; y < 7; y ++){
            if (x == a || y == b) continue;
            auto v = f[i][j][x][y];
            s0 = (s0 + v.s0) % P;
            s1 = (s1 + v.s1) % P;
            s2 = (s2 + v.s2) % P;
        }
    return {s0, s1, s2};
}

int dp(LL n){
    if (!n) return 0;
    
    LL backup_n = n % P;
    vector <int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0;
    LL last_a = 0, last_b = 0;
    for (int i = nums.size() - 1; i >= 0; i --){
        int x = nums[i];
        for (int j = 0; j < x; j ++){
            if (j == 7) continue;
            int a = mod(-last_a * power7[i + 1], 7);
            int b = mod(-last_b, 7);
            auto v = get(i + 1, j, a, b);
            res = mod(res + (last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P + 
                      2 * last_a % P * power9[i + 1] % P * v.s1 + v.s2, P);
        }
        
        if (x == 7) break;
        last_a = last_a * 10 + x;
        last_b += x;
        
        if (!i && last_a % 7 && last_b % 7) res = (res + backup_n * backup_n) % P;
    }
    
    return res;
}

int main(){
    init();
    
    int T;
    scanf("%d", &T);
    while (T --){
        LL l, r;
        scanf("%lld %lld", &l, &r);
        printf("%d\n", mod(dp(r) - dp(l - 1), P));
    }
    
    return 0;
}

你可能感兴趣的:(AcWing算法提高课,算法,动态规划,c++)