Variety of Digits (数位dp)

Variety of Digits

[Link](F - Variety of Digits (atcoder.jp))

题意

给你一个数字 n n n,和 m m m个一位数,问 1 1 1到 n n n中数位中含有这 m m m个数字的数的 s u m sum sum是多少。

思路

考虑数位 d p dp dp,从前往后枚举每一位,对每一位的合法填法进行分类讨论转移,进而不重不漏的划分集合,算贡献。

假设当前数为 a b c d e abcde abcde,我们枚举到了第三位,则将当前的集合划分为:

  1. 前缀属于 [ 1 , a b − 1 ] [1,ab-1] [1,ab−1]这样第三位可以填 [ 0 , 9 ] [0,9] [0,9],因为前缀小于 a b ab ab所以第三位填什么都不会导致后面大于 a b c d e abcde abcde
  2. 前缀属于 0 0 0,这样第三位可以填 [ 1 , 9 ] [1,9] [1,9],因为前缀为 0 0 0小于 a b ab ab,第三位可以随便填,但是 0 0 0没有意义所以只能填 [ 1 , 9 ] [1,9] [1,9]
  3. 前缀属于 a b ab ab即这个数,那么第三位可以填 [ 0 , c − 1 ] [0,c-1] [0,c−1],因为要在范围内所以填 [ 0 , c − 1 ] [0,c-1] [0,c−1]都合法。为什么不填 c c c呢?因为我们填 c c c这个情况会被我们用前缀记录下来。(这样下次转移的时候我的状态才是可以随便选的

​ 这里用状态压缩来记录这 m m m个数,假设到了第 i i i位,我需要知道前 i − 1 i-1 i−1位的不同状态的和,转移的话枚举当前这一位填什么,因为多了一位所以前 i − 1 i-1 i−1位的和要 × 10 \times 10 ×10,发现我们不知道这一位填的这个数可以接到多少个不同的方案后面,因此还要维护一个前 i − 1 i-1 i−1位不同方案的数目。

状态表示为: f c [ i ] [ j ] : 填 了 前 i 位 且 状 态 为 j 的 方 案 数 , f s [ i ] [ j ] : 填 了 前 i 位 且 状 态 为 j 的 所 有 方 案 的 总 和 fc[i][j]:填了前i位且状态为j的方案数,fs[i][j]:填了前i位且状态为j的所有方案的总和 fc[i][j]:填了前i位且状态为j的方案数,fs[i][j]:填了前i位且状态为j的所有方案的总和

状态转移:对于每一位按照集合划分转移即可。

最后要判断一下前缀(原数)是否合法。

Code

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <set>
#include <queue>
#include <vector>
#include <map>
#include <bitset>
#include <unordered_map>
#include <cmath> 
#include <stack>
#include <iomanip>
#include <deque> 
#include <sstream>
#define x first
#define y second
#define debug(x) cout<<#x<<":"<<x<<endl;
using namespace std;
typedef long double ld;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef unsigned long long ULL;
const int N = 1e5 + 10, M = 2 * N, INF = 0x3f3f3f3f, mod = 998244353;
const double eps = 1e-8, pi = acos(-1), inf = 1e20;
#define tpyeinput int
inline char nc() {static char buf[1000000],*p1=buf,*p2=buf;return p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++;}
inline void read(tpyeinput &sum) {char ch=nc();sum=0;while(!(ch>='0'&&ch<='9')) ch=nc();while(ch>='0'&&ch<='9') sum=(sum<<3)+(sum<<1)+(ch-48),ch=nc();}
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int v = 0) {
    e[idx] = b, w[idx] = v, ne[idx] = h[a], h[a] = idx ++;
}
int n, m, k;
int a[N];
LL fc[N][(1 << 10) + 1], fs[N][(1 << 10) + 1];
// fc[i][j]:前i个数字且状态为j的方案数
// fs[i][j]:前i个数字且状态为j的数的和
// 数位dp -> 分类讨论  对于每一位  保证算贡献的数都在范围内, 重点在于对状态的贡献
// xxxxaxxxx  分3类
//1. xxxxx非零且小于原数  则第i位 可以任意取  [0,9]
//2. xxxx为零            则第i位 可以取 [1,9]  
//3. xxxx 为原数字       则第i位 可以取得[0, a - 1]
// 最后单独算一下原数是否符合即可
// 转移由前面转移过来
// 变成连续问题  直接连续来取 
int main() {
    ios::sync_with_stdio(false), cin.tie(0);
    string str; cin >> str;
    int n = str.size();
    for (int i = 0; i < n; i ++) a[i] = str[i] - '0';
    
    cin >> m;
    int mask = 0;
    for (int i = 0; i < m; i ++) {
        int x; cin >> x;
        mask |= 1 << x;
    }

    int last = a[0], prebit = 1 << a[0];
    // 相当于前缀前缀且全为0的情况 因此只能由[1, a[i]) 转移过来
    for (int i = 1; i < a[0]; i ++) {
        (fc[0][1 << i] += 1) % mod;
        (fs[0][1 << i] += i) % mod;
    }

    for (int i = 1; i < n; i ++) {
        // 保证转移的时候这一位可以随便选取
        for (int j = 1; j < (1 << 10); j ++) 
            for (int x = 0; x < 10; x ++) {
                fc[i][j | (1 << x)] = (fc[i][j | (1 << x)] + fc[i - 1][j] + mod) % mod;
                fs[i][j | (1 << x)] = (fs[i][j | (1 << x)] + fs[i - 1][j] * 10 + fc[i - 1][j] * x + mod) % mod;
            }

        for (int x = 1; x < 10; x ++) {
            fc[i][1 << x] = (fc[i][1 << x] + 1 + mod) % mod;
            fs[i][1 << x] = (fs[i][1 << x] + x + mod) % mod;
        }
        
        // 不填满  前缀相同 如果这一位填到a[i] 那么f[i - 1][j]的这些已有的状态里就存在了abcd 就无法直接转移了,相当于一个分类讨论
        for (int x = 0; x < a[i]; x ++) {
            fc[i][prebit | (1 << x)] = (fc[i][prebit | (1 << x)] + 1 + mod) % mod;
            fs[i][prebit | (1 << x)] = (fs[i][prebit | (1 << x)] + (LL)last * 10 + x + mod) % mod;
        }
        last = ((LL)last * 10 + a[i] + mod) % mod;
        prebit = prebit | (1 << a[i]);
    }
   // cout << last << endl;
    LL res = 0;
    for (int j = 0; j < (1 << 10); j ++)
        if ((j & mask) == mask) res = (res + fs[n - 1][j]) % mod;

    if ((prebit & mask) == mask) res = (res + last) % mod;

    cout << res << endl;
    return 0;
}
上一篇:数据库相关习题


下一篇:条件概率、乘法公式