DP是一种建立模型,划分出阶段性状态,寻找转移规律的思想,如果硬要拿模板套未免有些舍本求末。但是数位DP还是有一些普遍规律,总结如下:
1.数位DP是求 F ( x ) F(x) F(x)的问题,有点类似于数论函数,其中的 x x x代表从 1... x 1...x 1...x的整数集合 X X X。返回的值代表在 X X X中满足某个条件的整数个数,或者满足条件的整数函数值。
2.数位DP通常采用的方法是记忆化搜索,其中搜索的参数往往有(不一定都要):
2.1搜索到当前的位置
p
p
p
2.2当前位是否可以从0搜索到9,还是只能搜索到给定的
x
x
x在
p
p
p上的那一位
2.3前面是否搜索了非0数(因为某些条件下如00…03不是一个正确的数)
2.4搜索中记录的状态
3.搜索的方法是将 x x x视为一个数字序列,从0位开始到n-1位,当搜到n位(即越界时)检查状态,看是否满足条件然后返回。
列举几道题
CF 1036c
here
算是比较简单的入门题
这个题的条件是不超过3个非0数,所以我们可以记录一个nz(not zero),代表非0数的个数
cap代表这一位是否“顶住”
x
x
x,比如本来这位是3,如果取了2,那么下一位就能取0-9.但是如果这位是3,前面的搜索都是“顶住”
x
x
x,那么下一位还是不能随心所欲去取数字。
这个题只有满足
n
z
≤
3
nz \le3
nz≤3才会继续搜下去,所以可以剪掉那些
n
z
>
3
nz \gt 3
nz>3的部分。
有一个隐患是init函数中的写法需要每次memset一遍,如果dp记录的状态太大容易TLE
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;
ll mod = ll(1e9 + 7);
int T;
ll f[20][2][4];
int a[20];
int n;
ll get(int pos, int cap, int nz) {
if (pos == n) {
return 1;
}
if (f[pos][cap][nz] != -1)
return f[pos][cap][nz];
ll& res = f[pos][cap][nz];
res = 0;
int nmax = cap ? a[pos] : 9;
for (int i = 0; i <= nmax; ++i) {
int ncap = i < a[pos] ? 0 : cap;
int nnz = i == 0 ? nz : nz + 1;
if (nnz <= 3) {
res += get(pos + 1, ncap, nnz);
}
}
return res;
}
void init(ll x) {
n = 0;
int temp[20] = { 0 };
while (x) {
temp[n++] = x % 10;
x /= 10;
}
for (int i = 0; i < n; ++i)
a[i] = temp[n - i - 1];
memset(f, 0xff, sizeof(f));
}
int main() {
//freopen("in.txt", "r", stdin);
scanf("%d", &T);
while (T--) {
ll l, r;
scanf("%lld%lld", &l, &r);
l--;
init(l);
ll al = get(0, 1, 0);
init(r);
ll ar = get(0, 1, 0);
printf("%lld\n", ar - al);
}
return 0;
}
AT abc208e
here
要难一点了,这个题的难点在于状态不好搞,乘积怎么表示。
但实际上小于9的自然数乘积并不多,可以用map离散记录。
这个题的第二个trick是乘积0的处理, 如果前面位不是全为0,那么如果填了0乘积就为0,否则乘积按1来计算
最后在剪枝的时候不要把乘积超过k的状态剪掉,因为后面还可能会变成0.这里的处理是如果超过k,那么把状态置为k+1。最后结算的时候检查乘积是否超过k即可。
#include <cstring>
#include <climits>
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <map>
using namespace std;
typedef pair<int, int> pii;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef vector<int> vi;
char s[20];
int a[20];
int n;
ll k;
map<ll, ll> f[20][2][2];
ll get(int pos, int cap, int lead, ll val) {
if(pos == n) {
// printf("%d %d %d %lld %lld\n", pos, cap, lead, val, 1);
if (val <= k)
return 1ll;
else
return 0;
}
if(f[pos][cap][lead].count(val)) {
return f[pos][cap][lead][val];
}
ll &res = f[pos][cap][lead][val];
int ncap = a[pos] == 0 ? cap : 0;
int nval = lead ? 0 : 1;
res += get(pos + 1, ncap, lead, nval);
int nmax = cap ? a[pos]: 9;
for(int i = 1; i <= nmax; ++ i) {
ncap = i < a[pos] ? 0 : cap;
nval = val * i > k ? k + 1 : val * i;
res += get(pos + 1, ncap, 1, nval);
}
//printf("%d %d %d %lld %lld\n", pos, cap, lead, val, res);
return res;
}
int main() {
//freopen("in.txt", "r", stdin);
scanf("%s", s);
n = strlen(s);
for(int i = 0; i < n; ++ i)
a[i] = s[i] - '0';
scanf("%lld", &k);
ll ret = get(0, 1, 0, 1);
printf("%lld\n", ret - 1);
return 0;
}
LG P4999
烦人的数学作业
入门题,记录搜到当前的和作为状态
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;
ll mod = ll(1e9 + 7);
int n;
int a[20];
ll dp[20][2][180];
void init(ll x) {
int temp[20] = {};
n = 0;
while (x) {
temp[n++] = x % 10;
x /= 10;
}
for (int i = 0; i < n; ++i) {
a[i] = temp[n - 1 - i];
}
memset(dp, 0xff, sizeof(dp));
}
ll dfs(int pos, int cap, ll cur) {
if (dp[pos][cap][cur] != -1)
return dp[pos][cap][cur];
if (pos == n) {
return cur;
}
ll& res = dp[pos][cap][cur];
res = 0;
int nmax = cap ? a[pos] : 9;
for (int i = 0; i <= nmax; ++i) {
int ncap = i < a[pos] ? 0 : cap;
res += dfs(pos + 1, ncap, cur + i);
res %= mod;
}
return res;
}
int main() {
//freopen("in.txt", "r", stdin);
int T;
scanf("%d", &T);
while (T--) {
ll l, r;
scanf("%lld%lld", &l, &r);
l--;
init(l);
ll a0 = dfs(0, 1, 0);
init(r);
ll a1 = dfs(0, 1, 0);
ll a = a1 - a0;
if (a < 0) a += mod;
printf("%lld\n", a);
}
return 0;
}
LG P4317
花神的数论题
和上一题几乎一样,只是最后求的是乘积
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;
int n;
ll x;
ll mod = (ll)(10000007);
ll dp[64][2][64];
int a[64];
ll dfs(int pos, int cap, int cnt) {
if (dp[pos][cap][cnt] != -1)
return dp[pos][cap][cnt];
if (pos == n) {
if (cnt == 0) return 1;
else return cnt;
}
ll& res = dp[pos][cap][cnt];
res = 1;
int nmax = cap ? a[pos] : 1;
for (int i = 0; i <= nmax; ++i) {
int ncap = i < a[pos] ? 0 : cap;
res *= dfs(pos + 1, ncap, cnt + i);
res %= mod;
}
return res;
}
int main() {
//freopen("in.txt", "r", stdin);
memset(dp, 0xff, sizeof(dp));
scanf("%lld", &x);
int temp[64] = {};
while (x) {
temp[n++] = x % 2;
x /= 2;
}
for (int i = 0; i < n; ++i)
a[i] = temp[n - 1 - i];
ll ans = dfs(0, 1, 0);
printf("%lld\n", ans);
return 0;
}
…
待续