(P.s.:)下文出现的部分词汇可能并不严谨,还请各位谅解(QwQ)
毕竟我比较菜kk
数位(DP? !) 机房某大佬:那不是快乐源泉吗?
记得之前在(qbxt)时听某林姓神仙讲得一脸懵(B),最近再系统性的学习一遍,感觉也没什么东西(……)
首先,数位(DP)一般用于解决这样的问题:
给定一个区间([L, R]),求符合某一条件的数的个数。而这一条件往往与大小无关,而是与数位的组成有关。
前置知识(:)前缀和,记忆化搜索。
首先我们将这个问题转化一下,思考一下就可以得到(:)这个问题显然满足前缀和的性质,我们用([0,R])内符合条件的数的个数减去([0,L-1])内符合条件的数的个数即为答案。
然后我们再思考一下:很显然,如果我们枚举数位,如果是十进制,显然只有(0sim9)十个数字可以填,所以很显然可以用(f[pos][val])来记录(pos)位为(val)时后面的答案,这样就大大缩短了程序运行时间。
当然能够记忆化还有两个前提:首先,它必须满足没有上限。 上限是什么意思呢?举一个例子,当我们要求([0,5648])内符合某条件的数的个数时,当我们搜索到百位时(6)时,显然十位只能枚举到(4),只是我们就称十位是有上限的,这时就不能记忆化。这是个很重要的内容,我们来详细看一下。
分两种情况考虑(:)
-
我们当前枚举到的这一位有上限,而我们记忆化的结果是一个一般性的,即我们记忆化记录的是后面所有位都可以取(0sim9)时的答案,而这一位有上限,不能随意取(0sim9),所以我们不能直接取记忆化记录的答案。
-
我们枚举到的这一位依旧有上限,那么我们在枚举时只会枚举到上限处,所以在这一位不能记忆化记录这一位的答案,因为他不具备一般性,依旧是上面的例子,我们枚举到十位(0sim4),这是求出的是([5600,5640])的答案,如果这时记录,很显然会少情况。
其次,我们需要特殊考虑一下前导(0)的问题,如果前导(0)对答案无影响,那自然最好,如果有影响,那么我们可能需要数组多开一
维或(dfs)参数多一维,这需要根据具体题目来定。
那么,数位(DP)的一般模板就基本成型了:
int dfs(int pos, bool limit, bool lead_zero, int sum) {
if (!pos) return 1; // or return sum;
if (!limit && !lead_zero && f[pos][val]) return f[pos][val]; //记忆化
int ans = 0;
int lim = limit ? val[pos] : 9; // 根据有无上限确定,枚举范围
for (int i = 0; i <= lim; i++) {
if (right(i))
ans += dfs(pos - 1, limit && (i == lim), (i == 0), sum + (i == 0) * calc(i));
}
if (!limit && !lead_zero) f[pos][val] = ans; // 记忆化
return ans;
}
int solve(int n) {
memset(f, 0, sizeof(f));
int len = 0;
len = 0;
while (n) val[++len] = n % 10, n /= 10; // 拆数位
return dfs(len, 1, 1,0); // 求值
}
然后你就会发现,基本上所有的数位(DP)都长这个亚子,当然根据题目不同参数或答案的计算会有所不同。
但大体框架都长这个亚子(……)
然后我们看几道例题:
这道题的条件是数字中不包含(4)或(62)。
显然我们随便乱搞一下就可以了。
code:
#include <iostream>
#include <cstdio>
#include <cstring>
int val[55], len, f[55][55];
int dfs(int pos, bool limit, bool lead_six) { //lead_six表示上一位是否为6
if (!pos) return 1;
if (!limit && f[pos][lead_six]) return f[pos][lead_six];
int ans = 0;
int lim = limit ? val[pos] : 9;
for (int i = 0; i <= lim; i++) {
if (lead_six && i == 2) continue;
if (i == 4) continue; // 保证符合条件
ans += dfs(pos - 1, limit && (i == lim), i == 6);
}
if (!limit) f[pos][lead_six] = ans;
return ans;
}
int solve(int n) {
memset(f, 0, sizeof(f));
int lne = 0;
len = 0;
while (n) val[++len] = n % 10, n /= 10;
return dfs(len, 1, 0);
}
int main() {
int L, R;
while (scanf("%d%d", &L, &R) == 2) {
if (L == 0 && R == 0) break;
printf("%d
", solve(R) - solve(L - 1));
}
return 0;
}
条件(:)不含前导(0),且相邻两数之差至少为(2)的数的个数。
这道题我们换一种方式,不采用记忆化搜索的方式来实现数位(DP),相邻两数之差至少为(2),很显然我们可以开一个数组(f[i][j])表示(i)位且最高位为(j)的(windy)数个数,这个数组我们可以通过预处理直接处理出来。
接下来我们考虑一个例子(:[0,5648])。
我们分这么几步来求(:)
-
(0sim999)
-
(1000sim4999)
-
(5000sim5649)
然后,为什么第三步要枚举到(5649)呢,为了保证我们数字严格小于(R),也是保证答案的正确性,我们再枚举个位时只枚举到减一 的位置,那么代码就很显然了。
code:
#include <iostream>
#include <cstdio>
#include <cstring>
#define int long long
#define abs(a) ((a) < 0 ? -(a) : (a))
int f[15][15], num[15], a, b;
void init() {
for (int i = 0; i <= 9; i++) f[1][i] = 1;
for (int i = 2; i <= 10; 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];
}
} // 预处理f数组
}
int calc(int x) {
int len = 0, ans = 0;
memset(num, 0, sizeof(num));
while (x) {
num[++len] = x % 10;
x /= 10;
} // 拆分数位
for (int i = 1; i <= len - 1; i++) {
for (int j = 1; j <= 9; j++)
ans += f[i][j];
} //处理 0~999
for (int i = 1; i < num[len]; i++)
ans += f[len][i]; //处理 1000~4999
for (int i = len - 1; i >= 1; i--) {
for (int j = 0; j < num[i]; j++) {
if (abs(num[i + 1] - j) >= 2) ans += f[i][j];
}
if (abs(num[i + 1] - num[i]) < 2) break;
} // 最难的一部分,处理5000~5648
// 一个小优化,如果当前位不满足是个windy数,那么就没必要在向后枚举。
return ans;
}
signed main() {
init();
scanf("%lld%lld", &a, &b);
printf("%lld
", calc(b + 1) - calc(a)); // 因为我们之枚举到减一的位置,所以要整体向后移一位。
return 0;
}
题目大意(:)求([L,R])内每个数各个数位上的和。
这道题我们考虑(0sim9)每个数字,我们只需要把每个数字出现的个数求出来,在乘上这个数字本身最终加和就是最后的答案。
这样我们设计(f)数组,(f[pos][sum])表示枚举到(pos)位,且当前求的和为(sum)时的答案。
我们对每一个数字单独考虑,因为一次性记录十个数字的话显然空间会爆炸。
然后(……)大力套模板!!
code:
#include <iostream>
#include <cstdio>
#include <cstring>
#define int long long
const int maxn = 25;
const int MOD = 1e9 + 7;
int T, L, R;
int val[maxn], f[maxn][maxn];
template<class T>
inline T read(T &x) {
x = 0; int w = 1, ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') w = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {x = x * 10 + ch - 48; ch = getchar();}
return x *= w;
}
int dfs(int pos, int limit, int lead_zero, int k, int sum) {
if (!pos) return sum;
if (!limit && !lead_zero && f[pos][sum] != -1)
return f[pos][sum];
int lim = limit ? val[pos] : 9;
int ans = 0;
for (int i = 0; i <= lim; i++) {
if (lead_zero && !i)
ans += dfs(pos - 1, limit && (i == lim), 1, k, sum);
else
ans += dfs(pos - 1, limit && (i == lim), 0, k, sum + (i == k));
}
if (!limit && !lead_zero)
f[pos][sum] = ans;
return ans;
}
int solve(int n, int k) {
memset(f, -1, sizeof(f));
int len = 0;
while (n) val[++len] = n % 10, n /= 10;
return dfs(len, 1, 1, k, 0);
}
signed main() {
read(T);
while (T--) {
int ans = 0;
read(L), read(R);
for (int i = 1; i <= 9; i++)
ans += (((solve(R, i) - solve(L - 1, i) + MOD) % MOD) * i % MOD + MOD) % MOD, ans %= MOD;
printf("%lld
", ans);
}
return 0;
}
这题的一道双倍经验题目:数字计数
然后,这些其实都是数位(DP)中一些比较简单的题目,因为数位(DP)确实并不简单,不信的话去题库里找一下数位动规的标签吧,嘿嘿嘿(……)
蒟蒻再做一些比较好的题目会往这里面补充,不过(:)
咕咕咕(……)