动态规划·数位DP

数位DP

好东西(?),但我不会

直接上例题吧

呃呃呃:

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

【YbtOj】题解

A.B数计数

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

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

其中 \(f_{0,0,0}=1\) ,状态从低位往高位转移即可。

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

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

这样统计是统计不到\(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.区间圆数

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

\[f_{pos,cnt}=f_{pos-1,cnt}+f_{pos-1,cnt-1}\ ,f_{0,0}=1 \]

在统计答案时,可以有一个类似“前缀和”的思想:计算 \([l,r]\) 内的圆数,就是 \([1,r]\) 内的圆数数量减去 \([1,l-1]\) 内的圆数数量,所以只需要解决 \([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.数字计数

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

\[f_{pos,x}=f_{pos-1,x}\times 10+10^{\ pos-1} \]

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

\[f_{pos}=f_{pos-1}\times 10+10^{\ pos-1} \]

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

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

#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\)"共有 \(C_{cnt1}^{cnt2}\) 种排列方案,若 \(C_{cnt1}^{cnt2}\) 比我们想要的排名要大,就说明 \(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(p_1,p_2,…,p_n),(x\ mod\ p)\ mod\ p_i=x\ mod \ p_i$$

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

于是乎可以设状态 \(f_{pos,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 @ 2024-12-16 20:11  还是沄沄沄  阅读(9)  评论(0编辑  收藏  举报