day11 数位 dp 总结
数位 dp 的定义:所谓数位 dp 也就是根据一个数的每一位来做动态规划,而数位 dp 的题目不会像有些题目,需要你来仔细看需要用到什么算法,而数位 dp 的特点很显著。第一,一般情况下肯定是要往数字每一位数上去考虑的,第二,也就是数据范围,好一点的,可能 n ≤ 1 0 9 n \leq 10 ^ {9} n≤109 或者 n ≤ 1 0 18 n \leq 10 ^ {18} n≤1018,狠一点的直接干到 n ≤ 1 0 2000 n \leq 10 ^ {2000} n≤102000,这样就更加明显了,不可往数字的大小上看,必定得往数字的各个位数上考虑。
例题:
题目大致意思:
不含前导零且相邻两个数字之差至少为 2 2 2 的正整数被称为 windy 数。windy 想知道,在 a a a 和 b b b 之间,包括 a a a 和 b b b ,总共有多少个 windy 数?
题目大致思路:
这道题目很显然数位 dp,毕竟学的就是数位 dp,再接着我们可以想到一个很显然的结论,用数位 dp 处理区间
[
l
,
r
]
[l, r]
[l,r] 的区间不好处理,那么我们可以计算出
[
0
,
l
−
1
]
[0, l - 1]
[0,l−1] 的区间和
[
0
,
r
]
[0, r]
[0,r] 的区间,凭感性理解一下就会得知,答案就是区间到
r
r
r 的答案减去区间到
l
−
1
l - 1
l−1 的答案。
接下来就是数位 dp 部分,我使用记忆化搜索的方法来写,当然也可以用递推的方法,我的 dfs 流程如下:
-
设 d p i , j dp_{i, j} dpi,j 表示数位个数为 i i i,前一位数为 j j j 时,windy 数的个数。
-
用 4 4 4 个参数 n o w , l a s t , l a z y , f now, last, lazy, f now,last,lazy,f 来记录,分别表示当前数位个数,上一位数字,是否有前导 0 0 0,以及最高位是否有限制。
-
首先,我们设定边界,当位数超过,我们当前边界的数位时,显然返回 1 1 1。
-
最高位无限制时,且当前点被记录过,直接可以返回答案,这也就是记忆化。
-
初始化遍历范围,因为如果最高位有限制,那么我们要找到它最多能遍历到哪里,没有限制显然能够遍历到 9 9 9。
-
遍历, l a s t − i < 2 last - i < 2 last−i<2 也就是数位不满足条件,continue 跳过本次循环,这里及以下的 i i i 表示遍历的数字。
-
有前导 0 0 0,那么就 l a z y lazy lazy 一直为 1 1 1,继续往下搜,加入答案。
-
无前导 0 0 0,那么 l a z y lazy lazy 可以变成 0 0 0, l a s t last last 也就可以变成当前遍历的数字 i i i,继续往下搜,记录答案。
-
最后只要他没有前导 0 0 0,并且最高位没有限制, d p n o w , l a s t dp_{now, last} dpnow,last 就可以记录答案存起来了,最终返回此答案。
代码实现:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int l, r, siz, p, mn, mx;
int a[50], dp[50][50];
int dfs(int now, int last, int lazy, int f)//now表示当前数位个数,last表示前一位的数字,lazy表示是否有前导0,f表示最高位是否有限制
{
if(now > siz) return 1;
if(!f && dp[now][last] > -1) return dp[now][last];
int ans = 0, res = f ? (a[siz - now + 1]) : (9);//看看f有没有限制以确定遍历范围
for(int i = 0;i <= res; ++ i)
{
if(abs(last - i) < 2) continue;//不满足条件
if(lazy == 1 && i == 0)//还是前导0
{
ans += dfs(now + 1, -5, 1, f & i == res);
}
else
{
ans += dfs(now + 1, i, 0, f & i == res);
}
}
if(!lazy && !f)
{
dp[now][last] = ans;
}
return ans;
}
void add(int x)
{
++ p;
siz = 0;
memset(dp, -1, sizeof dp);
while(x > 0)
{
a[ ++ siz] = x % 10;//将每一位存入数组
x /= 10;//siz表示数字位数
}
if(p == 1) mn = dfs(1, -5, 1, 1);
else mx = dfs(1, -5, 1, 1);
}
signed main()
{
cin >> l >> r;
add(l - 1);
add(r);
cout << mx - mn << "\n";//答案显然为r的答案减去l-1的答案
return 0;
}
大致意思:
给定两个正整数 l , r l, r l,r,请求出 ∑ i = l r Digit(i) \sum_{i = l}^{r}\operatorname{Digit(i)} ∑i=lrDigit(i), Digit \operatorname{Digit} Digit 指的是十进制下 i i i 的数位之和。
大致思路:
我们定义 d p i , j dp_{i, j} dpi,j 表示一个 i i i 位数,最高位为 j j j 的数位和,我们可以对 d p dp dp 进行预处理,但在设想转移方程前我们要知道一个式子,那么就是以 j j j 为最高位的 i i i 位数总共有 1 0 i − 1 10 ^ {i - 1} 10i−1,那么我们可以得出转移方程 d p i , j = d p i − 1 , 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9 + 1 0 i − 1 × j dp_{i, j} = dp_{i - 1, 0/1/2/3/4/5/6/7/8/9} + 10 ^ {i - 1} × j dpi,j=dpi−1,0/1/2/3/4/5/6/7/8/9+10i−1×j,那么我们可以根据这个做数位 dp。
代码实现:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int l, r, dp[50][50], a[50], siz;
void dfs()
{
for(int i = 1;i <= 20; ++ i)
{
for(int j = 0;j <= 9; ++ j)
{
for(int k = 0;k <= 9; ++ k)
{
dp[i][j] += dp[i - 1][k];//预处理dp数组
}
int s = 1;
for(int k = 1;k <= i - 1; ++ k)
{
s *= 10;
}
dp[i][j] += s * j;
}
}
}
void add(int x)
{
memset(a, 0, sizeof a);
siz = 0;
while(x > 0)
{
a[ ++ siz] = x % 10;
x /= 10;
}
}
int ask(int x)
{
add(x);
int res = 0, getsum = 0;
for(int i = siz;i >= 1; -- i)
{
for(int j = 0;j < a[i]; ++ j)
{
res += dp[i][j];
int p = 1;
for(int k = 1;k < i; ++ k)
{
p *= 10;
}
res += (p * getsum);
}
getsum += a[i];//前n-i项数字和
}
res += getsum;
return res;
}
signed main()
{
// int T;
// cin >> T;
dfs();//预处理dfs数组
while(1)
{
cin >> l >> r;
if(l == 0 && r == 0) break;
cout << ask(r) - ask(l - 1) << "\n";
}
return 0;
}
大致意思:
求出 [ l , r ] [l, r] [l,r] 区间内满足要求的萌数(包含回文子串或本身是回文串)。
大致思路:
通过记忆化搜索的方法进行数位 dp,设计状态 d p i , j , 1 / 0 dp_{i, j, 1/0} dpi,j,1/0 表示 i i i 位数前一位是 j j j 的情况下是否满串及分别方案数即可。
代码实现:
#include <bits/stdc++.h>
using namespace std;
#define mn(a, b) a < b ? a : b
#define int long long
#define LLL __int128_t
#define LL long long
#define uint unsigned;
#define ull unsigned LL;
#define qi std::queue < int >;
#define vi std::vector < int >;
#define pii std::pair < int, int >;
#define lowbit(x) ((x) & -(x))
#define pq std::priority_queue
#define ve std::vector < pair < int, int> >
#define pr std::priority_queue < int, int >
#define pri std::priority_queue<pair<int,int> ,vector<pair<int,int> > ,greater<pair<int,int> > >
#define qcin std::ios::sync_with_stdio(0)
#define pb push_back
#define me(a, b) std::memset(a, b, sizeof(a))
const double TLS = 1;
const double eps = 1e-9;
const int inf = 1e9;
const int CPS = CLOCKS_PER_SEC;
const int INF = 1 << 30;//设置一个边界
const double TLC = TLS * 0.97 * CPS;
const int N = 23;
const int M = 1 << N;
const int MOD = 1e9 + 7;
void print(int x) {
if (!x)
return;
print(x / 10);
std::putchar(x % 10 + '0');
}
int dp[N][15][5], a[N];
string l, r;
int dfs(int now, int last1, int last2, int x, int lazy, int f)
{
if(!now) return x;//位数个数达到限制
if(~dp[now][last1][x] && !f)
{
return dp[now][last1][x];
}
int ans = 0, res = (f == 1) ? a[now] : 9;
for(int i = 0;i <= res; ++ i)
{
ans += (dfs(now - 1, i, lazy ? last1 : -1, (i == last2 & lazy) || (i == last1 & lazy) || x, lazy || (i > 0), f & res == i) % MOD);
ans %= MOD;
}
if(f == 0 && last2 != -1 && lazy) dp[now][last1][x] = ans;
return (ans % MOD + MOD) % MOD;
}
int add(string s)
{
int siz = 0, p = s.size() - 1;
memset(dp, -1, sizeof dp);
while(~p)
{
a[++ siz] = s[p] - '0';
-- p;
}
while(a[siz] == 0)
{
-- siz;
}
return dfs(siz, -1, -1, 0, 0, 1);
}
signed main(void) {
cin >> l >> r;
int head = l.size(), tail = 1;
while(head > tail && l[head - tail] == '0')//高精度减法
{
l[head - tail] = '9';
++ tail;
}
-- l[head - tail];
cout << (add(r) - add(l) % MOD + MOD) % MOD << "\n";
return 0;
}
/*
now: 当前数位(从高到低) last1: 前一个数 x: 存不存在回文
last2:前两个数,lazy:是否选过0以外的数(即:这个数开始计入答案)
f: 是不是一直都满着选
dp[now][last1][x]存在的必要条件是没有满着选。ta 的含义是 从pos位开始向下考虑,在上一个数是pre的情况,且之前有或者没有回文的条件下,有几种方案数
*/
目前先到这里。