KeepCode1 解题思路及代码实现
本次题目皆来源于 HDUOJ 题目编号:
ID | Origin | Title |
---|---|---|
Problem A | HDU 4475 | Downward paths |
Problem B | HDU 4476 | Cut the rope |
Problem C | HDU 1576 | A/B |
Problem D | HDU 1060 | Leftmost Digit |
Problem E | HDU 1066 | Last non-zero Digit in N! |
5个题目都属于思维题,且都没有复杂的编码,但是对逻辑思维转换还是有一定的要求. 笔者希望大家能够多细心思考,注重严谨的逻辑推导。
虽然题目不难也不易,但是Acm的队员们还是相当不错,二年级的唐仕首当其冲踩过了A,C两题,一年级的梁王凯也不赖第一个过了D,还有一个不错的应数一年级队员林巧生
秒杀了E题(全场仅此一村呀),就连我们的三年级学长 jian1573 也耐不住寂寞,陪大家练练,飘过了B题。
持续1天04小时的趣味编程最终5个题都被AC,个人AC最多为3题。
希望大家继续加油~~
A题: 对于拥有 n 层的三角塔,则从顶部到底部有 n+1 个不同的终点。 若定义 F ( n ) 为 n 层的三角塔从顶部移动到底部不同的方案总数 ,
则,我们可以发现,当从第 n 层 走到 第 n+1 层的时候, 在第 n 层底部,有 n+1 个点可以 走向 n+1 层, 且每一个点有两条路走,所以我们可以得出
F ( n+1 ) = F ( n ) * (n+1) * 2 , 通过求出生成函数,我们可以得出公式, F ( n ) = n! * 2^n % 1000003 ( n! 为N的阶乘,2^n 表示 2的n次方 )。
虽然我们得出了 F(n)的表达式,但是 n 的取值为 10^18 ,O(n)的时间计算 n! 与 2^n ,无法在 1 s内解决,这个时候我们面临两个问题待解决:
1. n! * 2^n 数据超出 64bit 表示范围,溢出 2. n! * 2^n 直接计算,时间达不到要求
对于问题1: 在这里,我们数学中有同余定理可以解决这个问题: (a*b)% c = [ ( a % c ) * ( b % c ) ] % c
通过这个性质,我们可以在计算 n! 与 2^n 的时候 不会溢出, 其峰值最大为 (c-1)^2 ,当且仅当 a, b取 c-1 时达到。
对于问题2:首先观察 n! % c = 1*2*3*...*c*..*n % c 不难看出,当n >= c 时 n! %c 必定为0 ,则对于 n!, 我们只需要计算到 c , 在本题中 c = 1000003
时间上还是能够接受。 再观察 2^n = 2^(n/2) + 2^( n/2 ) + 2^( n %2 ) (其中 / 为整除) 我们可以通过二分快速幂在 O(logN)的时间内计算出 2^n。
到此,两个问题已经解决,则我们所需要的答案就出来了。
这里提及一下,对于 2^n 我们也可以不用每次计算, 和 n!的阶乘一样预处理计算出来后存储,之后O(1)时间即可。
预处理解题代码:
#include <stdio.h> #define LL __int64 const LL mod = 1000003; LL ans[mod + 5]; int main() { int T; ans[0] = 1; for (LL i = 1; i <= mod; i++) { ans[i] = ans[i - 1] * (2 * i); ans[i] %= mod; } for (scanf("%d", &T); T--;) { LL n; scanf("%I64d", &n); if (n >= mod) puts("0"); else printf("%I64d\n", ans[n]); } return 0; }
使用二分快速幂的代码:
#include<stdio.h> #include<math.h> #include<string.h> const int mod = 1000003; typedef long long LL; LL tmp[mod+10]; void init(){ tmp[0] = 1; for(int i = 1; i <= mod; i++) { tmp[i] = tmp[i-1]*i%mod; } } LL Pow( LL x , LL n ) { LL res = 1; while( n ){ if(n&1) res *= x, res %= mod; x = x*x; x %= mod; n >>= 1; } return res; } LL MUL(LL a, LL b) { LL c = 0; while( b ){ if(b&1){ if( (c += a) >= mod ) c -= mod; } a <<= 1; if( a >= mod ) a -= mod; b >>= 1; } return c; } int main(){ init(); int T; scanf("%d", &T); LL n; while( T--) { scanf("%lld", &n); LL ans = 0; if( n < mod ) ans = MUL( Pow(2,n), tmp[n] ); printf("%lld\n", ans); } return 0; }
B题:依据题意,每条绳子最多截断一次。
定义 Count( x ) : 长度为 x 的绳子的数量。 Sum( x ) : 大于等于x的绳子的数量。
则 answer = MAX { 2*Count(i) + Sum( i/2 ) } ( 其中 i 为所有不同的绳子长度)
对于计算,我们可以在输入的时候就处理出Count( x ) , 然后O(n)枚举所有出现的长度的时候再用 树状数组或线段数 O( Log(N) ) 来统计。
总体时间复杂度为 O(N*log(N) )
这里给出 树状数组的解题代码:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<math.h> #define MIN(a,b) (a)<(b)?(a):(b) #define MAX(a,b) (a)>(b)?(a):(b) const int N = 100110; int rope[N], n, C[N], num[N]; int Lowbit( int x ){ return x&(-x); } void update( int x ){ while( x <= N ){ C[x] += 1; x += Lowbit(x); } } int read( int x ){ int res = 0; while( x >= 1 ) { res += C[x]; x -= Lowbit(x); } return res; } int main() { int T; scanf("%d", &T); while( T-- ){ scanf("%d", &n); memset( C, 0, sizeof(C)); memset( num, 0, sizeof(num)); int max = 0; for(int i = 1; i <= n; i++) { scanf("%d", &rope[i] ); update( rope[i] ); max = MAX( max, rope[i] ); num[rope[i]]++; } int ans = n; for(int i = 1; i <= max; i++) { if( num[i] > 0 ){ int x = ceil( i/2. ); //printf(" x = %d\n", x ); ans = MAX( ans , (read(max)-read(x-1))+num[i] ); } } printf("%d\n", ans ); } return 0; }
其实也可以全部预处理出来,然后利用辅助数组存储后输出,这里给出jian1573的解题代码
using namespace std; const int N = 100010; int a[N],s[N]; int main() { int T, n, x; scanf("%d",&T); while(T--) { scanf("%d",&n); memset(a,0,sizeof a ); memset(s,0,sizeof s ); for(int i=0;i<n;i++) { scanf("%d",&x); a[x]++; } for(int i=1;i<N;i++) s[i] = s[i-1]+a[i]; int ans = 0; for(int i=1;i<N;i++) if(a[i]) ans=ans>(a[i]+n-s[(i-1)>>1])?ans:(a[i]+n-s[(i-1)>>1]); printf( "%d\n",ans ); } return 0; }
C题:
解法一: 依据题意,方便表示,我们令 C = 9973 ,则有
A % C = n
A % B = 0
则
A = k1*C + n
A / B = k2*C + x 其中( k1, k2 为自然数, 0 <= x < C )
转换一下得 A = k1*C + n = B * ( k2*C + x ) ==> k1*C = B*k2*C + ( B*x-n )
可得 (B*x - n ) % C = 0
这里B ,n皆为已知量,x的取值范围为 [ 0, 9973 ) ,所以我们可以枚举 x 的值即可。
参考代码:
#include<stdio.h> typedef long long LL; const int mod = 9973; int main() { int T; LL n, B; scanf("%d", &T); while( T-- && scanf("%I64d%I64d", &n,&B) ){ for(int x = 0; x < mod; x++) if( (B*x - n) % mod == 0 ) { printf("%d\n", x ); break; } } return 0; }
解法二: 其实对于此题,我们还可以通过转换成线性同余方程 ( a*x + b*y = c )来求解: 关于线性同余方程的解法以及扩展GCD的证明请浏览笔者QQ空间的日志。
对于 A % C = n
A % B = 0
则 n = A - ( A/C ) *C ( A/C 为计算机的整除), 设 x = A / B 则 A = B*x
==> B*x + ( A/C ) *C = n
令 a = B, b = A/C , c = n
得到线性同余方程 ax + by = c
使用扩展GCD求出 符合 a * x0 +b * y0 = 1的一组合法解, 可得 x = x0*c + b*k 其中k属于自然数
这里给出参考代码:
#include<stdio.h> #include<iostream> using namespace std; typedef long long LL; LL exgcd( LL a, LL b, LL &x, LL &y) { if( b == 0 ) { x = 1; y = 0; return a; } LL d = exgcd( b, a%b, x, y ); LL t = x; x = y; y = t - (a/b)*y; return d; } LL gcd( LL a, LL b ) { return b == 0 ? a : gcd( b, a%b ); } void solve( int n, int B) { LL a = B, b = 9973, c = n; LL x, y; exgcd( a, b, x, y ); //printf("x = %lld, y = %lld\n", x, y ); LL ans = x*c%9973; while( ans < 0 ) ans += b; printf("%lld\n", ans ); } int main() { int T; scanf("%d", &T); while( T-- ) { int n, B; scanf("%d%d", &n,&B); solve( n, B ); } return 0; }
D题: 设 M = N^N , 等式两边去对数 log10
log10 ( M ) = log10 ( N^N ) = N * log10 ( N )
再化简过去 可得: M = 10 ^( N*log10(N) )
我们假设 N * log10( N ) = A + B (其中A为整数部分,B为小数部分)
则有 M = 10^A + 10^B , 对于整数A, 则10^A必定为 100...0的形式, 而对于10^B ,因为B = [0,1) 所以10^B 属于 [ 1,10 )区间
可以得出,对与M的最左边有影响的只有 10^B ,所以结果为 pow( 10 , N*log10(N) - floor( N*log10(N) ) )
解题代码:
#include<stdio.h> #include<math.h> int main() { int T; scanf("%d", &T); while( T-- ) { double n, ans; scanf("%lf", &n); double tmp = n*log10(n); tmp = tmp - floor(tmp); ans = pow(10, tmp); printf("%.0lf\n", floor(ans) ); } return 0; }
E题:对于 N ! = 1 * 2 * 3 * ... * n
当且仅当成对的出现2,5时,才会出现0。
我们可以将 N! 的所有 2,5因子成对的去掉后,即最后一位必定是一个非0。
那么接下来我们就需要求的是 去掉了成对的2,5后的 N!的最后一位
为了方面,我们用M!表示 N!去掉了 成对的2,5的数值
则我们需要的结果就是 M!%10, 通过同余定理,即可得出答案
这里给出 12级 应数林巧生的AC代码
#include<stdio.h> #include<string.h> int mod[20]={1,1,2,6,4,2,2,4,2,8,4,4,8,4,6,8,8,6,8,2}; int a[1000]; char n[1000]; int main() { int i,c,t,length; while(scanf("%s",n)!=EOF) { t=1; length=strlen(n); for(i=0;i<length;i++) a[i]=n[length-1-i]-'0'; while(length) { length-=!a[length-1]; t=t*mod[a[1]%2*10+a[0]]%10; for(c=0,i=length-1;i>=0;i--) c=c*10+a[i],a[i]=c/5,c%=5; } printf("%d\n",t); } return 0; }
引用下leemars的报告:
这道题要求N!的最后一个非0数字是多少,如果用一般作法,先统计2和5的个数,然
后补乘2,得到的将是TLE。所以还需要再做简化:
为了把0去掉,我们把所有的因数2和5都提出来,放到最后再处理。N!中的N个相乘的
数可以分成两堆:奇数和偶数。偶数相乘可以写成(2^M)*(M!),M=N DIV 2。M!可以
递归处理,因此现在只需讨论奇数相乘。考虑1*3*5*7*9*11*13*15*17* ... *N(如果
N为偶数则是N-1),这里面是5的倍数的有5,15,25,35,... ,可以其中的5提出来
,变成(5^P)*(1*3*5*7*9* ... ),后面括号中共P项,P=(N DIV 5+1) DIV 2,而后
面的括号又可以继续提5出来,递归处理。现在剩下的数是1 * 3 * 7 * 9 * 11 * 13
* 17 * 19 * ... 。这些数我们只需要他们的个位数,因为(1 * 3 * 9 * 11 * 13
* ... ) MOD 10 = (1 * 3 * 7 * 9 * 1 * 3 * ... ) MOD 10。我们列出余数表,
1 3 1 9 9 7 9 1 1 3 1 9 9 7 9 ……。我们发现每八项MOD 10的结果是一个循环。
算出奇数的结果后,我们再回头看统计了多少个2和5需要乘入。把2和5配对完都是N
!后面的0,看剩下的2有几个。如果有剩下的2,考虑2^N的个位数又是2 4 8 6 2 4
8 6 ……每四项一个循环,找出这个个位数后,和前面的结果相乘,再取个位数就是
答案。由于我们完全把2和5的因素另外处理,所以在所有的乘法中,都只需要计算个位数乘法,并且只保留个位数的结果。
但让我很惊异的是:为什么我提交总是WA?后来我才知道,原因是这道题的N相当大
!达到了10^100!要用大数来处理!GPC四项编译开关全关的,自然查不出来!而且
上面这个算法换成大数后会很麻烦。还有什么别的好方法吗?
答案是有的。我们设F(N)表示N!的尾数。
先考虑简单的。考虑某一个N!(N < 10),我们先将所有5的倍数提出来,用1代替原来
5的倍数的位置。由于5的倍数全被提走了,所以这样就不会出现尾数0了。我们先把
0..9的阶乘的尾数列出来(注意,5的倍数的位置上是1),可以得到table[0..9] =
(1, 1, 2, 6, 4, 4, 4, 8, 4, 6)。对于N < 5,直接输出table[N]即可;对于N >
= 5,由于提出了一个5,因此需要一个2与之配成10,即将尾数除以2。注意到除了0
!和1!,阶乘的最后一个非零数字必为偶数,所以有一个很特别的除法规律:2 / 2
= 6,4 / 2 = 2,6 / 2 = 8,8 / 2 = 4。比较特殊的就是2 / 2 = 12 / 2 = 6,
6 / 2 = 16 / 2 = 8。这样我们就可以得到如下式子:
代码:
table[N]
F(N) = ------------ (0 <= N < 10)
2^([N/5])
再考虑复杂的。考虑某一个N!(N >= 10),我们先将所有5的倍数提出来,用1代替原
来5的倍数的位置。由于5的倍数全被提走了,所以这样就不会出现尾数0了。我们观
察一下剩下的数的乘积的尾数,通过table表,我们发现这10个数的乘积的尾数是6,
6 * 6的尾数还是6,因此我们将剩下的数每10个分成一组,则剩下的数的乘积的尾数
只与最后一组的情况有关,即与N的最后一位数字有关。由于我们把5的倍数提出来了
,N!中一次可以提出[N/5]个5的倍数,有多少个5,就需要有多少个2与之配成10,所
以有多少个5,最后就要除以多少个2。注意到除2的结果变化是4个一循环,因此如果
有A个5,只需要除(A MOD 4)次2就可以了。A MOD 4只与A的最后两位数有关,很好求
算。剩下的5的倍数,由于5已经全部处理掉了,就变成[N/5]!。于是,我们可以得到
一个递归关系:
代码:
F([N/5]) * table[N的尾数] * 6
F(N) = ----------------------------------- (N > 10)
2^([N/5] MOD 4)
这样我们就得到了一个O(log5(N))的算法,整除5可以用高精度加法做,乘2再除10即
可。整个算法相当巧妙,写起来也比较轻松。
因为 2^N 是以4为循环节的
而且table[N]是以10为循环节的
所以从10开始
F([N/5]) * table[N的尾数] * 6
F(N) = ----------------------------------- (N > 10)
2^([N/5] MOD 4)
右边的式子除了F[n/5]外 是以20为循环节的
写出循环的末尾数字mod[20]={1,1,2,6,4,2,2,4,2,8,4,4,8,4,6,8,8,6,8,2}
此为笔者对于这些题的解题思路. 时间仓促,加之笔者能力有限,有错误的地方忘指出.