shuweiDP

前言

大家有没有认真做几道题呢?(╮( ̄▽ ̄)╭)
没有做也没关系辣,好好听课没有问题的。
(登录hznoi)

简介

\(copy\) 一下论文:

在信息学竞赛中,有一类难度不大但异常麻烦的问题——数位计数问题,这类问题的主要特点是询问的答案和一段连续的数的各个数位相关,并且需要对时间效率有一定要求。由于解决这类问题往往意味着巨大的代码量,而众多的特殊情况又意味着出现错误的巨大可能性,因此很少有人愿意解决此类问题,但只要掌握好的方法,解决这类问题也并非想象中的那样困难。

在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个 \(D\) 进制数或
此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小
顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利
用数位的性质,设计 \(\log(n)\)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。

数位 \(\text{DP}\) 还是挺有特征的,一般都能一眼看出来这道题是不是数位 \(\text{DP}\) ,所以关键的部分还在于具体实现方法,这里给出几种不同的方法:

1.递推

\((1)\) 优点:思路清晰,易于调试, for 循环很带感。

\((2)\) 缺点:码量看起来不太优秀,有时编程复杂度较大,有时需要很强的卡常能力,有时需要大力分类讨论,有的题不能用

\((3)\) 关键:

  1. 枚举东西,转化问题;(有时不需要)
  2. 定义状态,写转移方程;
  3. 大力 for 循环,大力卡边界,大力分类讨论;
  4. 卡不过就再大力;
  5. 还卡不过就用下一种方法 \(\downarrow\)

2.记忆化搜索

\((1)\) 优点:码量少,编程复杂度较小,几乎所有题都能做。

\((2)\) 缺点:好像没什么缺点,但我就是不喜欢用,因为“ for 循环很带感”;

\((3)\) 关键:与递推一样,但一般不用卡常,也不用大力分类讨论。

3.写函数

2009年国家集训队论文(高逸涵)

大家自己看吧,我感觉一般不会有人用这个方法,毕竟 \(3\sim5\) 个函数有些恐怖。

但这种解决问题的思想还是很重要的,下面会有题要用到。

例题

  1. #316. 花神的数论题:定义 \(f[i][0/1][j]\) 表示从高到低 \(\text{DP}\) 到第 \(i\) 位,是否有限制,已经有 \(j\)\(1\) 了的方案数,最后快速幂一下。
  2. #322. 0和1的熟练:定义 \(f[i][0/1][j]\) 表示从高到低 \(\text{DP}\) 到第 \(i\) 位,是否有限制, \(0\) 的个数 \(-\) \(1\) 的个数为 \(j\) 的方案数,由于出了负数所以加个 \(base\) ,最后直接统计答案。
  3. #191. 有趣的数:有详细题解,关键思路是枚举数位和。
  4. #315. windy数:介绍一种处理前导 \(0\) 的方法:就是枚举有多少位,然后修改上限,多次 \(\text{DP}\)
  5. #323. 苍与红的试炼:这个题只能用记忆化搜索 \(Bfs\) 的方式来做,因为是求最优解,而且没给上限。
  6. #343. 计数:用到了上面第三种方法的思路,有上限的一个个枚举,没有的直接算。
  7. #317. 手机号码:这道题做法比较多,主要展示几份代码:手机号码
  8. #342. 数字计数:同样用到了上面第三种方法的思路,有上限的一个个枚举,没有的直接算。
  9. #321. haha数:直接定义 \(f[i][0/1][a][b][c][d][j]\) 表示从高到低 \(\text{DP}\) 到第 \(i\) 位,是否有限制,有 \(a\)\(2\)\(b\)\(3\)\(c\)\(5\)\(d\)\(7\)\(mod\ \ 2520=j\) 的方案数,直接转移就行,记忆化搜索更好写。(但我喜欢写递推)
  10. #209. 自积:有详细题解,不是很难,关键是枚举 \(2,3,5,7\) 的个数,记忆化搜索更好写。(但我喜欢写递推)
  11. #324. F(x):有好几种方法:1.mengbier:从高位往低位算,高位的值可以传给低位,某一位的值超过 \(18\) 之后效果就都一样了,所以与 \(18\) 取个 \(min\) 后接着搜,定义 \(f[i][0/1][j]\) 表示从高到低 \(\text{DP}\) 到第 \(i\) 位,是否有限制,第 \(i\) 位的 \(F\) 值为 \(j\) 的方案数;2.Jumbo;3.Asttinx64
  12. #355. 方伯伯的商场之旅:预处理出六个数组,看代码讲吧。
  13. #356. 数数:用四个数组递推,看代码讲吧。
  14. #357. 魔法OIer小袁Asttinx64

虽然说可以直接看到,可怎么没人进来点个赞呢?(╮(╯﹏╰)╭)
Upd:呜呜呜,好像本来就没几个人看。
数位 \(\text{DP}\) 大部分题都不难,大家先做几道题感受一下套路。
推荐做题顺序:(之前的两道题,做过的同学可以再看一眼)

  1. #316. 花神的数论题
  2. #322. 0和1的熟练
  3. #191. 有趣的数
  4. #340. B数
  5. #315. windy数
  6. #323. 苍与红的试炼
  7. #343. 计数
  8. #317. 手机号码
  9. #342. 数字计数
  10. HDU-3709
  11. #321. haha数
  12. #209. 自积
  13. #324. F(x)
  14. HDU-4352
    这些题型都不太一样, \(1\sim9\) 都是比较常规的题, \(10\sim14\) 大都没有固定的套路,需要好好思考。

#191. 有趣的数

\(Description\)

  定义函数 \(f(n)\) 表示 \(n\) 在十进制表示下的数字之和,求不超过 \(N\) 的所有正整数中有多少个数满足 \(f(n)∣n\)
  (对于 100% 的数据, \(1 \leq N \leq 10^{18}\) 。)

\(Solution\)

  看到数据范围,很容易可以排除时间复杂度为 \(\Theta(\sqrt{n})\) 及更高的算法,再仔细读题便不难想到使用数位 \(DP\) 来解决这道题。首先考虑枚举 \(f\) 的值为 \(x\) ,因为 \(f(n) \in [1,9\,log_{10}n]\) ,这时只需求出有多少数满足 \(f(n)\!==\!x\ \&\&\ n\ mod\ x\!==\!0\) 即可。然后思考如何通过 \(DP\) 求解答案,我们定义 \(g[i][0/1][j][k]\) 表示从高位到低位枚举到了第 \(i\) 位、 \(1\) ~ \(i\!-\!1\) 位是否都跟上限 \(N\) 相同、当前数 \(mod\ x\!==\!j\) 、当前数 \(f\) 值为 \(k\) 的方案数,状态定义比较套路,都是根据我们所要求的东西直接定义出来的。然后就可以直接写出 \(DP\) 转移方程了,同时再把循环上界设得符合实际一些,便可以通过这个题。时间复杂度一般不用分析,即使它理论上无法 \(AC\) ,只要尽力去剪枝即可。

\(Experience\)

  1. 通过题意、数据范围和复杂度分析来判断题目为数位 \(DP\)
  2. 通过枚举某个限制来转化成 \(DP\) 求方案数;
  3. 按照限制设计状态并写出转移方程;
  4. 把循环上界设得符合实际一些来进行剪枝;

\(Code\)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
typedef long long LL;
const int maxn=200+3;
int n;
int a[maxn];
int v[maxn];
LL g[maxn/10][2][maxn][maxn];
char ch;
LL HAHA(const int);
int main(){
	while(ch=getchar(),ch<'/');a[n=1]=ch-'0';
	while(ch=getchar(),ch>'/') a[++n]=ch-'0';
	LL ans=0;
	for(int i=1;i<=std::min(n*9,162);++i)
		ans+=HAHA(i);
	std::cout<<ans<<"\n";
	return 0;
}
inline LL HAHA(const int m){
	register int i,j,k,l;LL x;
	for(x=1,i=n;i;--i,x*=10)v[i]=x%m;
	memset(g,0,sizeof(g));
	g[0][1][0][0]=1;
	for(i=1;i<=n;++i)
		for(k=0;k<m;++k)
			for(l=0;l<=(i-1)*9;++l){
				if(g[i-1][0][k][l])
					for(j=0;j<10;++j)
						g[i][0][(k+v[i]*j)%m][l+j]+=g[i-1][0][k][l];
				if(g[i-1][1][k][l]){
					for(j=0;j<a[i];++j)
						g[i][0][(k+v[i]*j)%m][l+j]+=g[i-1][1][k][l];
					g[i][1][(k+v[i]*a[i])%m][l+a[i]]+=g[i-1][1][k][l];
				}
			}
	return g[n][0][0][m]+g[n][1][0][m];
}

#209. 自积

\(Description\)

  一个十进制正整数的数字积是指它各位数字的乘积,一个十进制正整数的自积是它的数字积再与它自身的乘积,求有多少个正整数的自积恰好在区间 \([L,R]\) 中。

\(Solution\)

  根据上题的经验,并通过思考,我们可以枚举 \(2,3,5,7\) 的个数来枚举数字积,并可以通过贪心检验来得到所有合法状态。再定义 \(f[i][0/1][a][b][c][d]\) 从高位到低位枚举到了第 \(i\) 位、 \(1\) ~ \(i\!-\!1\) 位是否都跟上限 \(N\) 相同、当前数中 \(2\)\(a\) 个、 \(3\)\(b\) 个、 \(5\)\(c\) 个、 \(7\)\(d\) 个的方案数,直接进行 \(DP\) 即可。

\(Experience\)

  看了标程我就自闭了,好像数位 \(DP\)\(Dfs/Bfs\) 有时候效果更好。

\(Code\)

标程
mengbier
1.标程:

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#define SF scanf
#define PF printf
using namespace std;
typedef long long LL;
const int MAXN = 18;
const int MAXM = 30;
const LL MAX_VAL = 1000000000000000000LL;
LL dp[MAXN+10][32][20][15][12];
LL generate_max, ans, L, R;
int prime[] = { 2, 3, 5, 7 };
int t[4];
int add[10][4] = {
   { 0, 0, 0, 0 },
   { 0, 0, 0, 0 },
   { 1, 0, 0, 0 },
   { 0, 1, 0, 0 },
   { 2, 0, 0, 0 },
   { 0, 0, 1, 0 },
   { 1, 1, 0, 0 },
   { 0, 0, 0, 1 },
   { 3, 0, 0, 0 },
   { 0, 2, 0, 0 }
};
LL dfs(int cur, LL num, LL base, LL l, LL r) {
    LL max_num = num + base - 1;
    if(max_num < l || num > r) return 0;
    if(cur == MAXN) 
        return !t[0] && !t[1] && !t[2] && !t[3];
    if(l <= num && max_num <= r && ~dp[cur][t[0]][t[1]][t[2]][t[3]]) return dp[cur][t[0]][t[1]][t[2]][t[3]];
    LL ret = 0;
    base /= 10;
    for(int i = num != 0; i <= 9; i++) {
        bool ok = true;
        for(int j = 0; j < 4; j++) ok = ok && add[i][j] <= t[j];
        if(!ok) continue;
        for(int j = 0; j < 4; j++) t[j] -= add[i][j];
        ret += dfs(cur+1, num+i*base, base, l, r);
        for(int j = 0; j < 4; j++) t[j] += add[i][j];
    }
    if(l <= num && max_num <= r) dp[cur][t[0]][t[1]][t[2]][t[3]] = ret;
    return ret;
}
void generate(LL R, LL mul, int cur) {
    if(mul > generate_max) return ;
    if(cur > 3) {
        ans += dfs(0, 0, MAX_VAL, (L-1) / mul + 1, R / mul);
        return ;
    }
    generate(R, mul, cur+1);
    t[cur]++;
    generate(R, mul*prime[cur], cur);
    t[cur]--;
}
int main() {
    cin >> L >> R;
    memset(dp, -1, sizeof(dp));
    generate_max = sqrt(R) + 0.5;
    generate(R, 1, 0);
    cout << ans;
}

2.mengbier:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define min(x,y) (x<y?x:y)
typedef long long LL;
const int maxn=5e3+3;
const int A[]={0,0,1,0,2,0,1,0,3,0};
const int B[]={0,0,0,1,0,0,1,0,0,2};
const int C[]={0,0,0,0,0,1,0,0,0,0};
const int D[]={0,0,0,0,0,0,0,1,0,0};
struct Node{
	LL M;
	int a,b,c,d;
}s[maxn];
LL Max;
int tot;
int M[20];
LL f[20][2][29][19][11][10];
LL DP(Node);
LL HAHA(LL);
bool HA(int,int,int,int,LL);
signed main(){
	LL L,R;std::cin>>L>>R;
	std::cout<<HAHA(R)-HAHA(L-1)<<"\n";
	return 0;
}
inline LL HAHA(LL M){
	if(!M)return 0;
	tot=0,Max=M;
	for(LL a=0,A=1;a<=28;++a,A*=2)
		for(LL b=0,B=1;b<=18 && A*B<=Max;++b,B*=3)
			for(LL c=0,C=1;c<=10 && A*B*C<=Max;++c,C*=5)
				for(LL d=0,D=1;d<=9 && A*B*C*D<=Max;++d,D*=7)
					if(HA(a,b,c,d,A*B*C*D))
						s[++tot]=(Node){Max/(A*B*C*D),a,b,c,d};
	LL ans=0;
	for(int i=1;i<=tot;++i)
		ans+=DP(s[i]);
	return ans;
}
inline bool HA(int a,int b,int c,int d,LL M){
	LL x=1,ans=0;M=Max/M;
	while(b>1 && ans<=M)ans+=x*9,b-=2,x*=10;
	while(a>2 && ans<=M)ans+=x*8,a-=3,x*=10;
	while(d && ans<=M)ans+=x*7,--d,x*=10;
	while(a && b && ans<=M)ans+=x*6,--a,--b,x*=10;
	while(c && ans<=M)ans+=x*5,--c,x*=10;
	while(a>1 && ans<=M)ans+=x*4,a-=2,x*=10;
	while(b && ans<=M)ans+=x*3,--b,x*=10;
	while(a && ans<=M)ans+=x*2,--a,x*=10;
	return ans<=M;
}
inline LL DP(Node x){
	int n=0;LL temp=x.M;
	while(temp)
		M[++n]=temp%10,temp/=10;
	std::reverse(M+1,M+n+1);
	register int i,j,a,b,c,d;
	f[0][1][0][0][0][0]=1;
	for(i=1;i<n;++i)
		f[i][0][0][0][0][0]=1;
	for(i=1;i<=n;++i)
		for(a=min(x.a,3*(i-1));~a;--a)
			for(b=min(x.b,2*(i-1-a/3));~b;--b)
				for(c=min(x.c,i-1-a/3-b/2);~c;--c)
					for(d=min(x.d,i-1-a/3-b/2-c);~d;--d){
						if(f[i-1][0][a][b][c][d]){
							const LL xx=f[i-1][0][a][b][c][d];
							for(j=1;j<10;++j){
								if(d+D[j]>x.d)continue;
								if(c+C[j]>x.c)continue;
								if(b+B[j]>x.b)continue;
								if(a+A[j]>x.a)continue;
								f[i][0][a+A[j]][b+B[j]][c+C[j]][d+D[j]]+=xx;
							}
						}
						if(f[i-1][1][a][b][c][d] && M[i]){
							const LL xx=f[i-1][1][a][b][c][d];
							for(j=1;j<M[i];++j){
								if(d+D[j]>x.d)continue;
								if(c+C[j]>x.c)continue;
								if(b+B[j]>x.b)continue;
								if(a+A[j]>x.a)continue;
								f[i][0][a+A[j]][b+B[j]][c+C[j]][d+D[j]]+=xx;
							}
							if(d+D[j]>x.d)continue;
							if(c+C[j]>x.c)continue;
							if(b+B[j]>x.b)continue;
							if(a+A[j]>x.a)continue;
							f[i][1][a+A[j]][b+B[j]][c+C[j]][d+D[j]]+=xx;
						}
					}
	LL ans=f[n][0][x.a][x.b][x.c][x.d]+f[n][1][x.a][x.b][x.c][x.d];
	for(i=1;i<=n;++i)
		for(a=min(x.a,3*i);~a;--a)
			for(b=min(x.b,2*(i-a/3));~b;--b)
				for(c=min(x.c,i-a/3-b/2);~c;--c)
					for(d=min(x.d,i-a/3-b/2-c);~d;--d)
						f[i][0][a][b][c][d]=f[i][1][a][b][c][d]=0;
	return ans;
}
posted @ 2020-07-20 11:35  mengbier  阅读(268)  评论(0编辑  收藏  举报