题目:[NOIP 1998 普及组] 三连击
题号:三连击
难度:P1008
精华部分:利用短路&的短路效应来高效缩减时间复杂度,
相比于该题大多数题解的O(9!), 而该题解只有O(1),快到飞起。
题目分析
存在三个三位数,a1,a2,a3,这三位数中的九个数字各不相同(1~9)(都不为0)
且满足 a1:a2:a3=1:2:3
问题在于如何用代码使得九个数字三三组合成三个数,且成1:2:3比例。
思路
如果是多重循环遍历暴力计算,那肯定行不通,10^9 基数太大。
首席我的思路是:
1,既然三个三位数的九个数字各不相同,那么可确定一个范围
a1>=102 , a3<=987,a1<a2<a3; 然而这有什么用呐,往下看
2,知道范围,并且a1:a2:a3=1:2:3,也就是a3 = 3*a1;,而a3<=987,则a1<=987/3=329
判断得出a1具体范围 102<=a1<=329,同理,
102*2<=a2<=329*2, 需要注意这里a2最小值并不是102*2=204
需要去除与a1的交集得出 329<=a2<=658.后面的范围肯定就是a3了
总结范围
102<=a1<=329, 329<a2<=658 ,658<a3<=987.
那既然确定了一个范围,那是否可以开始遍历了,nonono,不到万不得已不要想着暴力枚举。此时的暴力枚举基数仍在(329-102)*(658-329)*(987-658)>10^6。
得到的上述范围并不全是有用的,虽然我们还可以根据一些规律来不断缩小范围,(例如:数字1,2,3肯定在a1中之类的条件)但是没有这个必要了。
核心思路
关于上述范围,我们只需要 102<=a1<=329 这个,从这个基础上遍历这329-102 =227种情况
通过 i 遍历该范围 让 c1 = 2*i 表示第二个数 c2 = 3*i 来表示第三个数,此时是以比例关系来确定的后两个数。(最后通过函数判断枚举出的数是否生效)
你肯定会发现,直接通过比例关系生成的后两个三位数a2,a3,大多数都不满足三个数字互异,此时我们需要一些函数来筛选。
函数代码解释
1,检查三位数中的三个数字的互异,并且都不为0 (这个较为简单不做解释)
int pd(int num) {
int a1 = num%10;
int a2 = (num/10)%10;
int a3 = (num/100)%10;
if(a1==a2|| a1==a3 || a2==a3 ||a1==0 || a2==0 || a3==0)
return 0;
return 1;
}
2,检查三个三位数中的九个数都互异(存在一定的时间复杂度,不可循环运行太多次)
int pd2(int num1,int num2,int num3) {
int a[10],n=0;
while(num1>0) {
a[n++]=num1%10;
num1/=10;
}
while(num2>0) {
a[n++]=num2%10;
num2/=10;
}
while(num3>0) {
a[n++]=num3%10;
num3/=10;
}
for(int y=0;y<8;y++)
for(int x=y+1;x<9;x++)
if(a[y]==a[x])
return 0;
return 1;
}
(该函数是将三个三位数中的九个数字全都分离出来,并且储存进临时数组,然后遍历枚举判断)
我才你肯定会疑惑,既然能直接判断九个数字的互异,那干嘛还要第一个函数来判断三个数互异。
原因就是:利用短路&的短路效应来高效缩减时间复杂度,接下来是代码核心枚举计算部分
for(int i=100; i<=329; i++)
if(pd(i)){
c1 = i * 2;
c2 = i * 3;
if(pd(c1) && pd(c2) && pd2(i,c1,c2)) {
printf("%d %d %d\n",i,c1,c2);
}
}
从代码中我们可以看到,先判断pd(c1)如果c1三个数不是互异的,那就直接进入下一次枚举了,如果满足条件,则再判断pd(c2),只有满足前两个至关重要的条件,才进行最后一条九个数的互异判断,因为pd函数只是整数分离然后比较判断,占用时间很短,而pd2的9个数判断是需要嵌套循环判断的,时间复杂度较大,所以只有经过前两个pd判断之后,确认三个三位数都是独自互异的且成比例,然后再判断是否九个数都互异。
总的代码
#include <stdio.h>
int pd(int num) {
int a1 = num%10;
int a2 = (num/10)%10;
int a3 = (num/100)%10;
if(a1==a2|| a1==a3 || a2==a3 ||a1==0 || a2==0 || a3==0)
return 0;
return 1;
}
int pd2(int num1,int num2,int num3) {
int a[10],n=0;
while(num1>0) {
a[n++]=num1%10;
num1/=10;
}
while(num2>0) {
a[n++]=num2%10;
num2/=10;
}
while(num3>0) {
a[n++]=num3%10;
num3/=10;
}
for(int y=0;y<8;y++)
for(int x=y+1;x<9;x++)
if(a[y]==a[x])
return 0;
return 1;
}
int main() {
int c1,c2;
for(int i=100; i<=329; i++)
if(pd(i)){
c1 = i * 2;
c2 = i * 3;
if(pd(c1) && pd(c2) && pd2(i,c1,c2)) {
printf("%d %d %d\n",i,c1,c2);
}
}
return 0;
}
这种题最需要注意的其实就是如何优化时间复杂度,因为本身并不是难的逻辑,需要的是实现形式
(需要注意的还有一点,遍历 i 的那一步,需要直接在遍历的代码块内完成判断,不可以将遍历得到的满足pd的结果存到数组中再进行遍历有效数组进行判断,这样虽然思路清晰一些,但是内存会不满足条件)
错误示范:
int a[1000],an,c1,c2;
for(int i=100; i<=333; i++)
if(pd(i))//先把有效的a1储存起来
a[an++]=i;
an--;//最大下标是an
for(int i=0; i<=an; i++) {//遍历a1寻找a2,a3
c1 = a[i] * 2;
c2 = a[i] * 3;
if(pd(c1) && pd(c2) && pd2(a[i],c1,c2)) {
printf("%d %d %d\n",a[i],c1,c2);
}
上述这个代码会不满足某些内存要求。谨慎使用。
这种解法我认为相比于别的题解来说,更加容易理解,大多数别的方法先枚举出9个数然后组合判断,后面有一点绕弯,时间复杂度大多在O(9!)而我们的函数时直接高效枚举到指定数,然后判断是否生效即可。我们的代码时间复杂夫只有O(1),快到飞起。
关于题解我甚至还看到个九层嵌套循环的,时间复杂度在O(9^9),不用理会这种方法,除了让你脑子越用越笨。