Eric Way's Personal Site

I Write $\sin(x)$ Not Tragedies

题解 CF55D :数位DP+数论

2020-12-16 Coding

这题做了半天。

美丽的数的定义是“可以被自己的所有非零数位整除的数”。

做数位DP,想的就是每个状态需要什么参量来表示。注意到这个定义的限制并不在选取每个数位的时候,而是在整个数字选完之后才能做一个整体判断。

很自然地,首先想到需要知道我们已经在$2, 3, \dots, 9$中选了哪些数字,这可以用一个$7$位二进制数来表示。这个想法虽然正确,但在本题中时间效率不够。能够观察到,假如我们选了$9$而没有选$3$,这种情况在最后需要的判断,和既选了$9$也选了$3$是等价的。这不免让我们意识到,可以拿我们已选的所有数位在$2, 3, 5, 7$的最高次幂分别作为一个状态参量,它一共有$4\times3\times2\times2=48$种可能取值,比一个$7$位二进制数来得小很多。这样做其实是可以的,但写起来稍微麻烦一点。

我们依然可以更进一步:能够被我们的所有数位整除,就等价于能够被我们的所有数位的最小公倍数整除。虽然这个最小公倍数最大能达到$2520$,但根据刚才的论述,它只有$48$个可能的取值,对应的是$2520$的所有因数,简单离散化即可。 我们需要判断的是整个数能否被所有数位的最小公倍数整除。最理想的做法是记录状态参量包括“每一步选完后的数前缀”模“我们所选的所有数位的最小公倍数”的值。但这是不可能的,因为在选取的每一步,我们无法预知后面的步骤会选择什么,也就无法预知这个最小公倍数是几。但所幸其实只要是这个最小公倍数的倍数作为模数,我们都可以用余数来判断原数对最小公倍数的整除性。因此最后我们选取的模数相等于是“所有最小公倍数的最小公倍数”,不难发现这个虚张声势的说法等价于$2, 3, \dots, 9$的最小公倍数$2520$。

理论上这道题就做完了。但是我又被卡时间了。这时候我意识到数位DP未必非要走套路:先求小于等于$n$的“满足条件的数”,再拿区间两个端点求出的值减一下——其实可以搜索的时候同时卡两个端点,然后一遍就搜完了。做法就是不仅记录“已选择的所有数中是否均和右端点对应数位相等”,也记录“已选择的所有数中是否均和左端点对应数位相等”,这两个状态分别影响每一步能选择的数的上界和下界。这么做的好处在于,由此搜索的复杂度就只和区间长度有关,和左右端点数的大小本身无关了。换言之,对于原先左右端点都很大、但区间长度未必大的数据,这样做显然能缩短运行时间;对于其他数据,也不会明显劣于之前的算法(未严格证明)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <bits/stdc++.h>
using namespace std;
#define rep(i,from,to) for(register int i=(int)from;i<=(int)to;++i)
#define For(i,to) for(register int i=0;i<(int)to;++i)
typedef long long ll;
inline ll read(){
ll x=0; ll sign=1; char c=getchar();
while(c>'9' c<'0') {if (c=='-') sign=-1;c=getchar();}
while(c>='0' && c<='9'){x=(x<<3)+(x<<1)+c-'0';c=getchar();}
return x*sign;
}
const ll M = 2520;

ll l, r;
ll f[20][50][2521];
int fac[2521];
int dim_l[25]; int dim_l_size;
int dim_r[25]; int dim_r_size;

int gcd(int a, int b){ return b ? gcd(b, a % b) : a; }
inline int lcm(int a, int b) { return a / gcd(a, b) * b;}

ll dp(int x, int scm, int st, int opl, int opr) {
if (x == -1) { return (st % scm) == 0; }
ll &mem = f[x][fac[scm]][st];
if (!opl && !opr && mem > -1) return mem;
ll ret = 0;
int maxx = opr ? dim_r[x] : 9;
int minx = opl ? dim_l[x] : 0;
rep(i, minx, maxx) {
ret += dp(x - 1, (i == 0) ? scm : lcm(i, scm), ((st * 10 + i) % M),
(opl & (i == minx)), (opr & (i == maxx)));
}
if(!opl && !opr) mem = ret;
return ret;
}

inline void cal_dim(ll n, int* dim, int &dim_size) {
dim_size = 0;
while(n) {
dim[dim_size++] = n % 10; n /= 10;
}
}

ll solve(ll l, ll r) {
memset(dim_l, 0, sizeof(dim_l));
memset(dim_r, 0, sizeof(dim_r));
cal_dim(l, dim_l, dim_l_size);
cal_dim(r, dim_r, dim_r_size);
memset(f, -1, sizeof(f));
return dp(dim_r_size-1, 1, 0, 1, 1);
}

int main() {
#ifdef D
freopen("CF55D.in", "r", stdin);
double TIMEA = clock();
#endif

int cnt = 0;
for(int i = 1; i <= M; ++i) {
if (M % i == 0) fac[i] = ++cnt;
}

int T; T = read();
while(T--) {
l = read(); r = read();
ll ans = solve(l, r);
cout << ans << "\n";
}

#ifdef D
double TIMEB=clock();
printf("\n# Time consumed: %.3fs.\n", (TIMEB-TIMEA)/1000);
#endif
return 0;
}
This article was last updated on days ago, and the information described in the article may have changed.