带通配符的数 CCF程序设计实训

题3:带通配符的数(20240826)

【问题描述】给定一个可以带通配符问号的正整数W,问号可以代表任意一个一位数字。再给定一个正整数X,和W具有同样的长度。问有多少个整数符合W的形式并且比X大?

【输入形式】多组数据,每组数据两行,第一行是W,第二行是X,它们长度相同,在[1..10]之间。

【输出形式】每行一个整数表示结果。

【样例输入】

36?1?8

236428

8?3

910

?

5

【样例输出】

100

0

4

对于这道题,一开始拿到私以为比较简单,像这种问题,我们可以用自己的视觉处理器和大脑一眼看出来答案,关键是给计算机设置一套行之有效的方法论。

回忆我们比较十进制数字的过程,提炼出计算机可以执行的有效方法,即个位对齐,数位多者大。对于数位相同者,从最高位开始依次比较,找到一个不相等的数位,即完全确定关系。根据休谟对人认知事物的方法论来说,我们已经提取到了事物的特征,接下来是推广方法。

回到正题,从刚刚的分析我们可以得知,对于相同位数的数字(题目中就是)如果我们在碰到问号之前就已经完成了比较,剩下的数字,就不需要比较了,这也就是说,剩下的数字不影响这个数与另一个数的相对大小关系了;也就是说,这里可以是任意数字。凡事都可以找到它共轭的一面,显然对于小于关系来说,这个结论也完全成立,可以用离散数学的偏序关系证明,在实数集上,大于和小于都有自反性,反对称性,可传递性。

那么接下来,考虑相等的情况,相等的话,其实等价于去掉那些相等的位数的数字来继续比较。比如12345和12346,前面的1234都是相等的,那它就没用,起决定性作用的只有5和6。现在,比较数字的三种情况已经讨论完毕,我以为这道题已经解决,交上去,发现是错的,因为我忽视了这个问号取任意数字对该数字的后效影响!可以看出,对于大于和小于关系,基本上就是一次定性,那问号取什么值不行?可是对于我们的等号来说,前面问号的取值确实是会对后面问号的取值产生影响,我们要求问号取了值以后,数字要比另外一个数大,那么比如说对于?和5,我可以取6,7,8,9四个值。肯定不能取0,1,2,3,4,5。但是对于?6和55来说,?可以取5、6、7、8、9。这个时候,发现等号也是可以取的!

现在知道了,我们之前对于事物的认识不完全准确,马克思主义告诉我们,人民对事物的认识是螺旋上升的,是发展的,通过对数据的研究和穷举,还有之前的失败,我们扩充了现有的方法论。发现取等的情况可以细分为两种情况,下面为了方便叙述,我把含有问号的数叫做A,被比较的数叫做B。对于在碰到?之前所有的数位上的数值都相等的情况,A的?可以取得大于B该数位的数,如果A在?后的数比B在同等情况下取得的数大,那么?可以取得等于B该数位的数。

我们现在的算法已经可以很好地处理单一?的情况,但是对于多个不确定的数位,还无法很好地解决,为什么呢?看一个例子:对于如下两个数:

11?51?

114515

我的第一处?如果取得4,后面那个?的数字就要取得比5大,如果第一处取得严格大于4的数字,那么后面随便取。那也就是说,如果像这个例子一样,我们的取值划分出两种情况,那么对于更多的?情况也更加复杂。

对于数据无限长,状态无限多的情况,我们一般采用递归或者栈来解决。对于栈的话,我们可以存储抽象状态,然后逐个解决,这样比较省堆栈,但是这题数据比较水,就用递归了。

我觉得,写好递归,就要熟悉自己的决策树,知道自己的决策树是怎么开枝散叶的,这一点很重要。我们要思考转移的状态和做决策的条件,就得先想一想递归什么时候结束,那就是扫描到最后一个数字的时候结束,还有就是我可以直接知道这个子问题的方案数量的时候结束。我们可以搞一个用来确定上一个函数状态的参数sit,sit为1的时候是=,为2是>,为3是<。如果sit是2,3,那程序就直接出结果了(这是很好的剪枝),如果是1的话,还得继续递归。

既然是统计方案数,那么我们写一个int型的函数,主程序部分开始扫描,就像前面的方法论提到的一样,依次比较每个数位的大小,找到?,然后确定此时的状态,大于小于的情况上文已经说明了,小于的话直接返回0吧,大于的话,统计剩下问号的个数x,然后算出10的x次幂,然后将x乘以之前已经算出来的方案数(根据乘法原理),就是最终方案。对于等于的情况,需要分情况讨论,我们前面搞的sit参数有用了。令方案数为sum,根据组合数学的乘法原理与加法原理,sum总=(当前取大数的方案数)*sum(?取了大数的情况)+1(这个就是取了相等的数)*sum(取了相等的数的情况)。

这个方程就是状态转移方程了,其实我们可以写成动态规划的形式的,但是确实懒得写了,因为数据太水了。思路想了一下,先用记忆化搜索吧,记录一下当时的状态,毕竟动态规划本质就是把算过的东西记录下来。这里写有难度是,我个人是觉得,因为我们这里递归要的条件参数是这个?左边的大小关系,这个是可以唯一确定的,但是动态规划递推的时候,我们要知道?右边的大小关系(递过去,然后归来的时候我们才能知道),这个关系不太好求得,因为始终是靠左的数占主导,要增加多余的循环,我确实就卡在这了,我还是挺想知道DP解法的。

我们把两个数读起来,用向量存储这两个数的每一位,到时候写递归直接以只读的模式引用就好了,变得参数是一个指针,这样就可以降低内存开销,这是我在学二叉树的前序遍历后序遍历构造一棵二叉树那道题的时候得到的技巧。

然后接下来写具体的边界条件,对于大于和小于的情况,一旦判断到我们就可以直接出答案了,这里就略去。对于之前的状态是等于的话,它的边界条件,也就是那个指针指到数组末尾的话,我们这里还是要判断一下的,不能直接return 0啊,比如1?5和194这两个数,现在处理到?,传参了,发现那个参数是=的情况,有两个分支,大于的那个不说了,接下来说等于的情况,我们还是要判断下剩下的数是大于还是小于等于,如果是大于的话就说明可以取等,得return 1,这个和我们之前说过的递归主体语句一样,但是我在写边界条件甚至是造数据的时候也没有发现这种问题,一定要记住,到了底,一定还要再判断一次!要不然死得很惨。

接下来是扫描,遍历一遍那个数组,然后判断大小关系,判断好了以后传参。然后在最后写一下到了结尾的那个终止条件,因为最后一个?过去以后没办法再进新的堆栈了,得有返回值。

36?1?8
236428

因为3>2,第一个?可以任取,加上前面的代销关系第二个?也可以任取,最后答案是100。

一定得好好考虑递归的终止条件,很容易漏情况,因为很多时候递归到最后其实是还剩最后一点信息没有确定的,然后离成功只有一步之遥,你忘记了,计算机也想不起来,这道题就没了。还有就是在脑海里面想象一下你的决策树,和你的代码的return是否对的上,要不然就会返回一个任意值。这不是叫你考虑递归的细节,而是状态转移的过程!

其实思考递归问题,就是先用常法来做,然后发现会有无限复杂的重复子过程(如果不是这个就换种思路),然后就马上出来了,注意边界条件!

#include <iostream>
#include <vector>
#include <cmath>
#include <unordered_map>
#include <map>
#include <cstring>
#include <sstream>
#include <fstream>
using namespace std;
int solve(const vector<int>& num1,int p1,const vector<int>& num2,int len,int sit){
	if(p1==len-1){
		if(num1[p1]==15&&sit==1){
			return 9-num2[p1];
		}else if(num1[p1]==15&&sit==2){
			return 10;
		}else if(sit==2){
			return 1;
		}else if(sit==1&&num1[p1]>num2[p1]){
			return 1;//这里没有注意到收尾还有这情况,情况没有分类,导致讨论的时候不清晰
		}else{
			return 0;
		}
	}
	if(sit==3){
		return 0;
	}
	if(sit==2){
		for(int i=p1;i<len;i++){
			if(num1[i]==15){
				return 10*solve(num1,i+1,num2,len,2);
			}
		}
		return 1;
	}
	int signl=0,signs=0,signe=1;
	//int res=0;
	for(int i=p1;i<len-1;i++){
		if(num1[i]==15){
			if(signl==1){
				return 10*solve(num1,i+1,num2,len,2);
			}
			if(signs==1){
				return 0;
			}
			if(signe==1){
				return (9-num2[i])*solve(num1,i+1,num2,len,2)+solve(num1,i+1,num2,len,1);
			}
		}
		if(num1[i]>num2[i]&&signs==0&&signl==0){
			signl=1;
		}
		//&&signs==0&&signl==0保证只进入一次这里
		if(num1[i]<num2[i]&&signs==0&&signl==0){
			signs=1;
		}
		if(num1[i]==num2[i]&&signs==0&&signl==0){
			signe=1;
		}//只要有一个数不相等,这里就是0
		else{
			signe=0;
		}
	}
	if(signl==1){
		return solve(num1,len-1,num2,len,2);
	}
	if(signs==1){
		return 0;
	}
	else {
		return solve(num1,len-1,num2,len,1);
	}
}
int main(){
	char ch1[101];char ch2[101];
	while(cin>>ch1>>ch2){
		vector<int> num1;vector<int> num2;
		int len1=strlen(ch1);//len2=strlen(ch2);
		for(int i=0;i<len1;i++){
			num1.push_back(ch1[i]^'0');
			num2.push_back(ch2[i]^'0');
		}
		cout<<solve(num1,0,num2,len1,1)<<endl;//1=,2>,3<;
		//cout<<ch1<<' '<<ch2<<endl;
	}
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值