UVA12799 题解

· · 题解

题意简述

选出两个不相等的奇质数 p,q,令 n = pq,算出 \varphi(n) = (p - 1)(q - 1),选出整数 e(1 \le e < \varphi(n),\gcd(e, \varphi(n)) = 1),再算出 e 在模 \varphi(n) 意义下的逆元 d(1 \le d < \varphi(n))。现有多组数据,每组数据给出整数 n, e, c(15 \le n \le 10^9, 1 \le e, c \le n),求出每组数据对应的 m = c^d \bmod n
最后给出题目地址 UVA12799 RSA。

正解思路

这道题的每组数据需要做三件事。

一、求出 \varphi(n)

按照题意,\varphi(n) = (p - 1)(q - 1),不妨 p < q,又有 n = pq,所以一定有 p < \sqrt{n},并且 q = \frac{n}{p}。所以,可以从 1\lfloor \sqrt{n} \rfloor 枚举 p(显然,这样的 p 只有一个)。找到后,求出 q = \frac{n}{p},进而求出 \varphi(n) = (p - 1)(q - 1)
时间复杂度:\Omicron(\sqrt{n})

二、求出 e 在模 \varphi(n) 意义下的逆元 d

由题意,de \equiv 1 \pmod n。那么,已知 en,如何求出 d 呢?
有一种方法,根据欧拉定理,因为 \gcd(d, \varphi(n)) = 1,所以可以求出 d = e^{\varphi(\varphi(n)) - 1}(求 \varphi 函数的方法就用模板即可,求幂方法同【三、求出 c^d \bmod n】中的方法)。虽然这种方法的时间复杂度是 \Omicron(\sqrt{n} + \log_2 n) = \Omicron(\sqrt{n}),足以通过此题,但是其实有一种更优的方法(拓展欧拉定理),在这里介绍一下。
首先,因为 de \equiv 1 \pmod n,所以有 de + nt = 1(t \in \Z)
注意到,若有一方程 a_0x_0 + b_0y_0 = \gcd(a_0, b_0),其中 a_0, x_0, b_0 \in \Ny_0 \in \Z,且 a_0, x_0 < b_0。则令 a_0 = e, b_0=n,有 \gcd(a_0, b_0) = \gcd(e, n) = 1,则解出的 x_0 就是 a_0 在模 b_0 意义下的逆元,即 e 在模 n 意义下的逆元。
那么怎么解出这个方程呢?其实最开始的方程的右边是 \gcd(a_0, b_0),只是因为我们代入的值使其为 1。我们想到,可否使用类似于求最大公因数的方法来求解这个方程呢?
若有 i \in \N,且 b_i \ne 0,则令 a_{i + 1} = b_i,b_{i + 1} = a_i \bmod b_i,得到一个新的方程 a_{i+1}x_{i+1} + b_{i+1}y_{i+1} = \gcd(a_0,b_0)(因为 \gcd(a_{i+1},b_{i+1}) = \gcd(a_i,b_i))。直到 b_i = 0,令此时的 ik。那接下来就需要依次求出 i \le k 时,x_i,y_i 的解。
然而正常情况下这样的方程的解应该不止一个,但是在我们要求的问题中,若 i < k,显然 x_i 的不同解都在同一个模 b_i 的剩余类上,每个 x_i 的解都对应出一个 y_i,所以在要求 0 \le x_i < b_i 的情况下,显然有 x_i 唯一;否则,即 i = k,此时 a_i = \gcd(a_0,b_0),b_i = 0,所以 \gcd(a_0,b_0)x_k + 0 \cdot y_k = \gcd(a_0,b_0),即 x_k = \frac{\gcd(a_0,b_0)}{\gcd(a_0,b_0)} = 1,而 y_k 可取任意值。当然,在我们所求的问题中,y_k 取何值都没关系,不妨 y_k = 0
那么,若已知 x_{i+1},y_{i+1}(0 \le i < k),如何求出 x_iy_i 呢?显然,a_{i + 1} = b_i,b_{i + 1} = a_i \bmod b_i。令 r = \lfloor \frac{a_i}{b_i} \rfloor,则 b_{i+1} = a_i - r \cdot b_i。代入 a_{i+1}x_{i+1} + b_{i+1}y_{i+1} = \gcd(a_0,b_0),得:b_ix_{i+1} + (a_i - r \cdot b_i)y_{i+1} = \gcd(a_0,b_0)。即:a_iy_{i+1} + b_i(x_{i+1}-r \cdot y_{i+1}) = \gcd(a_0,b_0)。所以:x_i = y_{i+1},y_i = x_{i+1} - r \cdot y_{i+1} = x_{i + 1} - \lfloor \frac{a_i}{b_i} \rfloor \cdot y_{i+1}
这样,只要利用类似于求最小公约数的方法求出上面方程中令 a_0 = d,b_0=\varphi(n) 的一组解中的 x_0 并将其转换为与其同余的大于等于 0 小于模数 b_0 的整数即可求出 e 在模 \varphi(n) 意义下的逆元 d
时间复杂度:\Omicron(\log_2 n)
但是这里有一个细节,那就是最后取模的时候得出的 x_0 可能是负数,所以一定要记得判断正负性,因为在 C++ 中,是不能直接对负数取模的。所以,假设已经算出 \varphi(n) 并将其存储至变量 r 中,则如果要让 xr 取模,不能写成 x = x % r;,而要写成 x = (x >= 0) ? (x % r) : ((r - (-x) % r) % r)(码风不好,见谅,懂意思就行)。

三、求出 c^d \bmod n

好了,接下来就是求出 m = c^d \bmod n 并输出了(显然,d \ge 1)。但是注意到 n \le 10^9。那么,如果按照下面代码片段的方法求 c^d \bmod n 的话,是肯定会超时的(因为时间复杂度是 \Omicron(d) = \Omicron(n)):

long long s = 1;
for(int i = 1; i <= d; i ++) {
    s = s * c % n;
}
return s;

所以,需要进行优化。
这里需要用到分治或倍增的思想(有两种实现方式)。

分治法

f(x) = c^x \bmod n(x \in \N^*)。显然当 x > 1 时,有:

\begin{aligned} f(x) &= c^x \bmod n \\ &= c^{2 \lfloor \frac{x}{2} \rfloor + x \bmod 2} \bmod n \\ &= ((c^{\lfloor \frac{x}{2} \rfloor})^2 \cdot c^{x \bmod 2}) \bmod n \\ &= (f(\lfloor \frac{x}{2} \rfloor)^2 \cdot c^{x \bmod 2}) \bmod n \end{aligned}

所以:

f(x) = \begin{cases} c & x = 1 \\ (f(\lfloor \frac{x}{2} \rfloor)^2 \cdot c^{x \bmod 2}) \bmod n & x > 1 \end{cases}

所以就可以用上面方法递归求出 f(d) 的值,即 c^d \bmod n 的值。时间复杂度:\Omicron(\log_2 d) = \Omicron(\log_2 n)

倍增法

因为 d \in \N^*,所以显然存在正整数 k,以及正整数 w_0,w_1,\cdots,w_{k-1}(w_{k-1} \ne 0),且对于所有的 i \in \N 并且 i < k 都有 w_i \in \{0,1\},并满足 d = \sum_{i=0}^{k-1} 2^i w_i。所以:

\begin{aligned} c^d &= c^{\sum_{i=0}^{k-1} 2^i w_i} \\ &= \prod_{i=0}^{k-1} c^{2^i w_i} \\ &= \prod_{i=0}^{k-1} (c^{2^i})^{w_i} \end{aligned}

显然,若 i \in \N,则:

c^{2^i} = \begin{cases} c & i = 0 \\ (c^{2^{i-1}})^2 & i > 0 \end{cases}

所以,用一个变量存储答案(初值为 1),从小(0)到大(k - 1)循环枚举整数 i:每次按照上面方法求出 c^{2^i} \bmod n,如果 w_i = 1,则让答案变量乘上 c^{2^i} \bmod n 后对 n 取模。显然,k = \lfloor \log_2 n \rfloor + 1。所以时间复杂度也是 \Omicron(\log_2 n)

所以,正解思路总的时间复杂度就是 \Omicron(t \sqrt{n})t 表示询问总数)。

代码

在放代码之前,我想说一些细节:

  1. 记得开 long long!!
  2. 注意是多组数据,想好读入方式。

好了,接下来放代码。

分治法

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, e, c, r, x, y;
int Phi(int t) {//这道题情况比较特殊,不用打全求欧拉函数的模板 
    for(int i = 2; ; i ++) {
        if(t % i == 0) {
            return (i - 1) * (t / i - 1);
        }
    }
}
void Exgcd(int a, int b) {//拓展欧拉定理求逆元 
    if(!b) {
        x = 1;
        y = 0;
        return; 
    }
    Exgcd(b, a % b);
    int temp = y;
    y = x - (a / b) * y;
    x = temp;
}
int Quick_pow(int t) {
    if(t == 1) return c;
    int tmp = Quick_pow(t >> 1);
    (tmp *= tmp) %= n;
    if(t & 1) (tmp *= c) %= n;
    return tmp;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    //读入小优化,不介意吧 
    while(cin >> n >> e >> c) {
        r = Phi(n);
        Exgcd(e, r);
        cout << Quick_pow((x >= 0) ? (x % r) : ((r - (-x) % r) % r)) << endl;
    }
    return 0;
}

倍增法

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, e, c, r, x, y;
int Phi(int t) {//这道题情况比较特殊,不用打全求欧拉函数的模板 
    for(int i = 2; ; i ++) {
        if(t % i == 0) {
            return (i - 1) * (t / i - 1);
        }
    }
}
void Exgcd(int a, int b) {//拓展欧拉定理求逆元 
    if(!b) {
        x = 1;
        y = 0;
        return; 
    }
    Exgcd(b, a % b);
    int temp = y;
    y = x - (a / b) * y;
    x = temp;
}
int Quick_pow(int t) {
    int tmp = c, s = 1;
    while(t) {
        if(t & 1) (s *= tmp) %= n;
        (tmp *= tmp) %= n;
        t >>= 1;
    }
    return s;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    //读入小优化,不介意吧 
    while(cin >> n >> e >> c) {
        r = Phi(n);
        Exgcd(e, r);
        cout << Quick_pow((x >= 0) ? (x % r) : ((r - (-x) % r) % r)) << endl;
    }
    return 0;
}

提交记录

分治法提交记录
倍增法提交记录

总结

本题思路较为简单,只是需要一些数论的知识,照题意模拟即可。代码比较短,自己手打。