1 条题解

  • 0
    @ 2025-8-24 21:45:22

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar whiteqwq
    寻找着梦与现实的交点 在哪呢 在哪呢

    搬运于2025-08-24 21:45:22,当前版本为作者最后更新于2020-06-12 22:31:08,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    P3107 [USACO14OPEN]Odometer S解题报告:

    更好的阅读体验

    题意

    题意:给定区间[l,r][l,r],求区间至少一半的数位相同的数数量。

    数据范围:1lr10181\leqslant l\leqslant r\leqslant 10^{18}

    数位dp的普通题,但是有些细节需要注意。

    分析

    这里,我会把我所有的思考过程记录下来:

    观察题面,发现这道题很显然是数位dp。

    我们发现处理区间至少一半的数位相同的数的数量不好做,那么就把它拆成处理每个数字kk出现超过一半的数的数量。

    我们可以先写出calc\text{calc}函数的板子(即拆解数位):

    int calc(int x,int k){
    	memset(f,-1,sizeof(f));
    	for(len=0;x;x/=10)
    		num[++len]=x%10;
    	return dfs1(...);
    }
    

    这里calc(x,k)\text{calc(x,k)}指拆解xx的数位为numnum数组,并求小于等于xx,且数字kk不少于一半的数的个数的函数。

    然后开始写记忆化搜索的dfs\text{dfs}函数,考虑有哪些值可以影响答案:

    • 首先是len,k,flg1len,k,flg1,这三个数分别代表当前处理的数的长度,求解的数字kk和卡位的标志。(卡位的标志flg1flg1在当前选择的数与前面的数完全相同时为11,否则为00。卡位的标志影响答案的原因:如果flg1=1flg1=1,则当前数位只能取到num[len]num[len],因此答案会有差别)
    • 为了处理答案(数字kk必须超过一半),我们要记录两个参数cnt1,cnt2cnt1,cnt2,分别指kk出现的次数与非kk的数字出现的次数。
    • 发现前导零也会对答案造成影响(因为位数可能不一,这样会导致前导零。而前导零中的00是不能算到cnt1cnt1cnt2cnt2中的),因此我们也要记录前导零的标志flg2flg2

    这样,dfs\text{dfs}函数的的参数就出来了:int dfs1(int len,int k,int cnt1,int cnt2,int flg1,int flg2)

    然后是记忆化搜索的套路:用数组f[len][cnt1][cnt2][flg1][flg2]f[len][cnt1][cnt2][flg1][flg2]表示处理长度为lenlen,当前钦定的数出现次数出现cnt1cnt1次,非当前钦定的数出现cnt2cnt2次,卡位标志为flg1flg1,前导零标志为flg2flg2的答案。

    考虑转移也很简单:枚举数位ii,然后对所有可以放得下(flg1=0flg1=0inum[len]i\leqslant num[len])的方案进行转移,注意在cnt1cnt1cnt2cnt2进行转移的时候需要判断前导零,即:res+=dfs1(len-1,k,cnt1+(i!=0||flg2==0)*(i==k),cnt2+(i!=0||flg2==0)*(i!=k),i==num[len]&&flg1,i==0&&flg2);

    这样,dfs\text{dfs}函数也出来了:

    int dfs(int len,int k,int cnt1,int cnt2,int flg1,int flg2){
    	if(len==0)
    		return cnt1>=cnt2;
    	if(f[len][cnt1][cnt2][flg1][flg2]!=-1)
    		return f[len][cnt1][cnt2][flg1][flg2];
    	int res=0;
    	for(int i=0;i<=9;i++)
    		if(i<=num[len]||flg1==0)
    			res+=dfs1(len-1,k,cnt1+(i!=0||flg2==0)*(i==k),cnt2+(i!=0||flg2==0)*(i!=k),i==num[len]&&flg1,i==0&&flg2);
    	return f[len][cnt1][cnt2][flg1][flg2]=res;
    }
    

    虽然这样能过样例,但是交上去后会收获WA的好成绩。

    这个程序有两点错误:

    1. 没开long longlong\ long
    2. 求解发生错误。

    第二点是为什么呢?我们举一个例子:112212112212,这个数可以在k=1k=1时产生贡献,也会在k=2k=2时产生贡献,即一个长度为偶数的数有可能会出现两个数出现次数同时不少于一半。

    此时我们可以使用容斥,将总数减去这些特殊的数,在这里为了方便思考,我把这种特殊的数的求解用了另一个函数calc2\text{calc2},当然也用了另一个记忆化搜索函数dfs2\text{dfs2}

    calc2\text{calc2}的代码很容易写出,这里就不单独提供了(后面有完整代码),我们讲一下dfs2\text{dfs2}的写法: 首先带入的参数有些变化:

    1. 增加了一个k2k2,代表另一个求解的数。
    2. cnt2cnt2的意义改成k2k2的出现次数。

    记忆化很容易写出,然后就是转移了:

    由于这个数只由两个数字组成,我们不需要循环,只需要把循环展开就好了,即写两个ifif,再把计算贡献的copycopy下来就okok了。(不过这里要记得考虑一下cnt2cnt2的转移)

    long long dfs2(long long len,long long k1,long long k2,long long cnt1,long long cnt2,long long flg1,long long flg2){
    	if(len==0)
    		return cnt1==cnt2;
    	if(f[len][cnt1][cnt2][flg1][flg2]!=-1)
    		return f[len][cnt1][cnt2][flg1][flg2];
    	long long res=0;
    	if(k1<=num[len]||flg1==0)
    		res+=dfs2(len-1,k1,k2,cnt1+(k1!=0||flg2==0),cnt2,k1==num[len]&&flg1,k1==0&&flg2);
    	if(k2<=num[len]||flg1==0)
    		res+=dfs2(len-1,k1,k2,cnt1,cnt2+(k2!=0||flg2==0),k2==num[len]&&flg1,k2==0&&flg2);
    	return f[len][cnt1][cnt2][flg1][flg2]=res;
    }
    

    但是交上去后发现并没有这么简单,还是WA。

    为什么呢?这里我想了很久,发现前导零没有考虑:因为位数不一定,因此要考虑前导零的转移(记得不要和k1k1k2k2的转移重复计算贡献):

    	if(k1!=0&&k2!=0&&flg2==1)
    		res+=dfs2(len-1,k1,k2,cnt1,cnt2,num[len]==0&&flg1,flg2);
    

    代码

    注意,这里数位dp采用更简单的记忆化搜索形式。

    #include<stdio.h>
    #include<string.h>
    const int maxl=25;
    long long i,j,k,m,n,len,ans;
    long long num[maxl],f[maxl][maxl][maxl][2][2];
    long long dfs1(long long len,long long k,long long cnt1,long long cnt2,long long flg1,long long flg2){
    	if(len==0)
    		return cnt1>=cnt2;
    	if(f[len][cnt1][cnt2][flg1][flg2]!=-1)
    		return f[len][cnt1][cnt2][flg1][flg2];
    	long long res=0;
    	for(long long i=0;i<=9;i++)
    		if(i<=num[len]||flg1==0)
    			res+=dfs1(len-1,k,cnt1+(i!=0||flg2==0)*(i==k),cnt2+(i!=0||flg2==0)*(i!=k),i==num[len]&&flg1,i==0&&flg2);
    	return f[len][cnt1][cnt2][flg1][flg2]=res;
    }
    long long calc1(long long x,long long k){
    	memset(f,-1,sizeof(f));
    	for(len=0;x;x/=10)
    		num[++len]=x%10;
    	return dfs1(len,k,0,0,1,1);
    }
    long long dfs2(long long len,long long k1,long long k2,long long cnt1,long long cnt2,long long flg1,long long flg2){
    	if(len==0)
    		return cnt1==cnt2;
    	if(f[len][cnt1][cnt2][flg1][flg2]!=-1)
    		return f[len][cnt1][cnt2][flg1][flg2];
    	long long res=0;
    	if(k1<=num[len]||flg1==0)
    		res+=dfs2(len-1,k1,k2,cnt1+(k1!=0||flg2==0),cnt2,k1==num[len]&&flg1,k1==0&&flg2);
    	if(k2<=num[len]||flg1==0)
    		res+=dfs2(len-1,k1,k2,cnt1,cnt2+(k2!=0||flg2==0),k2==num[len]&&flg1,k2==0&&flg2);
    	if(k1!=0&&k2!=0&&flg2==1)
    		res+=dfs2(len-1,k1,k2,cnt1,cnt2,num[len]==0&&flg1,flg2);
    	return f[len][cnt1][cnt2][flg1][flg2]=res;
    }
    long long calc2(long long x,long long k1,long long k2){
    	memset(f,-1,sizeof(f));
    	for(len=0;x;x/=10)
    		num[++len]=x%10;
    	return dfs2(len,k1,k2,0,0,1,1);
    }
    int main(){
    	scanf("%lld%lld",&n,&m);
    	for(i=0;i<=9;i++)
    		ans+=calc1(m,i)-calc1(n-1,i);
    	for(i=0;i<=9;i++)
    		for(j=i+1;j<=9;j++)
    			ans-=calc2(m,i,j)-calc2(n-1,i,j);
    	printf("%lld\n",ans);
    	return 0;
    }
    
    • 1

    信息

    ID
    2171
    时间
    1000ms
    内存
    125MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者