POJ 1061青蛙的约会题解

POJ 1061青蛙的约会题解

网上似乎有不少此题的解法。我这个post和其他人的相比主要时想说下面几点。
1. 给出一个试图不死记硬背公式的思路;
2. 谈谈暴力解为什么看起来这么诱人,却无法通过;
3. 一些细节及对此题的评价(个人观点)

原题请参考poj: http://poj.org/problem?id=1061

我认为ACM/ICPC应该给人思维的快乐,甚至带有幽默感。这是几个世纪以来数学趣题吸引人们的那种美。
人们会废寝忘食地思考一道题目,解开的一刹那会不由自主的说: 啊--啊哈--哈哈

但是这道题目丝毫没有给我这种快感,这是一道被中国式的考试(竞赛)制度八股化的题目。
首先一看此题要求无解时给出Impossible的输出,就知道一定是要运用数论中的线性同余知识了。

真正思维的快乐在一开始的建模部分。很容易写出下面的等式:
(x + a m) mod L = (y + b n) mod L

我一开始认为两只青蛙跳跃的次数是不同的,一个为a, 另外一个为b,要求出最小的a + b。如果
题目真是是这样,那就有趣多了。可惜本题主旨是要靠死记硬背。所以实际上题意是a == b。

这样的话,上面的等式就化简为求最小的a,使得:
(x + a m) mod L = (y + a n) mod L

利用线性同余进一步化成如下形式:
a x == b (mod n)
其中 a = (n - m)
b = (x - y)
n = L

现在的要求就是,首先判断此线性同余方程是否有解,有解的话,给出最小正数解。
这时就必须死记硬背了。题目开始无趣了。查wikipedia得知:
[1] http://en.wikipedia.org/wiki/Linear_congruence_theorem
这个线性方程有解,当且仅当gcd(a, n)能整除b

而求gcd,当然就是递归的hello world,Euclidean算法了。可惜我脑子不好,记不得了,所以自己推导一下。
若求gcd(a, b),假设a < b, 那么一定可以写成:
b = a q + r

其中q是商,r是余数。假设d = gcd(a, b); 那么显然d能整除b和a,所以d也能整除上式的r。由于r 一定小于a
所以,我们只要求 gcd(r, a)。于是问题的规模就递归化简了。再想想边界条件,显然是 a | b。这时r为0.

所以可以轻易写出gcd的Euclidean算法了:

int gcd(int a, int b) {
return a == 0 ? b : gcd(b % a, a);
}


于是就可以判定是否有解了。接着考虑有可能出现a或者b为负数的情形,所以需要处理下输入。正好最近读了
<<短码之美>>,索性扬弃地写出了下面的代码:


int gcd(int a, int b) {
return a == 0 ? b : gcd(b % a, a);
}

main(a, b, x, y, m, n, L) {
scanf("%d%d%d%d%d", &x, &y, &m, &n, &L);
a = m < n ? n - m : m - n;
b = m < n ? x - y : y - x;
b = (L + b) % L; // avoid negative b
if ( b % gcd(a, L))
printf("Impossible\n");
else
...???...
}


现在的问题是...???...的部分怎么写? 第一个思路当然是暴力解了:


if ( b % gcd(a, L))
printf("Impossible\n");
else {
for (x = 0, n = 0; x != b; x += a, x -= (x >= L) ? L : 0, ++n);
printf("%d\n", n);
}


Jon Bentley在Porgramming Pearls中告诉我们,取mod是个很慢的操作,所以我就用加法和减法达到了同样的目的。

用样本测试下能通过,然后上POJ,一下子失败了,结果是Wrong Answer,结果发现这个题目的输入数据不只一行,
(虽然题目说是一行),于是把scanf等用循环包起来。

接着还是Wrong Answer.再次读题发现,输入数据很大,是个10位数。
10位数用long能装下,但是一旦做乘法就危险了。这就是网上其他的答案中要么用long long,要么用int64的原因。
可是我的暴力解法中,没有乘法,我用加法减法化简了。所以我可以用long,改成long后上POJ,这次仍然fail了。
原因是超时Exceed Time Limit。

我自己试了试,解同余方程 11 x == 2000000000 (mod 2100000000) 的确很慢,大约用了7,8秒的样子,而题目
要求是1秒内计算出答案。

到了这里,明白这道题目就是拒绝暴力解法的。剩下的唯一思路就是快速解线性同余方程了。到这里彻底看透了考察死记硬背
的无聊用意。再次查看wikipedia,看到了Extended Euclidean algorithm:
[2] http://en.wikipedia.org/wiki/Linear_congruence_theorem#Solving_a_linear_congruence

也就是说,通过Extended Euclidean算法可以得到 a x + b y = gcd(a, b)的线性组合x和y。
这样就可以直接得到特解x0 = x b / d,其中d = gcd(a, b)。其他的解都可以表达为:
x0 + k n / d的形式,其中k为整数。

到这里已经很无趣了,实现Extended Euclidean就可以了。回想起当年看CLRS算法导论的时候,讲解RSA算法时,说过
这个扩展Euclidean。伪代码如下:

function egcd(a, b)
if b == 0
return (a, 1, 0)
(d', x', y') = egcd(b, a mod b)
return (d', y', x' - a / b * y')

翻译成C, 然后上POJ,又失败了。这次是wrong answer,这是意识到Extended Euclidean返回的x可能是负数。例如:
12 x == 20 (mod 28), 不加以处理的话,结果是-10. 于是稍作调整得到最终的程序。

翻译为C就是:


typedef long long BIG;

BIG egcd(BIG a, BIG b, BIG *x, BIG *y) {
BIG d, x1, y1;
if (b == 0) {
d = a;
*x = 1;
*y = 0;
}
else {
d = egcd(b, a % b, &x1, &y1);
*x = y1;
*y = x1 - a / b * y1;
}
return d;
}

main() {
BIG a, b, x, y, m, n, L, d;
while(scanf("%lld%lld%lld%lld%lld", &x, &y, &m, &n, &L) != -1) {
a = m < n ? n - m : m - n;
b = m < n ? x - y : y - x;
b = (L + b) % L; // avoid negative b
d = egcd(a, L, &x, &y);
if ( b % d)
printf("Impossible\n");
else {
for(x *= b / d, L /= d; x < 0; x += L); /*hanldle negative answer case. e.g. 12 x == 20 (mod 28)*/
printf("%lld\n", x % L);
}
}
}


上POJ,终于AC了。

最后我再次认为这倒题目很无趣,整个题目的设计,尤其是细节,都强迫人走同一条道路。
由于POJ不支持Python, Scheme等(这点ZOJ就好多了),必须使用64位整数,然后就是死记硬背扩展Euclidean算法。
好在这道题目是中文的,免得拿到世界大赛上贻笑大方了。
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
POJ3635是一道经典的数学题,需要使用一些数学知识和算法进行解决。 题目描述: 给定四个正整数 a、b、p 和 k,求 a^b^p mod k 的值。 解题思路: 首先,我们可以将指数 b^p 写成二进制形式:b^p = c0 * 2^0 + c1 * 2^1 + c2 * 2^2 + ... + ck * 2^k,其中 ci 为二进制数的第 i 位。 然后,我们可以通过快速幂算法来计算 a^(2^i) mod k 的值。具体来说,我们可以用一个变量 x 来存储 a^(2^i) mod k 的值,然后每次将 i 加 1,如果 ci 为 1,则将 x 乘上 a^(2^i) mod k,最后得到 a^b^p mod k 的值。 代码实现: 以下是 Java 的代码实现: import java.util.*; import java.math.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); BigInteger a = sc.nextBigInteger(); BigInteger b = sc.nextBigInteger(); BigInteger p = sc.nextBigInteger(); BigInteger k = sc.nextBigInteger(); BigInteger ans = BigInteger.ONE; for (int i = 0; i < p.bitLength(); i++) { if (b.testBit(i)) { ans = ans.multiply(a.modPow(BigInteger.ONE.shiftLeft(i), k)).mod(k); } } System.out.println(ans); } } 其中,bitLength() 函数用于获取二进制数的位数,testBit() 函数用于判断二进制数的第 i 位是否为 1,modPow() 函数用于计算 a^(2^i) mod k 的值,multiply() 函数用于计算两个 BigInteger 对象的乘积,mod() 函数用于计算模数。 时间复杂度: 快速幂算法的时间复杂度为 O(log b^p),其中 b^p 为指数。由于 b^p 的位数不超过 32,因此时间复杂度为 O(log 32) = O(1)。 总结: POJ3635 是一道经典的数学题,需要使用快速幂算法来求解。在实现时,需要注意 BigInteger 类的使用方法,以及快速幂算法的细节。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值