「学习笔记」数位DP

虽然叫 DP,但是基本所有数位 DP 题我们都可以用好打好想好理解的 记忆化搜索 来做。

记搜模板

有一个大致的记忆化搜索模板, AK ALL 数位 DP

int dfs(int len, bool lead, bool limit, ...){
	if(!len) return 1; //len = 0,即所有位都搜完
	if(~f[len][lead][limit]...) return f[len][lead][limit]...; //记忆化,搜过当前情况直接返回
	
	int r = limit ? lim[len] : 9, ans = 0; //若有上限搜到当前位的限制,否则搜到9
	for(int i=0; i<=r; i++){
		ans += dfs(len-1, lead&&i==0, limit&&i==r, ...);
	}
	return f[len][lead][limit]... = ans;
}

$ f $ 数组用来记忆化,\(lim[i ]\) 表示当前数第 \(i\) 位的上限。

\(dfs 中的变量 lead 和 limit\) 分别表示有无前导零和是否是上限,\(len\) 表示现在搜到第几位(从高位到低位搜)。


预处理 如下:

int work(int x){
	int cnt = 0;
	while(x)
	{
		lim[++cnt] = x % 10;
		x /= 10;
	}
	memset(f, -1, sizeof f);
	return dfs(cnt, -10, true, true);
}

注意

数据范围看清,记得开 long long


解释

主要用了填数的思想。

\(limit\)

对于很多题,我们都需要求 1~n 中有多少符合条件的数,举个例子,\(n = 866666\),如果最高位搜的数 \(<8\) 时,好说,后面几位随便是什么都可以,但若最高位搜的数 \(=8\) 时,后一位需要保证搜 \(<=6\) 的数,不然就超出 \(n\) 的范围了。这就是我们记 \(limit\) 的原因。

\(lead\)

有些题保证不能有前导零,需要记一下。


常见题型

连续3个6

启示录
题意:含有连续 3 个 6 的数为魔鬼数,给定 \(x\),求第 \(x\) 小的魔鬼数。

该数整除其各位数字之和

记一个 \(sum\) 表示各位数字之和,\(s\) 表示当前模数,\(mod\) 表示当前数 mod s 之后的值。
设有 \(cnt\) 位数,那么 \(sum\) 最大可以到 \(9 * cnt\),我们将从 \(0 - 9*cnt\) 的所有数作为模数都搜一遍,搜索结束时判断一下 是否有 \(sum == s && mod == 0\) 即可 。

long long dfs(int len, int sum, int mod, int limit){
	if(!len) return sum == s and !mod ? 1 : 0;
	if(f[len][sum][mod]!=-1 and !limit) return f[len][sum][mod];
	long long ans = 0, m = limit ? lim[len] : 9;

	for(int i=0; i<=m; i++){
		ans += dfs(len-1, sum+i, (mod*10+i)%s, limit and (i == m));
	}
	return limit ? ans : f[len][sum][mod] = ans;
}

long long work(long long x){
	int cnt = 0;
	long long ans = 0;
	while(x)
	{
		lim[++cnt] = x % 10;
		x /= 10;
	}
	for(s=1; s<=cnt*9; s++){
		memset(f, -1, sizeof f);
		ans += dfs(cnt, 0, 0, 1);
	}
	return ans;
}

月之谜

有趣的数

code


#include<bits/stdc++.h>
#define in(n) scanf("%lld", &n)
using namespace std;

long long L, R;
int s, T;
long long f[20][200][200], lim[200];

long long dfs(int len, int sum, int mod, int limit){
	if(!len) return sum == s and !mod ? 1 : 0;
	if(f[len][sum][mod]!=-1 and !limit) return f[len][sum][mod];
	long long ans = 0, m = limit ? lim[len] : 9;

	for(int i=0; i<=m; i++){
		ans += dfs(len-1, sum+i, (mod*10+i)%s, limit and (i == m));
	}
	return limit ? ans : f[len][sum][mod] = ans;
}

long long work(long long x){
	int cnt = 0;
	long long ans = 0;
	while(x)
	{
		lim[++cnt] = x % 10;
		x /= 10;
	}
	for(s=1; s<=cnt*9; s++){
		memset(f, -1, sizeof f);
		ans += dfs(cnt, 0, 0, 1);
	}
	return ans;
}

int main(){
	// freopen("in.in", "r", stdin); freopen("out.out", "w", stdout);

	in(R);
	printf("%lld\n", work(R));
    
	return 0;
}


二进制

处理 \(lim[]\) 时换成二进制的处理即可。

int dfs(int len, int _0, int _1, bool limit, bool lead){
	if(!len&&!lead)return _0>=_1;
	if(!len)return 0;
	if(~f[len][_0][_1][limit][lead]) return f[len][_0][_1][limit][lead];

	int r = limit ? lim[len] : 1;
	int ans = 0;
	for(int i=0; i<=r; i++){
		ans += dfs(len-1, (i==0&&!lead)?(_0+1):_0, (i==1)?(_1+1):_1, limit&&(i==r), lead&&(i==0));
	}
	return f[len][_0][_1][limit][lead] = ans;
}

int work(int x){
	int cnt=0;
	while(x)lim[++cnt]=x&1,x>>=1;
	memset(f, -1, sizeof f);
	return dfs(cnt,0,0,1,1);
}

0和1的熟练

code


#include<bits/stdc++.h>
#define int long long 
#define ll long long
using namespace std;

int L, R;
int f[35][35][35][2][2];
int lim[35];

int dfs(int len, int _0, int _1, bool limit, bool lead){
	if(!len&&!lead)return _0>=_1;
	if(!len)return 0;
	if(~f[len][_0][_1][limit][lead]) return f[len][_0][_1][limit][lead];

	int r = limit ? lim[len] : 1;
	int ans = 0;
	for(int i=0; i<=r; i++){
		ans += dfs(len-1, (i==0&&!lead)?(_0+1):_0, (i==1)?(_1+1):_1, limit&&(i==r), lead&&(i==0));
	}
	return f[len][_0][_1][limit][lead] = ans;
}

int work(int x){
	int cnt=0;
	while(x)lim[++cnt]=x&1,x>>=1;
	memset(f, -1, sizeof f);
	return dfs(cnt,0,0,1,1);
}

signed main(){

    scanf("%lld%lld", &L, &R);
    printf("%lld ", work(R) - work(L-1));
    
     return 0;
}

各位数字都被该数本身整除

我们知道 \(0,1,2,3,4,5,6,7,8,9\) 的最小公倍数为 2520,所以记该数本身 mod 2520 的值,再记一个各位数字之和的最小公倍数即可。

inline int get_gvc(int gvc, int now){
	if(!now) return gvc;
	else return gvc * now / __gcd(gvc, now);
}

inline void yuen(){
	int tot = 0;
	for(int i=1; i<=2520; i++){
		if(2520 % i == 0) a[i] = ++tot;
	}
	for(int i=0; i<=18; i++) g[i] = pow(10, i);
}

inline ll dfs(int len, int sum, int gvc, bool limit, bool lead){
	if(!len) return (sum % gvc == 0);
	if(~f[len][sum][a[gvc]] and !limit and !lead) return f[len][sum][a[gvc]];

	int r = limit ? lim[len] : 9;

	ll ans = 0;
	for(int i=0; i<=r; i++){
		ans += dfs(len-1, (sum*10+i)%2520, get_gvc(gvc, i), limit&&(i==r), lead&&(i==0));
	}
	if(limit or lead) return ans;
	return f[len][sum][a[gvc]] = ans;
}

inline ll work(ll x){
	int cnt = 0;
	while(x){ lim[++cnt] = x % 10; x /= 10;}
	return dfs(cnt, 0, 1, true, true);
}

haha数

code
#include<bits/stdc++.h>
#define ll long long
using namespace std;

ll L, R;
int lim[22];
int T, a[2530];
ll f[20][2530][60], g[20];

inline int get_gvc(int gvc, int now){
	if(!now) return gvc;
	else return gvc * now / __gcd(gvc, now);
}

inline void yuen(){
	int tot = 0;
	for(int i=1; i<=2520; i++){
		if(2520 % i == 0) a[i] = ++tot;
	}
	for(int i=0; i<=18; i++) g[i] = pow(10, i);
}

inline ll dfs(int len, int sum, int gvc, bool limit, bool lead){
	if(!len) return (sum % gvc == 0);
	if(~f[len][sum][a[gvc]] and !limit and !lead) return f[len][sum][a[gvc]];

	int r = limit ? lim[len] : 9;

	ll ans = 0;
	for(int i=0; i<=r; i++){
		ans += dfs(len-1, (sum*10+i)%2520, get_gvc(gvc, i), limit&&(i==r), lead&&(i==0));
	}
	if(limit or lead) return ans;
	return f[len][sum][a[gvc]] = ans;
}

inline ll work(ll x){
	int cnt = 0;
	while(x){ lim[++cnt] = x % 10; x /= 10;}
	return dfs(cnt, 0, 1, true, true);
}

signed main(){
	//freopen("in.in", "r", stdin); freopen("out.out", "w", stdout);

	memset(f, -1, sizeof f); yuen();

	cin>>T;
	while(T--)
	{
		scanf("%lld%lld", &L, &R);
    	printf("%lld\n", work(R) - work(L-1));
	}
    
    return 0;
}
posted @ 2024-07-09 21:35  Aqr_Rn  阅读(33)  评论(0编辑  收藏  举报