【输入】
仅一行,包含有三个整数(用一个空格分开),分别表示P、L、K。
【输出】
一个整数。
表示合理的数列个数。
【样例】
输入:
332
输出:
22
【样例说明】
121212312
122213313
123221321
131222322
132223323
133231331
232332
233333
(合理的数列个数共22种)。
【算法分析】
显然这是一道数学题,我们就应该考虑用数学方法来解决。
我们很容易得到这样的一个递推式:
设
为长度为n,且末尾有t个1的数列总数。
那么有递推式:
最后求
。
初始边界为
显然,这个递推式的复杂度为O(L*K*K)即O(L3)左右,因为L仅为30,因此即使再加上高精度运算,这个复杂度还是可以忍受的。
但是在二维数组中使用高精度有些繁琐,所以我们还可以进一步推导,将
(1)式代入
(2)式可以得到:
我们可以将这个等式转化为一维的式子,即:
其中
表示长度为i,末尾不以1结尾的数列个数。
最后求的值相应变为
。
初始边界变为
空间复杂度降为O(L),时间复杂度也降为O(L*K)。
为了获得更快的速度,进一步推导该式子,有:
该式子的适用范围为n≥2,所以边界条件增加1个为
(因为
无法由该式推出)由于K不等于1,所以这个边界是可以的。
由于这道题最后的数据极大(约为4*1019),所以必然考虑要用高精度计算,为了简化运算,我们可以发现,当n≤k时,后面的
始终为0,也即有:
这样,可以省去数组f中的负数部分。
这样一来,时间复杂度进一步降低为O(L),数据量再大一些也可以承受。
显然,根据该递推式可以考虑O
(1)的算法,但那需要更大量的数学推导,限于篇幅,在这里就不进一步赘述了。
有兴趣的同学可以进一步自行研究。
【程序清单】
ProgramCount;
Type
Num=Array[0..100]ofInteger;
Var
P,L,K,I:
Longint;
F:
Array[0..31]ofNum;
Ans:
Num;
ProcedureCheng(Str:
Num;N:
Longint;VarStr1:
Num);
Var
I:
Longint;
Begin
ForI:
=1toStr[0]do
Str1[I]:
=Str[I]*N;
ForI:
=1toStr[0]do
Begin
Str1[I+1]:
=Str1[I+1]+Str1[I]Div10;
Str1[I]:
=Str1[I]Mod10;
End;
IfStr1[Str[0]+1]<>0
ThenStr1[0]:
=Str[0]+1
ElseStr1[0]:
=Str[0];
End;
ProcedureAdd(Str:
Num);
Var
I,T:
Longint;
Begin
IfStr[0]>Ans[0]
ThenT:
=Str[0]
ElseT:
=Ans[0];
ForI:
=1toTdo
Ans[I]:
=Ans[I]+Str[I];
ForI:
=1toTdo
Begin
Ans[I+1]:
=Ans[I+1]+Ans[I]Div10;
Ans[I]:
=Ans[I]Mod10;
End;
Ans[0]:
=T;
IfAns[T+1]<>0ThenAns[0]:
=T;
End;
ProcedureMinus(Str,Str1:
Num;VarStr2:
Num);
Var
I:
Longint;
Begin
ForI:
=1toStr[0]do
Str2[I]:
=Str[I]-Str1[I];
ForI:
=1toStr[0]do
WhileStr2[I]<0do
Begin
Str2[I+1]:
=Str2[I+1]-1;
Str2[I]:
=Str2[I]+10;
End;
WhileStr2[Str2[0]]=0do
Dec(Str2[0]);
End;
Begin
Assign(Input,’Count.in’);
Assign(Output,’Count.out’);
Reset(Input);
Rewrite(Output);
Read(P,L,K);
F[0,1]:
=1;
F[0,0]:
=1;
F[1,1]:
=P-1;
F[1,0]:
=1;
ForI:
=2toLdo
Begin
Cheng(F[I-1],P,F[I]);
IfI>K
Then
Begin
Cheng(F[I-K-1],P-1,F[31]);
Minus(F[I],F[31],F[I]);
End;
End;
Fillchar(Ans,Sizeof(Ans),0);
ForI:
=L-K+1toLdo
Add(F[I]);
ForI:
=Ans[0]downto1do
Write(Ans[I]);
Close(Input);
Close(Output);
End.
四、谜题
【问题描述】
有些人花很多时间去解各种各样的谜题,这里要考虑的一种典型的谜题看上去是这个样子的:
这里给出了一个包含各种符号的等式(包括横向等式与纵向等式),必须找到将符号一一对应地替代为数字0,…,9的方法。
如果你曾经尝试过解这种谜题,你就会发现这是一件多么烦琐的事情。
【编程任务】
给你一组包含符号A,B,…,J的等式。
请你找到将符号替代位数字的方法,使得所有的等式同时成立。
所有有数字组成的数都是正的,并且第一个数字都不是0。
如果有多个解法存在,则只打印出将替换后的数字串看成一个整数是数值最小的一组解,如:
解2345678901和解1987654320都满足条件,则只打印出1987654320,因为它在“数值”上最小。
【输入】
输入由一行单独的整数n开始(n代表等式的个数,1≤n≤10)。
接下来是n个等式。
每个等式的语法如下:
equation=argument1[‘+’|‘-’|‘*’|‘/’]argument2‘-’result
argument1=digit{digit}*
argument2=digit{digit}*
result=digit{digit}*
digit=[‘A’|…|‘J’]
注意:
并不是所有A到J的符号都会出现在等式中。
然而,如果其中一个符号出现了,那么所有在字母排序上比它小的符号就同样会出现。
所有出现的数最多有11个数字(即最多是11为整数)。
【输出】
输出仅一行,格式如下:
按字母表顺序排列的所有出现字符
一个箭头‘-->’
对出现的符号在树枝上最小的替换。
如果没有解,则输出一行‘Nosolution’
【样例1】
输入
6
EBCE/CED=AE
FBGB-FHBG=ADC
DIAA-GIHJ=FJHF
EBCE-FBGB=DIAA
CED+FHBG=GIHJ
AE*ADC=FJHF
输出
ABCDEFGHIJ-->1746823509
【样例2】
输入
1
A+A=AA
输出
A-->Nosolution
【算法分析】
这道题很眼熟,和“虫食算”很相似,而且比“虫食算”还稍简单一点。
显然这道题的做法应该是按字母顺序DFS搜出第一个解即输出。
(因为题目要求字典序,所以按字母顺序搜索,显然第一个解组后得到的数字序列比后面的小)
搜索时使用函数search(x,S)来进行,其中x表示当前搜索第x个字母,S表示当前还未检查过的式子的集合。
函数中要进行:
判断剩下式子中再搜出前x-1个字母后是否成立,以及给第x个字母赋值。
S的作用在于,当我进行下一步搜索时,不需要将n个式子全部搜索一遍,而只需搜索剩余没检查过的式子即可。
鉴于集合的运算相对比较不变,我们考虑用一种类似Hash的方法:
我们用数字来表示这个集合S,将该数字表示为二进制后,它的倒数第i位表示第i各式子的状态,这样每种状态对应一个数字,显然,由于式子总数为11,所以有S<1024。
因为将S作为数字,所以相应的检查运算就要利用数字之间的运算。
我们利用PASCAL中的位运算来进行:
加入第i个式子:
S:
=S+2i
减去第i个式子:
S:
=S-2I
判断第i个式子是否在S中:
Sand1shl(i-1)<>0
接下来考虑剪枝,显然我们可以看见一个基本的剪枝,当某个式子AopB=C的所有字母值都已经搜索出来之后,我们可以判断这个式子的正确性,如果正确,则在S中删除该式子(已判定必正确)否则回溯(显然,这个式子中有些地方不正确)进而达到剪枝的目的。
这个剪枝是一个很基本的剪枝,很容易想到,而且比较容易实现(通过PASCAL中字符串函数的应用)。
但是显然这个剪枝的力度不强,比如说当每个式子都有10个未知数时,显然要将所有字母都搜索出来才能利用它来剪枝,此时的时间复杂度仍然是没有剪枝时的O(n!
)左右。
我们可以将这个剪枝拓展一下。
比如式子AopB=C中,如果A和B的值都已经搜索出来了,而C还没有全部搜索出来(如果全部搜索出来就可利用上面那个较弱的剪枝)那么显然我们可以计算AopB的值与C进行比较。
如果与已搜索出的值不相同,那么就可以直接回溯到上层,如果相同,则可以直接确定C中剩下字母的值(显然为了式子正确,C必然为AopB的值),并将该式子从S中去掉。
这个剪枝的力度比上一个强很多,同时也可以减少搜索的次数(因为已经确定了后面的值)。
但我们还可以将这个剪枝进一步拓展:
当A,B,C中有两个计算出来后,可以将另一个计算出来,判断正确性。
正确,则可以确定一部分未确定的值,不正确,则立刻回溯。
这一步扩展后可以使搜索速度加快更多。
另一个重要的剪枝是通过不等式剪枝。
如搜索中有式子3G*2DE=3F2C,显然,这个式子后面无论搜索出什么值都不会成立,这是我们一眼就看得出来的,那如何让电脑来判断呢?
我们先把左边所有的未知数都换成9,那么左边可以达到的最大值就是39*299=11661,再把所有未知数换成1,那么左边可以达到的最小值就是31*211=6541。
再看右边,将所有未知数换成9,得最大值3929;再把所有未知数换成1,得最小值3121。
显然,如果左边的最大值(这里是11661)小于右边的最小值(3121)(这里显然是大于的),或者左边的最小值(6541)大于右边的最大值(3929)(显然这一点成立),那么也可以立刻回溯到上一层。
这个剪枝可以运用在加、减、乘三种运算上,但是除要运用到实数,所以比较麻烦。
如何克服这一点呢?
因为减法是加法的逆运算,除法是乘法的逆运算,所以我们可以在一开始就将减法和除法与处理为加法和乘法,这样在处理时,只需要处理乘法和加法的情况就可以了。
【程序清单】
(鉴于篇幅过长,在此略去)
小结
在这次JSOI09省对第一轮选拔的试题中,只有最后一题需要复杂的思考过程,另三道题目相对来说难度不大。
但是在选拔赛过程中,需要时刻对于题目和自己的程序有着清醒的认识。
譬如我在比赛中,对于第一题中的输入理解有误,没有考虑到给出的两个结点并不一定是按父子结点排好的,所以直接考虑使用并查集来做。
显然,这一算法必然得不到满分。
而第二题中,对于灯泡要照亮的部分要判断清楚,尤其是这道题中给出的山峰是以点为标记的,而要照亮的地方则是线段。
第三题的递推公式则只要能够发现第一个公式就可以完成这个题目了,但是如果能够再花一点时间往后推导,那么效率肯定更高。
最后一题中的两个大剪枝(依据等式性质和不等式性质)相对来说比较难想到,在比赛中应该有选择性地选择变成容易较简单的剪枝,比如前一个剪枝的基础样式比较容易想到,在这个基础上,如果时间充分那么完全可以考虑进一步深入,否则的话也应该果断放弃(因为深入后的编程复杂度由于修改处的增多而大大增加)。
这也是在比赛中应该做到的。
总的来说,这一次比赛对我来说是一次很大的磨炼。
在这次比赛中,充分暴露出我在正式比赛中对于细节相当不注意的缺点,这一点对于比赛是极为致命的。
希望在以后的训练中,我能够在这一点上有所改善。