数位 DP + CF_Round_1035
【注意】本篇文章同步在 博客园 上发布。
数位 DP 算法
数位 DP 主要的原理就是通过对不同的连续数位进行相关操作,然后通过一定的加加减减得到答案。数位 DP 要求我们各数位之间是有递进关系,在状态存储上不断递推(或递归)。
实现数位 DP,就像刚刚说的,常用递推或递归方法。最常用的还是递归方法,也就是记忆化搜索。当然,这里贴个模板代码,下面是一道模板题。
给定一个数 \(x\),满足 \(1\leq x \leq 9\),现请你在 \(1,2,3,\dots,n\) 中找出数字 \(x\) 出现的次数。请注意,若数字在一个数中出现多次,那么次数也是多次。例如,对于数 \(i=10x+x\),即 \(i=\texttt{xx}\),那么出现次数在这个数中为 \(2\)。
\(\text{\bf{记忆化搜索代码}}\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
int x,n;
int len;
int f[10][10][2][2],numm[20];
int w[20];
int dfs(int cur,int d,bool up,bool zero){
	if(cur==0) return 0;
	if(~f[cur][d][up][zero]) return f[cur][d][up][zero];
	int limit;
	if(up==true) limit=numm[cur];
	else limit=9;
	int res=0;
	for(int i=0;i<=limit;i++){
		if(i==x){
			if(up&&i==limit) res+=n%w[cur]+1;
			else res+=w[cur];
		}
		res+=dfs(cur-1,i,up&&(i==numm[cur]),zero&&(i==0));
	}
	f[cur][d][up][zero]=res;
	return res;
}
void solve(){
	for(int i=n;i>0;i/=10){
		numm[++len]=i%10;
	}
	w[1]=1;
	for(int i=2;i<=len;i++){
		w[i]=w[i-1]*10;
	}
	memset(f,-1,sizeof(f));
	cout<<dfs(len,0,true,true)<<endl;
}
signed main(){
	cin>>n>>x;
	solve();
	return 0;
}
\(\text{\bf{数位 DP 递推代码}}\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[15][10],w[15],numm[15];
int n,x,len;
int ans;
void solve(){
	memset(dp,0,sizeof(dp));
	int s=1;
	dp[1][x]=1;
	for(int i=2;i<=len;i++){
		int ss=s;
		s=0;
		for(int j=0;j<=9;j++){
			if(j==x){
				dp[i][j]=ss+w[i];
			} else {
				dp[i][j]=ss;
			}
			s+=dp[i][j];
		}
	}
	ans=0;
	for(int i=len;i>=1;i--){
		for(int j=0;j<numm[i];j++){
			ans+=dp[i][j];
		}
		if(i>1&&numm[i]==x) ans+=n%w[i]+1;
	}
	ans+=dp[1][numm[1]];
	/*if(x==0) {
		for(int i=len;i>=2;i--){
			ans-=w[i];
		}
	}*/
}
signed main(){
	cin>>n>>x;
	for(int i=n;i>0;i/=10){
		len++;
		numm[len]=i%10;
	}
	w[1]=1;
	for(int i=2;i<=len;i++){
		w[i]=w[i-1]*10;
	}
	solve();
	cout<<ans<<endl;
	return 0;
}
\(\text{\bf{一些注意事项}}\)
- 
数位 DP 最好还是用递归(即记忆化搜索)来实现。因为一旦状态多起来、条件复杂起来,递推要考虑的特殊情况实在太多,代码过于难写。递归中,模板比较套路,题目也大多是按照那些参数套路来,所以不用过于担心很多问题。(例如前导〇)
 - 
数位 DP 中除了模板套路,我们还可以设计一些其他的状态。但请记住,无论如何设计状态,我们一定要用与答案密切相关的量,例如 \(sum\)。
 
CF Round 1035
怎么说呢?先说一下具体情况。刚开始比赛,第一题看了之后以为是裸搜索,但之后发现按照我的期望的搜索方法需要使用优先队列,还必须重载运算符,问题是我不会所以就开始考虑其他方法。通过举例和不重不漏的分类,最终发现 \(O(1)\) 算法,打出来了,时间 \(\texttt{19:25}\)。
之后就没了。为什么?第二题卡了半天。不会?不可能的。但是不管怎样,怎么调都不对, \(\texttt{test2}\) 总是过不去。既然如此,那就去打第三题吧。第三题推出了一些特殊性质,好像足够解题了(注意,这里并没有做出来),于是就回去打第二题想着调完第二题再回来。当然,不出意外地,第二题还是 \(\texttt{WA on test2}\)。这下可给我急坏了,感觉调不出来了,于是尝试考虑第三题,根据特殊性质写代码。结果,愣是没写出来,一个细节始终没处理出来。最后 \(8\) 分钟的时候,突然想到第二题可以重构代码换种思路,于是疯狂打,在 \(\texttt{20:57}\) 的时候交了一发,结果 \(\texttt{WA}\) 了。迅速调试,发现少打了一个等于号,连验都不验了,直接保存交上去,结果又 \(\texttt{WA}\) 了。时间来到 \(\texttt{20:59}\)。突然间,会查代码时注意到自己调试没删。。。疯狂删除保存提交,最终还是没能力挽狂澜,交的时候比赛已经结束了。。。结果,\(\texttt{AC}\) 了。比完赛后,发现第三题也是非常可做的,但是死脑筋了。
总结一下:
- 
\(\text{\bf{比赛要有合理策略。}}\)不要死磕,不要用一种方法死磕,不要一直死磕。如果发现自己正在死磕,不妨换一种方式考虑问题。不然,还真可能就被赖上了。
 - 
\(\text{\bf{更深的思考,更旁观的思路。}}\)在思考过程中,如第三题,仅仅推出特殊性质还不够(其实已经够了),我们还要思考一下利用特殊性质解题。解决不了的,我们仍然要使用这种特殊性质,仔细把清题目要我们求什么,而不是按照自己先入为主的理念利用特殊性质。例如,第三题中,我就想如果得到 \(0\) 需要什么,得到 \(1\) 需要什么,却没有一起考虑,错误归类,并没有归类成不同条件下相同需要什么。这就导致我浪费了我得出的特殊性质。
 - 
$\text{\bf{细节考虑到位。}} $有的时候,也许就是那一细节,也许就是隔了 \(2\) 分钟写题目产生的割裂感,都会导致两个东西完全不相同。可能在草稿纸上演练完了以后,\(\leq\) 就变成了 \(\geq\) 或 \(<\),导致最终判错。所以,思路清晰且专一是很有必要的。可以尝试对于一些易错代码先打注释,再按照注释写。
 
                    
                
                
            
        
浙公网安备 33010602011771号