1585 例题1 Amount of Degrees(Ural 1057 LOJ10163) 进制转换纯枚举从40分到60分 数位DP100分 初次使用string

总目录

在线测评地址(ybt)

在线测评地址(LOJ)

看完题目,感觉就是一个进制转换。

样例数据如下:

17对应2进制10001 数1的个数有2个
18对应2进制10010 数1的个数有2个
20对应2进制10100 数1的个数有2个

该题有个极端的测试数据:

1 2^31-1
20
2

即
1 2147483647
20
2

很明显,AC的解法,一定是成片的计算数量。

目前能怎么办?从初级的算法入手,慢慢改进,看能到什么程度。

1.进制转换,纯枚举,输出符合的数量.

样例通过,提交40分

ybt

未通过

测试点结果内存时间
测试点1答案正确620KB2MS
测试点2答案错误616KB2MS
测试点3运行超时580KB995MS
测试点4运行超时596KB998MS
测试点5运行超时580KB998MS
测试点6答案正确612KB63MS
测试点7运行超时588KB1000MS
测试点8运行超时580KB998MS
测试点9运行超时580KB998MS
测试点10运行超时584KB998MS

LOJ

 40分代码如下:

#include <bits/stdc++.h>
using namespace std;
int x,y,k,b;
void init(){
	scanf("%d%d%d%d",&x,&y,&k,&b);
}
int count1(int a){
	int cnt=0;
	while(a){
		cnt+=((a%b)==1);//统计1的数量 
		a/=b;
		if(cnt>k){//优化,避免无谓的计算 
			cnt=-1;
			break;
		}
	}
	return cnt;
}
void solve(){
	int i,ans=0;
	for(i=x;i<=y;i++){
		ans+=(count1(i)==k);
	}
	printf("%d\n",ans);
}
int main(){
	init();
	solve();
	return 0;
}

超时可以理解,有测试点错误,不可接受,想不通。

突然想到一组测试数据:

输入:
1 9
1 3

输出:
3

有效的是
1=1*3^0
3=1*3^1
9=1*3^2

然而上述代码对应的输出是5

很显然5=2*3^0+1*3^1等无效数据也被统计了。

ybt

未通过

测试点结果内存时间
测试点1答案正确608KB2MS
测试点2答案正确612KB2MS
测试点3运行超时576KB998MS
测试点4运行超时584KB998MS
测试点5运行超时580KB998MS
测试点6答案正确616KB92MS
测试点7运行超时576KB996MS
测试点8运行超时576KB998MS
测试点9运行超时572KB998MS
测试点10运行超时572KB997MS

LOJ

 60分代码如下:

#include <bits/stdc++.h>
using namespace std;
int x,y,k,b;
void init(){
	scanf("%d%d%d%d",&x,&y,&k,&b);
}
int count1(int a){
	int cnt=0;
	while(a){
		if(a%b==1)cnt+=1;//统计1的数量
		else if(a%b){//系数不是1 
			cnt=-1;
			break;
		} 
		a/=b;
		if(cnt>k){//优化,避免无谓的计算 
			cnt=-1;
			break;
		}
	}
	return cnt;
}
void solve(){
	int i,ans=0;
	for(i=x;i<=y;i++){
		ans+=(count1(i)==k);
	}
	printf("%d\n",ans);
}
int main(){
	init();
	solve();
	return 0;
}

2.数位DP​​​​​​

由于所求的幂互不相等,符合条件的数写成B进制一定只由01组成。

因此我们只考虑B=2的情况。

此处区间满足区间减法,即count(X,Y)=count(0,Y)-count(0,X-1)

因此我们唯一需要求的就是,给定n,求出所有不超过n的正整数中,其二进制表示恰好含有K"1"的有多少。

这里,我使用一棵完全二叉树来代表一个区间内的数。

例如图中高度为4的完全二叉树就可以代表所有长度为4的二进制数。

(左枝是0,右枝是1,有点像哈夫曼编码。)

其范围为[0,2^4-1]

每个叶子就代表了一个数。

假设K=3 n=13,其二进制为1101

为了方便起见,树的根( 人为引入 )用 0 表示。这样,这棵高度为 4 的完全二叉树就可以表示所有 4
位二进制数( 0..2^ 4 -1 ),每一个叶子节点代表一个数。其中,红色路径表示 n 。所有小于 n
数组成了三棵子树,分别用蓝色、绿色、紫色表示。因此,统计小于 13 的数,就只需统计
这三棵完整的完全二叉树:统计蓝子树内含 3 1 的数的个数、统计绿子树内含 2 1 的数
的个数(因为从根到此处的路径上已经有 1 1 ),以及统计紫子树内含 1 1 的数的个数。
注意到,只要是高度相同的子树统计结果一定相同。而需要统计的子树都是“右转”时遇到
的( 请注意:统计的全是左子树 )。当然,我们不能忘记统计 n 本身。实际上,在算法最初时将 n 自加 1 ,可以避免讨论 n
本身,但是需要注意防止上溢。

很容易递推求出这个问题:

f[h,s]表示在高度为h(h>=1,因根节点0是人为引入)的完全二叉树包含的数中(范围是[0,2^h-1]),二进制中恰含s个1数有多少。(注意s<=h,因具体到某个数,是从根节点走到叶节点经历的01)

f[h,s]  =  f[h-1,s] (左子树中的个数) +  f[h-1,s-1](右子树中的个数)

(注:f[h,s]根是0

f[h-1,s]左子树的根是0,故除左子树根节点0外,还需s个1.

f[h-1,s-1]右子树的根是1,故除右子树根节点1外,还需s-1个1.)

剩下的问题就是,如何将任意进制问题转化为二进制问题。

(若左起有大于1的数位,下面转换对右边界Y没有问题,转换只是将不大于Y的符合题意的数找出,因Y本身不符合题意,故右边界没有少计数,也没有多计数。)

(若左起有大于1的数位,下面转换对左边界X没有问题,转换只是将不大于X的符合题意的数找出,因X本身不符合题意,故左边界没有少计数,也没有多计数。)

(思维难点)只需贪心将nB进制表示转化为只含01:找到n的左起(左是高位)第一位大于1的数位,将它以及它右边所有数位赋为1(即找到最接近n的符合题意的数)

递推求f的时间复杂度为O(log^2(n)),每次询问的时间复杂度为O(log(n)),因此总时间复杂度为O(log^2(n))

问题得到完美解决!

实际上,我们也可以只用“位”的思想来考虑这个问题,而不使用树的思想。

我们每次找到n"1"数位,将它赋为0,则其右面的数位可任取01

可根据组合数算出满足题意的数的个数。

穷举所有“1”数位,计算组合数,即可完成询问。

事实上,f的递推公式正是组合数公式!

与树的思想异曲同工!

 使用高度为h的完全B叉树表示所有长度为hB进制数,思考起来更加直观,在处理较复杂问题时优点明显。

当然,最终程序我们并不真正建树,它的作用只是帮助我们思考。

无论从树结构考虑还是从数位的角度考虑,基本思想都是:

 逐位确定

ybt

通过

测试点结果内存时间
测试点1答案正确600KB2MS
测试点2答案正确604KB1MS
测试点3答案正确604KB1MS
测试点4答案正确616KB1MS
测试点5答案正确600KB1MS
测试点6答案正确608KB1MS
测试点7答案正确604KB1MS
测试点8答案正确600KB1MS
测试点9答案正确600KB1MS
测试点10答案正确612KB1MS

LOJ

#include <bits/stdc++.h>
using namespace std;
int x,y,k,b;
int f[35][35];
void init(){
	scanf("%d%d%d%d",&x,&y,&k,&b);
}
int i2b(int x){//将整数x转换成B进制 
	string s;
	int i,j;
	while(x){//将整数x转换成字符串,逆序存储 
		s=s+char(x%b+'0');//注意 2<=B<=10,故s%b的值落在[1,9]区间 
		x/=b;
	}
	for(i=0;i<s.size();i++){
		if(s[i]>'1')//若i位大于1,将0-i位之间数据设置为1 
			for(j=0;j<=i;j++)
				s[j]='1';
	}
	x=0;
	for(i=0;i<s.size();i++)//以01形式重写x 
		x|=((s[i]-'0')<<i);
	return x;
}
void fx(){//处理手法,类杨辉三角 
	int i,j;
	//全是0,全是1,均只有一种情形,初始化,需注意
	for(i=0;i<=31;i++)//边界初始化 
		f[i][0]=f[i][i]=1; 
	for(i=1;i<=31;i++)
		for(j=1;j<i;j++)
			f[i][j]=f[i-1][j]+f[i-1][j-1];
}
int count(int x){
	int i,j,tot=0,ans=0;
	for(i=31;i>0;i--){//自高位向低位扫描 
		if(x&(1<<i)){
			tot++;
			if(tot>k)break;
			x^=(1<<i);//将第i位从1置为0 
		}
		if((1<<(i-1))<=x)//寻找合适的左子树 
			ans+=f[i-1][k-tot];
	}
	if(tot+x==k)ans++;//判断自身是否相同
	return ans; 
}
int main(){
	init();
	x=i2b(x-1);
	y=i2b(y);
	fx();
	printf("%d\n",count(y)-count(x));
	return 0;
}

该题习得什么?

第一次对数位DP有了印象,心心念的二进制位操作出现了。

在统计ans过程中,细节较多,该题若作为试题,100分的代码初次编写时很难完成的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值