数位DP板子(DFS写法)
vector<int>A(20, 0);
ll dp[20][];
bool check(int a);
ll dfs(int pos, int a, bool limit, bool lead) {
if (!pos) {
if(check(a))return 1;
return 0;
}
if (!limit && dp[pos][a] != -1)return dp[pos][a];
ll ans = 0; int up = limit ? A[pos] : 9;
repi(now, 0, up) {
if (lead && now == 0) {
ans += dfs(pos - 1, a, limit & (now == up), 1);
}
else {
ans += dfs(pos - 1, next_a, limit & (now == up), 0);
}
}
if (!limit)dp[pos][a]= ans;
return ans;
}
ll solve(ll x) {
int cnt = 0;
while (x)A[++cnt] = x % 10, x /= 10;
return dfs(cnt, 0, a, 1);
}
其中a代表特殊的限制条件
PS:数位DP有普通的循环写法和DFS写法,普通循环胜在好理解,DFS写法胜在够灵活,大家可以灵活选择使用~
板子题:不降数
#数位dp #dp
题目
题目要求在一个给定的整数区间 ([a, b]) 内,找出有多少个“不降数”。不降数是指从左到右各位数字呈非下降关系的数字,例如 123 和 446。
输入格式:
- 输入包含多组测试数据。
- 每组数据占一行,包含两个整数 (a) 和 (b)。
- (1 \leq a \leq b \leq 2^{31} - 1)
输出格式:
- 每行给出一组测试数据的答案,即 ([a, b]) 之间有多少个不降数。
输入样例:
1 9
1 19
1 2557
1 5728
输出样例:
9
18
472
669
思路
状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示一个i位数,最高位为j时的不降数的个数
状态转移:
d
p
[
i
]
[
j
]
=
∑
k
=
j
9
d
p
[
i
−
1
]
[
k
]
dp[i][j]=\sum_{k=j}^{9} dp[i-1][k]
dp[i][j]=k=j∑9dp[i−1][k]
在预处理完dp之后,可以利用前缀和计算dp1(r)-dp1(l-1)
对于一个n,先将其转换成数组形式用于从高位遍历到低位
对于每一个位,分为最高位为a[i]
与小于a[i]
两种情况,小于a[i]
的时候可以直接加上dp[i][j]
,如果等于了a[i]
那么就进入下一个位。
a
n
s
+
=
∑
j
=
l
a
s
t
a
[
i
]
−
1
d
p
[
i
]
[
j
]
ans+=\sum_{j=last}^{a[i]-1}dp[i][j]
ans+=j=last∑a[i]−1dp[i][j]
因此当位数只剩下一位的时候,答案就只有n这一种了。
代码实现
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<queue>
#include<cmath>
#include<set>
#include<map>
using namespace std;
using ll = long long;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
const ll inf = 0x3f3f3f3f3f3f3f3f;
const ll mod = 1e8;
vector<vector<ll>>dp(12, vector<ll>(12,0));
void init() {
rep(i, 0, 9)dp[1][i] = 1;
rep(i, 2, 11) {
rep(j, 0, 9) {
rep(k, j, 9) {
dp[i][j] += dp[i - 1][k];
}
}
}
}
ll dp1(ll n) {
if (n == 0)return 1;
vector<int>a;
while (n) { a.push_back(n % 10), n /= 10; }
ll ans = 0, last = 0;
per(i,a.size()-1,0) {
ll now = a[i];
rep(j, last, now - 1) {
ans += dp[i+1][j];
}
if (now < last)break;
last = now;
if (i == 0)ans++;
}
return ans;
}
void eachT() {
init();
ll l, r;
while (cin >> l >> r) {
cout << dp1(r) - dp1(l - 1) << '\n';
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
ll t = 1;
//cin >> t;
while (t--) { eachT(); }
}
P2657 [SCOI2009]
windy 数
#数位dp #dp
题目
题目背景
windy 定义了一种 windy 数。
题目描述
不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 a 和 b 之间,包括 a 和 b ,总共有多少个 windy 数?
输入格式
输入只有一行两个整数,分别表示 a 和 b。
输出格式
输出一行一个整数表示答案。
思路
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i位数,最高位为 j j j的windy数的个数
状态转移:
d
p
[
i
]
[
j
]
=
∑
k
=
0
∣
k
−
j
∣
≥
2
d
p
[
i
−
1
]
[
k
]
dp[i][j]=\sum_{k=0}^{|k-j|\geq 2}dp[i-1][k]
dp[i][j]=k=0∑∣k−j∣≥2dp[i−1][k]
先初始化dp数组,将前缀和打出来
再针对输入的l和r进行计算:
将输入的数字n转换成数组a
从高位开始倒序遍历a,枚举每一位的数字并判断合法性
rep(j,0,now-1){
if(i==a.size()-1&&j==0)continue;
if(abs(last-j)>=2)res+=dp[i][j];
}
特别需要注意的是,枚举最高位的时候,i不能为0,因为不能有前导零;然而在初始化dp的时候,要算上最高位为0的情况,因为前缀和计算需要相减。
在a的当前位置不合法的时候,直接break,因为后面的数字都是以这个不合法的状态作为最高位的。
特别需要注意的是,这个枚举只考虑了a.size()-1位数的情况,需要加上1~a.size()-2位数的情况。由于这些数必定小于a,所以可以直接累加dp值。
最后输出dp1(r)-dp1(l-1)
即可
代码实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i,a,b) for(ll i=(a);i<=(b);i++)
#define per(i,a,b) for(ll i=(a);i>=(b);i--)
#define see(stl) for(auto&ele:stl)cout<<ele<<" "; cout<<'\n';
vector<vector<ll>>dp(11, vector<ll>(10, 0));
void init() {
rep(i, 0, 9)dp[1][i] = 1;
rep(i, 2, 10) {
rep(j, 0, 9) {
rep(k, 0, 9) {
if (abs(k - j) >= 2)dp[i][j] += dp[i - 1][k];
}
}
}
}
ll dp1(ll n) {
if (!n)return 0;
vector<int>a; a.push_back(0);
while (n)a.push_back(n % 10), n /= 10;
ll res = 0, last = -2;
per(i, a.size() - 1, 1) {
ll now = a[i];
rep(j, 0, now - 1) {
if (i == a.size() - 1 && j == 0)continue;
if (abs(last - j) >= 2)res += dp[i][j];
}
if (abs(now - last) < 2)break;
last = now;
if (i == 1)res++;
}
rep(i, 1, a.size() - 2)rep(j, 1, 9)res += dp[i][j];
return res;
}
void eachT() {
ll l, r; cin >> l >> r;
init();
cout << dp1(r) - dp1(l - 1) << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
ll t = 1;
//cin>>t;
while (t--) { eachT(); }
}
H - Unlucky Numbers
#数位dp #dp
题目
In this problem, unlike problem A, you need to look for unluckiest number, not the luckiest one.
Note that the constraints of this problem differ from such in problem A.
Olympus City recently launched the production of personal starships. Now everyone on Mars can buy one and fly to other planets inexpensively.
Each starship has a number —some positive integer xx. Let’s define the luckiness of a number xx as the difference between the largest and smallest digits of that number. For example, 142857142857 has 88 as its largest digit and 11 as its smallest digit, so its luckiness is 8−1=78−1=7. And the number 111111 has all digits equal to 11, so its luckiness is zero.
Hateehc is a famous Martian blogger who often flies to different corners of the solar system. To release interesting videos even faster, he decided to buy himself a starship. When he came to the store, he saw starships with numbers from ll to rr inclusively. While in the store, Hateehc wanted to find a starship with the unluckiest number.
Since there are a lot of starships in the store, and Hateehc can’t program, you have to help the blogger and write a program that answers his question.
Input
The first line contains an integer tt (1≤t≤6001≤t≤600) —the number of test cases.
Each of the following tt lines contains a description of the test case. The description consists of two integers ll, rr (1≤l≤r≤10181≤l≤r≤1018) — the largest and smallest numbers of the starships in the store.
Output
Print tt lines, one line for each test case, containing the unluckiest starship number in the store.
If there are several ways to choose the unluckiest number, output any of them.
翻译
在这个问题中,与问题A不同的是,你需要寻找最不幸运的数字,而不是最幸运的数字。
注意本题的约束条件与问题A中的不同。
奥林巴斯城最近开始生产个人星际飞船。现在火星上的每个人都可以购买一艘,并以低廉的价格飞往其他星球。
每艘星际飞船都有一个编号——某个正整数x。我们定义数字x的幸运度为该数字的最大数字和最小数字之差。例如,142857的最大数字是8,最小数字是1,因此其幸运度为8−1=7。而数字111的所有数字都等于1,因此其幸运度为零。
赫特赫克是一位著名的火星博主,他经常飞往太阳系的不同角落。为了更快地发布有趣的视频,他决定给自己买一艘星际飞船。当他来到商店时,他看到星际飞船的编号从l到r(包含l和r)。在商店里时,赫特赫克想要找到一艘编号最不幸运的星际飞船。
由于商店里的星际飞船数量众多,而赫特赫克不会编程,因此他需要你编写一个程序来回答他的问题。
输入
第一行包含一个整数t(1≤t≤600),表示测试用例的数量。
接下来的t行每行描述一个测试用例。每个测试用例由两个整数l和r(1≤l≤r≤10^18)组成,表示商店中星际飞船编号的最小值和最大值。
输出
对于每个测试用例,输出一行,包含商店中最不幸运的星际飞船编号。
如果有多个最不幸运的数字可供选择,输出其中任意一个即可。
思路
虽然这个题目是需要寻找一个满足条件的数,但是我们还是可以通过求出范围内所有满足条件的数的数量,再通过二分不断逼近寻找出这个数。
由于幸运值一共就只有0-9一共10种情况,所以可以从小到大遍历这个幸运值,一旦发现当前幸运值是可以找到答案的,就break。
break之后,拿着这个最优的幸运值去二分查找一个答案。
dfs过程:
- 传入参数:
- int pos:当前处理的数位,从高位到低位降序遍历
- int ma、int mi:从最高位到pos中的数位最大值与最小值
- bool limit:判断现在是否还满足上界的限制。如果还满足,那么当前位的上界也由数组A确定,否则无上界限制。
- bool lead:判断当前处理的位置是否含有前导零
终止条件: - 如果pos已经越界到-1了,那么对当前构造出的数进行判断
如果最大最小值的差值等于幸运值,那么情况数就++
- 剪枝:
- 如果当前没有上界限制,并且之前已经dfs过当前状态,可以返回记忆化数组
dp[pos][ma][mi]
的值
- 如果当前没有上界限制,并且之前已经dfs过当前状态,可以返回记忆化数组
- 入:
- 确定上界up,如果存在上界限制,那么up就是
A[pos]
,否则无上界就是9 - 遍历当前数位的所有可能状态:
- 如果上一位是前导零并且现在填入的数也是0,那么就相当于搜索位数更小的数(比如上界是54321为5位数,但是我们也需要搜索位数不是5位的数,比如00342),此时传入的状态中不能让0去影响最大最小值。
- 否则最大值与最小值需要对当前填入的数更新。
- 特别地,如果上一个状态存在限制,并且当前填入的数就是up,那么就相当于这次的状态存在限制,否则以后的状态都不存在上界限制了。
- 确定上界up,如果存在上界限制,那么up就是
- 回溯:
- 如果现在的状态没有上界限制,那么就把ans记忆化储存。
- 不能有限制是因为dp的状态就是定义成没有上界限制的合法的数量,也正因为这样设定,才可以在剪枝的时候读取。
- 离:
- 返回在当前状态下的合法数的数量
特别需要注意的是,本题的变量类型的定义必须非常小心,比如dp
数组,需要开ll!!!
代码实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i,a,b) for(ll i=(a);i<=(b);i++)
#define repi(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(ll i=(a);i>=(b);i--)
#define see(stl) for(auto&ele:stl)cout<<ele<<" "; cout<<'\n';
const ll inf = 0x3f3f3f3f3f3f3f3f;
vector<int>A(20, 0);
ll dp[20][10][10];
ll cha;
ll dfs(int pos, int ma, int mi, bool limit, bool lead) {
if (!pos) {
if (ma - mi == cha)return 1;
return 0;
}
if (!limit && dp[pos][ma][mi] != -1)return dp[pos][ma][mi];
ll ans = 0; int up = limit ? A[pos] : 9;
repi(now, 0, up) {
if (lead && now == 0) {
ans += dfs(pos - 1, ma, mi, limit & (now == up), 1);
}
else {
ans += dfs(pos - 1, max(ma, now), min(mi, now), limit & (now == up), 0);
}
}
if (!limit)dp[pos][ma][mi] = ans;
return ans;
}
ll solve(ll x) {
int cnt = 0;
while (x)A[++cnt] = x % 10, x /= 10;
return dfs(cnt, 0, 9, 1, 1);
}
void eachT() {
ll l, r; cin >> l >> r;
rep(i, 0, 9) {
cha = i;
memset(dp, -1, sizeof dp);
if (solve(r) - solve(l - 1)) {
break;
}
}
while (l <= r) {
ll mid = l + r >> 1;
if (solve(mid) - solve(l - 1) > 0) {
r = mid - 1;
}
else {
l = mid + 1;
}
}
cout << l << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
ll t = 1;
cin >> t;
while (t--) { eachT(); }
}
E. Sponsor of Your Problems
#数位dp
题目
思路
使用数位DP(DFS)的板子。
状态表示:
d
p
[
p
o
s
]
[
c
n
t
]
dp[pos][cnt]
dp[pos][cnt]表示当前遍历到第
p
o
s
pos
pos位,相同的数位累积了
c
n
t
cnt
cnt次
d
f
s
(
p
o
s
,
c
n
t
,
l
i
m
i
t
)
dfs(pos,cnt,limit)
dfs(pos,cnt,limit)
状态转移:
a
n
s
+
=
d
f
s
(
p
o
s
−
1
,
c
n
t
+
(
n
o
w
=
=
L
[
p
o
s
]
)
+
(
n
o
w
=
=
R
[
p
o
s
]
)
,
l
i
m
i
t
&
(
n
o
w
=
=
u
p
)
)
ans+=dfs(pos-1,cnt+(now==L[pos])+(now==R[pos]),limit\&(now==up))
ans+=dfs(pos−1,cnt+(now==L[pos])+(now==R[pos]),limit&(now==up))
需要特别小心的是,题目已经说了l和r的十进制位数是相同的,但是使用前缀和的时候
s
o
l
v
e
(
r
)
−
s
o
l
v
e
(
l
−
1
)
solve(r)-solve(l-1)
solve(r)−solve(l−1)会出现
l
−
1
l-1
l−1,可能位数不相同。因此不可以添加前导零的限制,否则会在位数不相同的时候错误输出!
其实dfs里面的lead可以不要掉
代码实现
#include<iostream>
#include<vector>
#include<queue>
#include<set>
#include<unordered_map>
#include<string.h>
using namespace std;
#define ll long long
#define rep(i,a,b) for(ll i=(a);i<=(b);i++)
#define per(i,a,b) for(ll i=(a);i>=(b);i--)
const ll inf = 0x3f3f3f3f3f3f3f3f;
vector<int>A(20, 0), L(20, 0), R(20, 0);
ll dp[20][20], CNT;
ll dfs(int pos, int cnt, bool limit, bool lead) {
if (!pos) {
if (cnt == CNT)return 1;
return 0;
}
if (!limit && dp[pos][cnt] != -1)return dp[pos][cnt];
ll ans = 0; int up = limit ? A[pos] : 9;
rep(now, 0, up) {
ans += dfs(pos - 1, cnt + (now == L[pos]) + (now == R[pos]), limit & (now == up), 0);
}
if (!limit)dp[pos][cnt] = ans;
return ans;
}
ll solve(ll x) {
memset(dp, -1, sizeof dp);
int idx = 0;
while (x)A[++idx] = x % 10, x /= 10;
return dfs(idx, 0, 1, 1);
}
void eachT() {
ll l, r; cin >> l >> r;
ll lt = l, rt = r;
int idx = 0;
while (l)L[++idx] = l % 10, l /= 10;
idx = 0;
while (r)R[++idx] = r % 10, r /= 10;
for (CNT = 0; CNT <= 20; CNT++) {
if (solve(rt) - solve(lt - 1)) {
cout << CNT << '\n'; return;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
ll t = 1;
cin >> t;
while (t--) { eachT(); }
}