动态规划·数位DP

数位DP

好东西(?),但我不会

直接上例题吧

呃呃呃:

  • 后文中的 pos 默认是从低位往高位数的
  • 为了在 n 的限制内,而后面方案的数都是随便选的,所以第 pos 位不会取到 n 的第 pos 位(保不齐就超了)
  • 但这就会导致取不到 n ,所以要特判

【YbtOj】题解

A.B数计数

设状态 fpos,res,op 表示从到第 pos 位、当前余数为 res 、“13”出现情况为 op 的B数个数:

  • op=0,表示没有“13”
  • op=1,表示当前填的数为“3”
  • op=2,表示已有“13”

其中 f0,0,0=1 ,状态从低位往高位转移即可。

在统计答案时,因为我们状态设的相当于是“k 位数的B数个数”,为了给它有一个 n 的限制,我们要从高位往低位统计答案。

详:设 n 的第 pos 位是 s ,若状态第 pos 位取了 s ,那么第 pos+1 位也会有 n 的限制;若第 pos 位取的是比 s 小的数,那么后面的数位从 09 就都可以取了。

这样统计是统计不到n的,所以最后要特判n

统计答案形象化(结合代码食用):

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=21;
int n;
int f[N][N][N];
int power[N];

void dp()
{
	memset(f,0,sizeof f);
	f[0][0][0]=1;
	
	for (int pos=1;pos<=10;pos++)//在填的数位
	for (int i=0;i<=9;i++)//试填pos位上的数 
	for (int res1=0;res1<13;res1++)//前pos位的余数 
	{
		int res2=(res1-i*power[pos-1]%13+13)%13;//不包含第pos位的余数 
		if (i!=3) f[pos][res1][0]+=f[pos-1][res2][0];
		if (i!=1&&i!=3) f[pos][res1][0]+=f[pos-1][res2][1];
		
		if (i==3) f[pos][res1][1]+=f[pos-1][res2][0]+f[pos-1][res2][1];
		if (i==1) f[pos][res1][2]+=f[pos-1][res2][1];
		f[pos][res1][2]+=f[pos-1][res2][2];
	}
}
int solve(int n)
{
	int op1=0,res1=0,ans=0;//op1,res1针对n 
	for (int pos=10;pos>=1;pos--)
	{
		int s=n/power[pos-1];
		for (int i=s-1;i>=0;i--)
		{
			int op2,res2=(res1+i*power[pos-1])%13;//res1:包括第pos+1位 res2:包括第pos位的余数 针对n 
			if (op1!=2)
			{
				if (i==3&&op1==1) op2=2;
				else if (i==1) op2=1;
				else op2=0;
			}
			else op2=2;
			
			int res3=(13-res2)%13;//对后面几位的限制 
			if (op2==2) ans+=f[pos-1][res3][0]+f[pos-1][res3][1]+f[pos-1][res3][2];
			else if (op2==1) ans+=f[pos-1][res3][2]+f[pos-1][res3][1];		
			else ans+=f[pos-1][res3][2];
		}
		if (op1!=2)
		{
			if (s==3&&op1==1) op1=2;
			else if (s==1) op1=1;
			else op1=0;
		}
		res1=(res1+s*power[pos-1])%13;
		n%=power[pos-1];
	}
	if (op1==2&&res1==0) ans++;//特判n
	return ans;
}
signed main()
{
	power[0]=1;
	for (int i=1;i<=9;i++) power[i]=power[i-1]*10;
	while (scanf("%lld",&n)!=EOF)
	{
		dp();
		printf("%lld\n",solve(n));
	}
	return 0;
}

B.区间圆数

因为“圆数”和二进制状态有关,所以设的是二进制下的状态、在二进制的基础上转移的。设状态 fpos,cnt 表示填到第 pos 位,共出现 cnt0 的个数。那么,状态转移显然为

fpos,cnt=fpos1,cnt+fpos1,cnt1 ,f0,0=1

在统计答案时,可以有一个类似“前缀和”的思想:计算 [l,r] 内的圆数,就是 [1,r] 内的圆数数量减去 [1,l1] 内的圆数数量,所以只需要解决 [1,n] 内的圆数数量就可以求出答案了。

与上题类似,因为 n 的限制,统计时从高位往低位统计;因为无法统计到 n ,所以最后要特判 n

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=35;
int l,r;
int f[N][N];//已填到第i位,共有j个0的数的个数
int s[N],cnt; 

void dp()
{
	f[0][0]=1;
	for (int i=1;i<=32;i++)
	for (int j=0;j<=i;j++)
	{
		if (!j) f[i][j]=f[i-1][j];
		else f[i][j]=f[i-1][j]+f[i-1][j-1];
	}
}
int solve(int n)
{
	if (!n) return 0;
	int cnt0=0,cnt1=0,ans=0,cnt=0;
	while (n) s[++cnt]=n%2,n/=2;
	
	for (int pos=cnt;pos>=1;pos--)
	{
		if (s[pos]&&pos!=cnt)//前面与n相同但这位为0的情况 
		{
			cnt0++;
			for (int i=0;i<=pos-1;i++)
			{
				if (i+cnt0>=cnt-cnt0-i) ans+=f[pos-1][i];
			}
			cnt0--;
		}
		if (pos!=cnt)//前面都是前导0的情况 
		{
			for (int i=0;i<=pos-1;i++)
			{
				if (i>=pos-i) ans+=f[pos-1][i];
			}
		}
		cnt0+=!s[pos];
		cnt1+=s[pos];
	}
	if (cnt0>=cnt1) ans++;
	return ans;
}
signed main()
{
	dp();
	scanf("%lld%lld",&l,&r);
	int ans=solve(r)-solve(l-1); 
	printf("%lld",ans);
	return 0;
}

C.数字计数

可以想到设状态 fpos,x 表示填到第 pos 位,数码 x 共出现的次数。容易想到状态转移方程

fpos,x=fpos1,x×10+10 pos1

然后就会发现 x 这一维没用。然后简化一下就是

fpos=fpos1×10+10 pos1

直接转移就转移完了(感动哭

统计答案时,和上题同(“前缀和”思想)。然后统计就行

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=16;
int l,r;
int power[N],f[N];//f[pos][j]:第pos位数码j出现的次数 
int s[N],a[N],ans[N];

void dp()
{
	power[0]=1;
	for (int i=1;i<N;i++) power[i]=power[i-1]*10;
	for (int pos=1;pos<N;pos++) f[pos]=f[pos-1]*10+power[pos-1];
}
void solve(int n)
{
	memset(a,0,sizeof a);
	int len=0,tmp=n;
	while (tmp)//处理n的数位
	{
		s[++len]=tmp%10;
		tmp/=10;
	}
	
	for (int pos=len;pos>=1;pos--)
	{
		for (int i=0;i<=9;i++)
		{
			a[i]+=f[pos-1]*s[pos];//pos位填0~s[pos]-1时pos位之后的贡献
			if (i<=s[pos]-1) a[i]+=power[pos-1];//pos位填0_s[pos]-1时pos位产生的贡献
		}
		a[s[pos]]+=(n%power[pos-1]+1);//pos位填s[pos]时pos位产生的贡献 后面数因为有限制,所以不在此次计算贡献
		a[0]-=power[pos-1];//减去这一位填0是前导0时多算的答案
	}
}
signed main()
{
	dp();
	scanf("%lld%lld",&l,&r);
	
	solve(r);
	for (int i=0;i<=9;i++) ans[i]=a[i];
	solve(l-1);
	for (int i=0;i<=9;i++)
	{
		ans[i]-=a[i];//差分
		printf("%lld ",ans[i]);
	}
	return 0;
}

D.数位翻转

据说很毒瘤,等我心情好了时候回来补

E.幸运666

要想办法转化成上面 n 的限制的数位dp,然后发现可以二分做!二分的 mid 就是数位限制。然后统计就没了

专吃太强了%%%

贴 记搜版
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=12;
const int M=1e12;
int T,n;
int len,s[N];
int f[N][5];//第二位状态表示6的个数

int dfs(int pos,int c,int lim)
{
	if (!pos) return c==3;
	if (!lim&&f[pos][c]!=-1) return f[pos][c];
	
	int up=9,res=0;
	if (lim) up=s[pos];
	for (int i=up;i>=0;i--)
	{
		if (c==3) res+=dfs(pos-1,3,lim&&i==s[pos]);
		else if (i==6) res+=dfs(pos-1,c+1,lim&&i==s[pos]);
		else res+=dfs(pos-1,0,lim&&i==s[pos]);
	}
	
	if (!lim) f[pos][c]=res;
	return res;
}
int check(int x)
{
	len=0,memset(f,-1,sizeof f);
	while (x)
	{
		s[++len]=x%10;
		x/=10;
	}
	return dfs(len,0,1); 
}
signed main()
{
	scanf("%lld",&T);
	while (T--)
	{
		scanf("%lld",&n);
		int l=666,r=M;
		while (l<r)
		{
			int mid=l+r>>1;
			if (check(mid)>=n) r=mid;
			else l=mid+1;
		}
		printf("%lld\n",l);
	}
	return 0;
}

F.乘积计算

直接做做完了(现在一看记搜确实更方便捏

贴 记搜版
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=50;
const int MOD=1e7+7;
int n;
int s[N],len;
int f[N][N];//f[pos][sum]:填到pos位共有sum个1的乘积

int dfs(int pos,int sum,int lim)//lim表示第pos位填的数是否有上限
{
	if (pos==0) return max(1LL*1,sum);//以防全是0
	if (!lim&&f[pos][sum]!=-1) return f[pos][sum];//已搜过
	
	int up=1,ans=1;
	if (lim) up=s[pos];//上限
	
	for (int i=0;i<=up;i++) ans=(ans*dfs(pos-1,sum+(i==1),lim&&i==s[pos]))%MOD;
	
	if (!lim) f[pos][sum]=ans;
	return ans%MOD;
}
signed main()
{
	scanf("%lld",&n);
	while (n)//统计n二进制的数位
	{
		s[++len]=n%2;
		n>>=1;
	}
	memset(f,-1,sizeof f);
	printf("%lld",dfs(len,0,1));
	return 0;
}

G.奶牛编号

“辛辛苦苦”敲了二分,但因为炸裂的输出 long long 也浅浅爆炸了。

所以这题要用组合数做。

考虑什么时候填1与0。在剩下 cnt1 位里填 cnt2 个"1"共有 Ccnt1cnt2 种排列方案,若 Ccnt1cnt2 比我们想要的排名要大,就说明 cnt1 的范围取大了,所以输出 0 继续往后看;反之,则填 1 并减去对应的排名。

然后就做完了

这和数位dp有个damn的关系啊

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5;
int n,k,c[N][35];

void init()
{
	c[0][0]=1;
	for (int i=1;i<N;i++)
	{
		c[i][0]=1;
		for (int j=1;j<=30;j++) c[i][j]=c[i-1][j]+c[i-1][j-1];
	}
}
signed main()
{
	init();
	scanf("%lld%lld",&n,&k);
	if (n==1)
	{
		for (int i=1;i<=k;i++) printf("1");
		return 0;
	}
	if (k==1)
	{
		printf("1");
		for (int i=2;i<=n;i++) printf("0");
		return 0;
	}
	
	int cnt=k;
	while (c[cnt][k]<=n) cnt++;
	cnt--,k--;
	printf("1");
	
	for (int i=k;i<cnt;i++) n-=c[i][k];
	for (int i=1;i<=cnt;i++)
	{
		if (k==0) printf("0");
		else if (c[cnt-i][k]>=n) printf("0");
		else
		{
			printf("1");
			n-=c[cnt-i][k],k--;
		}
	}
	return 0;
}

H.魔法数字

记录出现的一位数可以状压解决,然后就是余数问题了

有一个小结论(感性理解永远的神):p=lcm(p1,p2,,pn),(x mod p) mod pi=x mod pi

所以可以直接记录模 lcm(1,2,,10)=2520 的余数,再最后判断就行。

于是乎可以设状态 fpos,opt,res 表示填到了第 pos 位,数字出现情况为 opt ,当前 mod 2520 的余数为 res 的个数。

但是这样开的空间太大,时间复杂度会爆炸。怎么办捏

我们要想办法优化一维,发现第三维可以缩。显然,判断一个数是否是 5 的倍数只需要看它的末尾就行了,这样了话第三维开 504 就行了,转移的时候额外转一个上一个数 lst 判断就好了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=20;
int k,l,r;
int s[N],len;
int f[N][(1<<9)+5][512];

inline int dfs(int pos,int opt,int ys,int lim,int lst)
{
	if (!pos)
	{
		int cnt=0;
		for (int i=1;i<=9;i++)
		{
			if (i!=5&&(opt&(1<<(i-1)))&&ys%i==0) cnt++;
		}
		if (opt&(1<<4)&&(lst==5||lst==0)) cnt++;
		return cnt>=k;
	}
	if (!lim&&f[pos][opt][ys]!=-1) return f[pos][opt][ys];
	
	int up=9,res=0;
	if (lim) up=s[pos];
	
	for (int i=0;i<=up;i++)
	{
		if (i) res+=dfs(pos-1,opt|(1<<(i-1)),(ys*10+i)%504,lim&&i==up,i);
		else res+=dfs(pos-1,opt,(ys*10+i)%504,lim&&i==up,i);
	}
	if (!lim) f[pos][opt][ys]=res;
	return res;
}
inline int solve(int x)
{
	len=0;
	while (x)
	{
		s[++len]=x%10;
		x/=10;
	}
	return dfs(len,0,0,1,0);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	memset(f,-1,sizeof f);
	cin>>k>>l>>r;
	cout<<solve(r)-solve(l-1);
	return 0;
}

I.异或个数

据说是道史题,那我就不做了

posted @   还是沄沄沄  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示