CCPC-Wannafly Winter Camp - 数论(Div. 1)- 同余理论习题

CCPC-Wannafly Winter Camp - 数论(Div. 1)- 同余理论习题

感谢 tangjz 的精彩讲解以及有趣的题单!

本文中的题意全部来自原 Slideshow。

1. Codeforces - 194B - Square(同余的性质)

题目链接:https://codeforces.com/problemset/problem/194/B

难度: 1300 1300 1300

1.1 题意

环上有 4 n 4n 4n 个点,按照顺时针编号 1 1 1 4 n 4n 4n,求集合 { ( ( n + 1 ) x m o d    4 n ) + 1 ∣ x ∈ N } \left\{ ((n+1)x \mod 4n)+1|x\in \textbf{N}\right\} {((n+1)xmod4n)+1xN} 的元素个数。

数据范围: 1 ≤ n ≤ 1 0 9 1 \le n\le 10^9 1n109

1.2 解题过程

手推一个 n = 6 n=6 n=6 的情况即可发现答案就是
4 n gcd ⁡ ( n + 1 , 4 n ) \frac{4n}{\gcd(n+1,4n)} gcd(n+1,4n)4n
时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)

2. Codeforces - 1091C - New Year and the Sphere Transmission(同余的性质)

题目链接:https://codeforces.com/problemset/problem/1091/C

难度: 1400 1400 1400

2.1 题意

环上有 n n n 个点,按顺时针编号 1 1 1 n n n,对于整数 k k k,记集合 { ( k x m o d    n ) + 1 ∣ x ∈ N } \left\{ (kx \mod n) + 1|x \in \textbf{N}\right\} {(kxmodn)+1xN} 的元素和为 f k f_k fk,求有多少种不同的 f k f_k fk,升序输出。

数据范围: 2 ≤ n ≤ 1 0 9 2 \le n \le 10^9 2n109

2.2 解题过程

手推 n = 6 n=6 n=6 的规律可以发现,当 k k k 确定时,数字会从 1 1 1 开始按照 gcd ⁡ ( n , k ) \gcd(n,k) gcd(n,k) 的公差递增。

gcd ⁡ ( n , k ) ∣ n \gcd(n,k)|n gcd(n,k)n,因此我们只需枚举 n n n 的因子 d d d 作为 k k k 的代表,之后进行等差数列求和运算之后把答案丢到一个 set 中。

最后输出 set 中的所有值即可。

时间复杂度: O ( n log ⁡ n ) O(\sqrt n \log n) O(n logn)

2.3 代码

set<ll> ans;
ll sum(ll d, ll n) {
    ll an = 1LL + (n - 1) * d;
    ll res = n * (1 + an) / 2;
    return res;
}
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i * i <= n; i++) {
        if (n % i) continue;
        ans.insert(sum(1LL * i, 1LL * n / i));
        if (i * i != n) {
            ans.insert(sum(1LL * n / i, 1LL * i));
        }
    }
    for (auto element: ans) {
        printf("%lld ", element);
    }
    return 0;
}

3. Codeforces - 492E - Vanya and Field(同余的性质)

题目链接:https://codeforces.com/problemset/problem/492/E

难度: 2200 2200 2200

3.1 题意

n × n n \times n n×n 的矩阵(下标从 0 0 0 开始)里有 m m m 个特殊点,给定整数 d x , d y dx,dy dx,dy,选定一组整数 x , y x,y x,y 使得集合 { ( ( x + k ⋅ d x ) m o d    n , ( y + k ⋅ d y ) m o d    n ) ∣ k ∈ N } \left\{ ((x + k \cdot dx) \mod n,(y + k \cdot dy) \mod n)|k \in \textbf{N} \right\} {((x+kdx)modn,(y+kdy)modn)kN} 中特殊点的数量最大。

数据范围: 1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1n106 1 ≤ m ≤ 1 0 5 1 \le m \le 10^5 1m105 1 ≤ d x , d y ≤ n 1 \le dx,dy \le n 1dx,dyn gcd ⁡ ( n , d x ) = gcd ⁡ ( n , d y ) = 1 \gcd(n,dx)=\gcd(n,dy)=1 gcd(n,dx)=gcd(n,dy)=1

3.2 解题过程

注意到, gcd ⁡ ( n , d x ) = gcd ⁡ ( n , d y ) = 1 \gcd(n,dx)=\gcd(n,dy)=1 gcd(n,dx)=gcd(n,dy)=1,这表明 { x + k ⋅ d x m o d    n } \left\{ x + k \cdot dx \mod n \right\} {x+kdxmodn} { y + k ⋅ d y m o d    n } \left\{ y + k \cdot dy \mod n \right\} {y+kdymodn} 均通过 { z m o d    n } \left\{ z \mod n \right\} {zmodn}

如果我们尝试写下几种 x x x y y y 对应的序列,会发现,我们只需要关心 x x x y y y相对位置。这时,我们不妨固定 x x x 0 0 0,只考虑 y y y 的变化。

之后,我们枚举所有的特殊点 ( x i , y i ) (x_i,y_i) (xi,yi),根据 x = 0 x=0 x=0 计算出 k k k 值,然后根据 y + k ⋅ d y ≡ y i m o d    n y + k \cdot dy \equiv y_i \mod n y+kdyyimodn 反推出 y y y 值,之后将 c n t [ y ] cnt[y] cnt[y] 1 1 1 c n t [ y ] cnt[y] cnt[y] 的含义是 x = 0 x=0 x=0 时选择这个 y y y 可以包含的特殊点数量。

最终的答案为 x = 0 x=0 x=0 和最大的 c n t [ y ] cnt[y] cnt[y] 所对应的 y y y

时间复杂度: O ( n + m ) O(n + m) O(n+m)

3.3 代码

pii points[maxn];
int sx[maxn], sy[maxn], posx[maxn];
int cnt[maxn];
int main() {
    int n, m, dx, dy;
    scanf("%d%d%d%d", &n, &m, &dx, &dy);
    for (int i = 1; i <= m; i++)    scanf("%d%d", &points[i].Fi, &points[i].Se);
    for (int i = 1; i < n; i++) {
        sx[i] = (sx[i - 1] + dx) % n;
        sy[i] = (sy[i - 1] + dy) % n;
        posx[sx[i]] = i;
    }
    for (int i = 1; i <= m; i++) {
        int x = points[i].Fi;
        int y = points[i].Se;
        int pos = posx[x];
        int y1 = sy[pos];
        int ty = (y - y1 + n) % n;
        cnt[ty]++;
    }
    int ansy = 0, maxvalue = 0;
    for (int i = 0; i < n; i++) {
        if (cnt[i] > maxvalue) {
            maxvalue = cnt[i];
            ansy = i;
        }
    }
    printf("0 %d\n", ansy);
    return 0;
}

4. Codeforces - 582C - Superior Periodic Subarrays(乱搞)

题目链接:https://codeforces.com/problemset/problem/582/C

难度: 2900 2900 2900

4.1 题意

给定一个周期为 n n n 无穷序列中连续 n n n a 0 , a 1 , ⋯   , a n − 1 a_0,a_1,\cdots,a_{n-1} a0,a1,,an1,统计有多少个以 l l l 开始,周期为 s s s 的后缀满足 0 ≤ l < n , 1 ≤ s < n 0 \le l <n,1 \le s < n 0l<n,1s<n 且对于任意的 k ∈ Z k \in \textbf{Z} kZ,都有 a k ≥ a l + k a_k \ge a_{l+k} akal+k

数据范围: 1 ≤ n ≤ 2 ⋅ 1 0 5 , 1 ≤ a i ≤ 1 0 6 1 \le n \le 2 \cdot 10^5,1 \le a_i \le 10^6 1n2105,1ai106

4.2 解题过程

因为原序列是无穷周期序列,所以满足

a i = a i  mod  n a_{i} = a_{i\text{ mod }n} ai=ai mod n

对于任意的 k ∈ Z k \in \textbf{Z} kZ,都有 a k ≥ a l + k a_k \ge a_{l+k} akal+k

利用同余的性质,这个条件可以转化为对于任意的 l ′ ≥ l l'\ge l ll,都满足

a l ′ ≥ a l ′ + k ⋅ gcd ⁡ ( s , n ) ( mod  n ) a_l' \ge a_{l' + k\cdot \gcd(s,n) (\text{mod }n)} alal+kgcd(s,n)(mod n)

这个条件可以进一步转化

a l ′ ≥ max ⁡ { a l ′ + k ⋅ gcd ⁡ ( s , n ) ( mod  n ) } a_l' \ge \max \left\{a_{l' + k\cdot \gcd(s,n) (\text{mod }n)}\right\} almax{al+kgcd(s,n)(mod n)}

会发现 max ⁡ { a l ′ + k ⋅ gcd ⁡ ( s , n ) ( mod  n ) } \max \left\{a_{l' + k\cdot \gcd(s,n) (\text{mod }n)}\right\} max{al+kgcd(s,n)(mod n)} 只与 l ′  mod  gcd ⁡ ( s , n ) l' \text{ mod } \gcd(s,n) l mod gcd(s,n) 有关,因此我们将最大值表达式记为 m a x v a l l ′  mod  gcd ⁡ ( s , n ) = max ⁡ { a l ′ + k ⋅ gcd ⁡ ( s , n ) ( mod  n ) } maxval_{l' \text{ mod } \gcd(s,n)} = \max \left\{a_{l' + k\cdot \gcd(s,n) (\text{mod }n)}\right\} maxvall mod gcd(s,n)=max{al+kgcd(s,n)(mod n)}

根据上面的推导,结合题中条件,会发现对于以 l l l 开始,周期为 s s s 的后缀,只有满足下面条件,才是一个合法的后缀:

对于任意的 l ′ ∈ [ l , l + s − 1 ] l' \in [l, l + s - 1] l[l,l+s1],都有
a l ′ = m a x v a l l ′  mod  gcd ⁡ ( s , n ) a_{l'} = maxval_{l' \text{ mod } \gcd(s,n)} al=maxvall mod gcd(s,n)

好了,理论分析完成,下面开始计算。

枚举 gcd ⁡ ( s , n ) = d \gcd(s,n)=d gcd(s,n)=d,之后对于所有的 r ∈ [ 0 , d ) r \in [0,d) r[0,d),通过从 [ 0 , n − 1 ] [0,n-1] [0,n1] 遍历 a a a 序列,计算 m a x v a l r maxval_r maxvalr

根据刚才的分析,我们需要找出所有的 a l ′ = m a x v a l l ′  mod  gcd ⁡ ( s , n ) a_{l'} = maxval_{l' \text{ mod } \gcd(s,n)} al=maxvall mod gcd(s,n)

因此我们再次遍历 a a a 数组,同时按照下面的方法构造 b b b 数组:

b l ′ = [ a l ′ = m a x v a l l ′  mod  gcd ⁡ ( s , n ) ] b_{l'}=[a_{l'} = maxval_{l' \text{ mod } \gcd(s,n)}] bl=[al=maxvall mod gcd(s,n)]

之后,我们将 b b b 数组看做一个环(基于 a a a 为无限周期序列的事实)。之后会发现,我们关心的是每一个连续的 1 1 1 序列。

为方便统计,我们 b b b 数组复制一份,放在其后面。

之后我们便可通过后缀和,计算以 l ′ l' l 为起始的连续 1 1 1 的个数。

这时,注意到问题转化为了连续 1 1 1 的区间个数问题。

但这样统计会产生一些无效的答案,问题在于当前枚举的 d d d,使得只有长度 s s s 满足 gcd ⁡ ( s , n ) = d \gcd(s,n)=d gcd(s,n)=d 的区间才可产生贡献。

所以我们还需预处理出最大长度为 s s s 时可产生贡献的长度数量,通过下面的方式统计:

for (int i = 1; i <= n; i++) {
		cnt[i] = cnt[i - 1] + (gcd[i] == d);
}

其中 gcd ⁡ [ i ] = gcd ⁡ ( i , n ) \gcd[i] = \gcd(i,n) gcd[i]=gcd(i,n)

之后便可枚举 l ′ l' l,设以 l ′ l' l 为起始的连续 1 1 1 的个数为 n u m num num,则答案加上 c n t [ min ⁡ ( n u m , n ) ] cnt[\min(num, n)] cnt[min(num,n)]

时间复杂度: O ( n ⋅ d ( n ) ) O(n \cdot d(n)) O(nd(n))

果然是个 2900 2900 2900 难度的硬核题啊!补都补得很费劲!!

4.3 代码

ll maxvalue[maxn];
ll gcd[maxn];
ll a[maxn];
set<ll> factor;
int cnt[maxn], val[2 * maxn];
int suffix[2 * maxn];
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%lld", &a[i]);
        gcd[i + 1] = __gcd(i + 1, n);
        if (i + 1 < n) factor.insert(gcd[i + 1]);
    }
    ll ans = 0;
    for (auto d: factor) {
        memset(maxvalue, 0, sizeof(maxvalue));
        for (int i = 0; i < n; i++) {
            maxvalue[i % d] = max(maxvalue[i % d], a[i]);
        }
        for (int i = 1; i <= n; i++) {
            cnt[i] = cnt[i - 1] + (gcd[i] == d);
        }
        for (int i = 0; i < n; i++) {
            val[i] = (maxvalue[i % d] == a[i]);
            val[i + n] = val[i];
        }
        for (int i = 2 * n - 1; i >= 0; i--) {
            if (val[i]) {
                suffix[i] = suffix[i + 1] + val[i];
            } else {
                suffix[i] = 0;
            }
        }
        for (int i = 0; i < n; i++) {
            ans += cnt[min(n, suffix[i])];
        }
    }
    printf("%lld\n", ans);
    return 0;
}

5. Codeforces - 303A - Lucky Permutation Triple(完全剩余系 + 构造)

题目链接:https://codeforces.com/problemset/problem/303/A

难度: 1400 1400 1400

5.1 题意

构造三个 0 0 0 n − 1 n-1 n1 的置换 a [ 0.. n − 1 ] a[0..n-1] a[0..n1] b [ 0.. n − 1 ] b[0..n-1] b[0..n1] c [ 0.. n − 1 ] c[0..n-1] c[0..n1] 使得任意的 i ∈ [ 0 , n − 1 ] i \in [0,n-1] i[0,n1] 都满足 a i + b i ≡ c i ( mod  n ) a_i + b_i \equiv c_i (\text{mod } n) ai+bici(mod n),或者确定解不存在。

数据范围: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105

5.2 解题过程

a i + b i ≡ c i ( mod  n ) a_i + b_i \equiv c_i (\text{mod } n) ai+bici(mod n)

等价于
a i + b i = c i + k n a_i + b_i = c_i + kn ai+bi=ci+kn

因此
∑ a i + ∑ b i = ∑ c i + k n 2 \sum a_i + \sum b_i = \sum c_i + kn^2 ai+bi=ci+kn2


n ( n + 1 ) 2 + n ( n + 1 ) 2 = n ( n + 1 ) 2 + k n 2 \frac{n(n+1)}{2} + \frac{n(n+1)}{2} = \frac{n(n+1)}{2} + kn^2 2n(n+1)+2n(n+1)=2n(n+1)+kn2

化简得
n + 1 2 = k \frac{n+1}{2}=k 2n+1=k

为保证 k k k 为整数, n n n 必为奇数,因此 n n n 为偶数时无解。

我们假设 a i = b i a_i=b_i ai=bi,则 c i = 2 a i c_i=2a_i ci=2ai,因为 n n n 为奇数,所以 gcd ⁡ ( n , 2 ) = 1 \gcd(n,2)=1 gcd(n,2)=1,所以 c i c_i ci 可以取遍 [ 1 , n ] [1,n] [1,n],因此构造方法显然。

时间复杂度: O ( n ) O(n) O(n)

6. Codeforces - 487C - Prefix Product Sequence(扩展威尔逊定理 + 构造)

题目链接:https://codeforces.com/problemset/problem/487/C

难度: 2600 2600 2600

6.1 题意

构造一个 1 1 1 n n n 的置换 a [ 1.. n ] a[1..n] a[1..n] 使得序列

{ a 1 ( mod  n ) , a 1 a 2 ( mod  n ) , ⋯   , ∏ i = 1 n a i ( mod  n ) } \left\{ a_1 (\text{mod } n),a_1 a_2 (\text{mod } n),\cdots,\prod_{i=1}^na_i(\text{mod } n) \right\} {a1(mod n),a1a2(mod n),,i=1nai(mod n)}
为一个 0 0 0 n − 1 n-1 n1 的置换,或者确定解不存在。

数据范围: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105

6.2 解题过程

首先, a [ n ] a[n] a[n] 必须为 n n n,否则会使生成的序列出现多个后缀 0 0 0

因此,我们只需要考虑 a [ 1.. n − 1 ] a[1..n-1] a[1..n1],设 p r e f i x [ i ] = ∏ k = 1 i a [ i ] prefix[i] = \prod_{k=1}^i a[i] prefix[i]=k=1ia[i]

分几种情况:

  1. n n n 为质数时, [ 1 , n − 1 ] [1,n-1] [1,n1] 均可求出逆元,所以可以如此构造
    p r e f i x [ i ] = i prefix[i] = i\\ prefix[i]=i
    因为
    p r e f i x [ i ] = ∏ k = 1 i a [ i ] = p r e f i x [ i − 1 ] ⋅ a [ i ] prefix[i] = \prod_{k=1}^i a[i] = prefix[i-1] \cdot a[i] prefix[i]=k=1ia[i]=prefix[i1]a[i]
    所以可以得到
    a [ i ] = p r e f i x [ i ] ⋅ i n v ( p r e f i x [ i − 1 ] ) a[i] = prefix[i] \cdot inv(prefix[i - 1]) a[i]=prefix[i]inv(prefix[i1])
    注意到 i + 1 i ≠ j + 1 j \frac{i+1}{i} \ne \frac{j+1}{j} ii+1=jj+1,因此可保证 a [ i ] a[i] a[i] 唯一。

因此按照上述方式构造即可。

  1. n = 1 n=1 n=1 时,答案就是 1 1 1
  2. n n n 为合数时,只有 n = 4 n=4 n=4 时有解,此时的解为 1   3   2   4 \texttt{1 3 2 4} 1 3 2 4
    根据扩展威尔逊定理,当 n > 4 n>4 n>4 n n n 为合数时,有 ( n − 1 ) ! = 0 ( mod  n ) (n-1)!=0(\text{mod }n) (n1)!=0(mod n),而对于任意的 n n n,必有 n ! = 0 ( mod  n ) n!=0(\text{mod }n) n!=0(mod n)。也就是说, p r e f i x prefix prefix 序列中会产生两个 0 0 0,不满足题意,故无解。

时间复杂度: O ( n ) O(n) O(n)(逆元可以线性预处理)。

6.3 错误点

没有特判 n = 1 n=1 n=1 n = 4 n=4 n=4 的情况。

6.4 代码

bool isnotprime[maxn];
ll prime[maxn];
int cnt = 0;
void linearsieve(ll range)
{
    for(ll i = 2; i <= range; i++)
    {
        if(!isnotprime[i]) prime[++cnt] = i;
        for(int j = 1; j <= cnt; j++)
        {
            if(i * prime[j] > range)    break;
            isnotprime[i * prime[j]] = true;
            if(i % prime[j] == 0)       break;
        }
    }
}
ll a[maxn], prefix[maxn];
ll inv[maxn];
void get_inv(int range, int mod)
{
    inv[1] = 1 % mod;
    for(int i = 2; i <= range; i++)
    {
        inv[i] = inv[mod % i] * (-mod / i + mod) % mod;
    }
}
int main()
{
    int n;
    linearsieve(100000);
    scanf("%d", &n);
    if (n == 4) {
        puts("YES");
        puts("1");
        puts("3");
        puts("2");
        puts("4");
        return 0;
    }
    if (isnotprime[n]) {
        puts("NO");
        return 0;
    }
    get_inv(n, n);
    a[1] = 1;
    prefix[1] = 1;
    a[n] = n;
    for (int i = 2; i < n; i++) {
        a[i] = 1LL * i * inv[prefix[i - 1]] % n;
        prefix[i] = prefix[i - 1] * a[i] % n;
    }
    puts("YES");
    for (int i = 1; i <= n; i++) {
        printf("%lld\n", a[i]);
    }
    return 0;
}

7. SGU - 106 - The equation(扩展欧几里得)

题目链接:https://codeforces.com/problemsets/acmsguru/problem/99999/106

7.1 题意

给定 a , b , c , x 1 , x 2 , y 1 , y 2 a,b,c,x_1,x_2,y_1,y_2 a,b,c,x1,x2,y1,y2,求 a x + b y + c = 0 ax+by+c=0 ax+by+c=0 满足 x 1 ≤ x ≤ x 2 x_1 \le x \le x_2 x1xx2 y 1 ≤ y ≤ y 2 y_1 \le y \le y_2 y1yy2 的整数解数量。

题目保证所有数字的绝对值不超过 1 0 8 10^8 108

7.2 解题过程

如果不考虑边界情况的话,实际上就是要先解出同余方程 a x ≡ c ( mod  b ) ax \equiv c(\text{mod } b) axc(mod b) 的最小非负整数解 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)

由此可得到方程的通解:
{ x t = x 0 + b gcd ⁡ ( a , b ) ⋅ t y t = y 0 − a gcd ⁡ ( a , b ) ⋅ t \begin{aligned} \left\{ \begin{array}{c} x_t=x_0+\frac{b}{\gcd(a,b)} \cdot t\\ y_t=y_0-\frac{a}{\gcd(a,b)} \cdot t\\ \end{array} \right. \end{aligned} {xt=x0+gcd(a,b)btyt=y0gcd(a,b)at
之后将 x t x_t xt y t y_t yt 代入到不等式中,得到两个区间。这两个区间的交集长度就是答案。

但到这里就完事了吗?

Wrong   answer   on   test   8! \texttt{Wrong answer on test 8!} Wrong answer on test 8!

请看 7.3!

7.3 错误点

  1. 没有特判 a = 0 a=0 a=0 b = 0 b=0 b=0 的情况。
  2. 处理左边界时需要向上取整,处理右边界时需要想下去中。
  3. 向上取整和向下取整都需要分正负两种情况讨论。

7.4 代码

ll a, b, c;
ll x1, x2, y1, y2;
long long res = 0;
int cnt = 0;
long long gcd(long long a, long long b)
{
    if(a % b == 0)
        return b;
    else
        return gcd(b, a % b);
}
long long ex_gcd(long long a, long long b, long long &x, long long &y)
{
    if (b == 0)
    {
        x = 1;
        y = 0;
        return a;
    }
    else
    {
        long long r = ex_gcd(b, a % b, y, x);
        y -= x * (a / b);
        return r;
    }
}
long long mod(long long number, long long k)
{
    if(number < 0)
        return (-((-number) % k) + k) % k;
    else
        return number % k;
}
bool solve(ll a, ll b, ll c) //ax = b mod c
{
    ll x, y;
    ex_gcd(a, c, x, y);
    ll d = gcd(a, c);
    if(b % d)   return false;
    x = mod(mod(x, c) + c, c);
    res = mod(x * b / d, c / d);
    return true;
}
ll up(ll a, ll b) {
    if (a % b == 0) return a / b;
    else {
        if (a * b > 0) {
            return a / b + 1;
        } else {
            return a / b;
        }
    }
}
ll down(ll a, ll b) {
    if (a % b == 0) return a / b;
    else {
        if (a * b > 0)  return a / b;
        else return a / b - 1;
    }
}
int main()
{
    scanf("%lld%lld%lld", &a, &b, &c);
    scanf("%lld%lld", &x1, &x2);
    scanf("%lld%lld", &y1, &y2);
    if (a == 0 && b == 0) {
        if (c == 0) {
            printf("%lld\n", (y2 - y1 + 1) * (x2 - x1 + 1));
        } else {
            puts("0\n");
        }
        return 0;
    } else if (a == 0) {
        if (b > 0) {
            if (-b * y2 <= c && c <= -b * y1)   printf("%lld\n", x2 - x1 + 1);
            else puts("0");
        } else {
            if (-b * y1 <= c && c <= -b * y2)   printf("%lld\n", x2 - x1 + 1);
            else puts("0");
        }
        return 0;
    } else if (b == 0) {
        if (a > 0) {
            if (-a * x2 <= c && c <= -a * x1)   printf("%lld\n", y2 - y1 + 1);
            else puts("0");
        } else {
            if (-a * x1 <= c && c <= -a * x2)   printf("%lld\n", y2 - y1 + 1);
            else puts("0");
        }
        return 0;
    }
    if (solve(a, -c, b)) {
        ll gcd = __gcd(a, b);
        ll x0 = res;
        ll y0 = -(c + a * x0) / b;
        ll k1 = b / gcd;
        ll k2 = a / gcd;
        ll l1, l2, r1, r2;
        if (k1 < 0) {
            l1 = up(x2 - x0, k1);
            r1 = down(x1 - x0, k1);
        } else {
            l1 = up(x1 - x0, k1);
            r1 = down(x2 - x0, k1);
        }
        if (k2 < 0) {
            l2 = up(y0 - y1, k2);
            r2 = down(y0 - y2, k2);
        } else {
            l2 = up(y0 - y2, k2);
            r2 = down(y0 - y1, k2);
        }
        ll answer = 0;
        if (min(r1, r2) - max(l1, l2) < 0) answer = 0;
        else answer = min(r1, r2) - max(l1, l2) + 1;
        printf("%lld\n", answer);
    } else {
        puts("0");
    }
    return 0;
}

8. Codeforces - 547A - Mike and Frog(扩展欧几里得 + 分类讨论)

题目链接:https://codeforces.com/problemset/problem/547/A/

难度: 1900 1900 1900

8.1 题意

给定 m , h i , 0 , a i , x i , y i ( i ∈ [ 1 , 2 ] ) m,h_{i,0},a_i,x_i,y_i(i \in [1,2]) m,hi,0,ai,xi,yi(i[1,2]),定义 h i , j + 1 = ( x i ⋅ h i , j + y i )  mod  m h_{i,j+1}=(x_i\cdot h_{i,j} + y_i) \text{ mod } m hi,j+1=(xihi,j+yi) mod m,求最小非负整数 k k k 使得 h i , k = a i h_{i,k}=a_i hi,k=ai

题目保证 h i , 0 ≠ a i h_{i,0} \ne a_i hi,0=ai

数据范围: 2 ≤ m ≤ 1 0 6 , 0 ≤ h i , 0 , a i , x i , y i < m 2 \le m \le 10^6,0 \le h_{i,0},a_i,x_i,y_i < m 2m106,0hi,0,ai,xi,yi<m

8.2 解题过程

本题有诸多坑点,在于一些边界条件的判断。

先讨论基本做法:

根据同余的性质, h i h_i hi 到达 a i a_i ai 的时间是存在有限循环节的。

[ 1 , 2 m ] [1,2m] [1,2m] 范围内循环,暴力求解 h i h_i hi 第一次到达 a i a_i ai 的时间 t i t_i ti,以及第二次到达 a i a_i ai 的时间 t i ′ t_i' ti,则可计算出周期 T i = t i ′ − t i T_i = t_i'-t_i Ti=titi

之后只要求解同余方程:
t 1 + k 1 T 1 = t 2 + k 2 T 2 t_1+k_1 T_1=t_2+k_2 T_2 t1+k1T1=t2+k2T2
并枚举所有的解,对答案取 min 即可得到最终答案。

时间复杂度: O ( m ) O(m) O(m)

但到这里就完事了吗?

Wrong   answer   on   test   x! \texttt{Wrong answer on test x!} Wrong answer on test x!

请看 8.3!

8.3 错误点

C++ 中,全局变量默认值为 0 0 0,所以下面的值如果为 0 0 0,证明这个值不存在。

  1. t 1 = t 2 > 0 t_1=t_2>0 t1=t2>0 时,如果直接解同余方程的话,答案一定是非法的 0 0 0。而此情况下,答案就是 t 1 t_1 t1
  2. t 1 = 0 t_1=0 t1=0 t 2 = 0 t_2=0 t2=0 时,因为 h i , 0 ≠ a i h_{i,0} \ne a_i hi,0=ai,所以永远无法达到 a 1 a_1 a1 a 2 a_2 a2,输出无解。下面的讨论,都是在 t 1 ≠ t 2 > 0 t_1\ne t_2>0 t1=t2>0 的基础上的。
  3. t 1 = t 2 > 0 t_1=t_2>0 t1=t2>0 T 1 = T 2 = 0 T_1=T_2=0 T1=T2=0 时, h 1 , k h_{1,k} h1,k h 2 , k h_{2,k} h2,k 永远无法分别同时达到 a 1 a_1 a1 a 2 a_2 a2,因此输出无解。
  4. T 1 = 0 T_1=0 T1=0 T 2 ≠ 0 T_2 \ne 0 T2=0 时, h 1 , k h_{1,k} h1,k 只有在 t 1 t_1 t1 时才可等于 a 1 a_1 a1,因此我们必须只靠 t 2 t_2 t2 T 2 T_2 T2 来达到 t 1 t_1 t1,因此只有在 t 2 ≠ t 1 t_2 \ne t_1 t2=t1 并且 T 2 ∣ ( t 1 − t 2 ) T_2|(t_1-t_2) T2(t1t2) 时才有解,此时的解为 t 1 t_1 t1
  5. T 1 ≠ 0 T_1 \ne 0 T1=0 T 2 = 0 T_2 = 0 T2=0 与 4 同理。
  6. 根据自己的解同余方程的写法,规定 T 2 T_2 T2 为模数。因此就存在边界情况:当 T 2 = 1 T_2=1 T2=1 时,任何数模 T 2 T_2 T2 都为 0 0 0,因此此时答案为不合法的 0 0 0。但是这种情况下,只要时间大于等于 t 2 t_2 t2 h 2 , k h_{2,k} h2,k 就会一直等于 a 2 a_2 a2
    因此我们仍然需要特判,这又有几种情况:
    (1)如果 t 1 ≥ t 2 t_1\ge t_2 t1t2,那就等着 h 2 , k h_{2,k} h2,k 等于 a 2 a_2 a2 就完事了,因此答案就是 t 1 t_1 t1
    (2)如果 t 1 < t 2 t_1< t_2 t1<t2,如果 T 1 ∣ ( t 2 − t 1 ) T_1|(t_2-t_1) T1(t2t1),那样的话只靠 t 1 t_1 t1 T 1 T_1 T1 就可直接凑出 t 2 t_2 t2,因此此时答案为 t 2 t_2 t2;否则,就需要在 T 1 ∣ ( t 2 − t 1 ) T_1|(t_2-t_1) T1(t2t1) 的情况下多一份的 T 1 T_1 T1 时间,使得多出来的时间靠 T 2 T_2 T2 来凑出 t 2 t_2 t2

好了,经过这么多的讨论,你可能还是很懵逼,那没事,你再看看代码吧!

另外,本人观察了一下,这题在场上 FST 了一车人,爆出了 Div.2 C 题 9/1515 的惨案!

本题最有价值的错误点就是在解同余方程时要特别注意模数为 1 1 1 的情况!

8.4 代码

ll h[maxn][2];
ll x[2], y[2], a[2];
ll t[2], T[2];
long long ans[maxn];
int cnt = 0;
long long ex_gcd(long long a, long long b, long long &x, long long &y)
{
    if (b == 0)
    {
        x = 1;
        y = 0;
        return a;
    }
    else
    {
        long long r = ex_gcd(b, a % b, y, x);
        y -= x * (a / b);
        return r;
    }
}
long long mod(long long number, long long k)
{
    if(number < 0)
        return (-((-number) % k) + k) % k;
    else
        return number % k;
}
bool solve(ll a, ll b, ll c) //ax = b mod c
{
    ll x, y;
    ex_gcd(a, c, x, y);
    ll d = __gcd(a, c);
    if(b % d)   return false;
    x = mod(mod(x, c) + c, c);
    ans[++cnt] = mod(x * b / d, c / d);
    for(ll i = 1; i <= d; i++)   ans[++cnt] = mod(ans[1] + i * c / d, c);
    return true;
}
int main()
{
    int m;
    scanf("%d", &m);
    scanf("%lld%lld", &h[0][0], &a[0]);
    scanf("%lld%lld", &x[0], &y[0]);
    scanf("%lld%lld", &h[0][1], &a[1]);
    scanf("%lld%lld", &x[1], &y[1]);
    for (int i = 1; i <= 2 * m; i++) {
        h[i][0] = (h[i - 1][0] * x[0] + y[0]) % m;
        h[i][1] = (h[i - 1][1] * x[1] + y[1]) % m;
        if (h[i][0] == a[0]) {
            if (!t[0])  t[0] = i;
            else if (!T[0]) T[0] = i - t[0];
        }
        if (h[i][1] == a[1]) {
            if (!t[1])  t[1] = i;
            else if (!T[1]) T[1] = i - t[1];
        }
    }
    if (t[0] == t[1] && t[0] > 0) {
        printf("%lld\n", t[0]);
        return 0;
    }
    if (t[0] == 0 || t[1] == 0 || (T[0] == 0 && T[1] == 0)) {
        puts("-1");
        return 0;
    }
    if (T[0] == 0) {
        if (t[0] >= t[1] && (t[0] - t[1]) % T[1] == 0) {
            printf("%lld\n", t[0]);
        } else {
            puts("-1");
        }
        return 0;
    } else if (T[1] == 0) {
        if (t[1] >= t[0] && (t[1] - t[0]) % T[0] == 0) {
            printf("%lld\n", t[1]);
        } else {
            puts("-1");
        }
        return 0;
    }
    if (solve(T[0], t[1] - t[0], T[1])) {
        ll answer = 0x3f3f3f3f3f3f3f3f;
        for (int i = 1; i <= cnt; i++) {
            answer = min(answer, ans[i] * T[0] + t[0]);
        }
        if (T[1] == 1) {
            if (t[0] != t[1]) {
                if (t[0] < t[1]) {
                    if ((t[1] - t[0]) % T[0] == 0) answer = t[1] - t[0] + t[0];
                    else {
                        answer = ((t[1] - t[0]) / T[0] + 1) * T[0] + t[0];
                    }
                } else {
                    answer = t[0];
                }
            }
            else answer = t[0];
        }
        printf("%lld\n", answer);
    } else {
        puts("-1");
    }
    return 0;
}

9. Codeforces - 819D - Mister B and Astronomers(同余的应用 + 乱搞)

题目链接:https://codeforces.com/contest/819/problem/D

难度: 2900 2900 2900

9.1 题意

n n n 个观察员,第一个观察员在 0 0 0 秒开始观察星空,随后第 i i i 个观察员会在第 i − 1 i-1 i1 个观察员之后 a i a_i ai 秒观察,第一个观察员也会在第 n n n 个观察员之后 a 1 a_1 a1 秒观察,有一颗星星每隔 T T T 秒闪烁一次,闪烁时一定是整数秒,问每个观察员有多少种可能成为第一个观察到这颗星星的人。

数据范围: 1 ≤ T ≤ 1 0 9 , 2 ≤ n   l e 2 ⋅ 1 0 5 , 1 ≤ a i ≤ 1 0 9 1 \le T \le 10^9,2 \le n\ le 2 \cdot 10^5,1 \le a_i \le 10^9 1T109,2n le2105,1ai109

9.2 解题过程

自己的第二道 2900 2900 2900 的题目了,真的太难补了!

首先,我们可以重新表述这个问题:有 T T T 个石子,从 [ 0 , T − 1 ] [0,T-1] [0,T1] 编号,第一个人第一次取 0 0 0 号石子,之后第 i i i 个人会取第 i − 1 i-1 i1 个人之后的第 a i a_i ai 个石子(某个位置的石头被取走之后,再经过这个位置就无法得到石头了),问最终每个人能够取到多少个石子。

我们需要设置两个值,方便后续的计算:
S = ∑ i = 1 n a i T [ 1 ] = 0 T [ i ] = ∑ j = 2 i a i S = \sum_{i=1}^n a_i\\ T[1]=0\\ T[i] = \sum_{j=2}^{i} a_i S=i=1naiT[1]=0T[i]=j=2iai
其中, T [ i ] T[i] T[i] 的含义是 i i i 第一次取的石子编号。

因此,对于第 i i i 个人,其可以取到的石子编号的集合为
{ T [ i ] + k ⋅ S ( mod  T ) } \left\{ T[i] + k \cdot S(\text{mod } T)\right\} {T[i]+kS(mod T)}
根据同余的性质,我们可以发现,第 i i i 个人只会经过起点为 T [ i ]  mod  gcd ⁡ ( S , T ) T[i] \text{ mod } \gcd(S, T) T[i] mod gcd(S,T),步长为 gcd ⁡ ( S , T ) \gcd(S,T) gcd(S,T),长度为 T gcd ⁡ ( S , T ) \frac{T}{\gcd(S,T)} gcd(S,T)T 的环,而不会经过任何的其他环。

所以,我们可以对每个环独立考虑,每个人第一个拿到石子相当于在环上占有了某一个位置,因此我们只需考虑每个人的相对位置。

在传统的环上面,每经过 S S S 时间,会进行一次跳跃。并且我们注意到点的跳跃顺序是固定且循环的。如果我们按照跳到的点的顺序重新将环上面的点排列的话,这个环每经过 S S S 时间经过一次跳跃就变成了只走一步。

在每个环上面计算出每个人从环的起点(假设范围强制规定为 [ 0 , gcd ⁡ ( S , T ) − 1 ] [0,\gcd(S,T)-1] [0,gcd(S,T)1],因为只考虑相对位置)需要多少步(每步长为 S S S,相当于一轮)才能到达第一个吃到石子的位置。

之后,将每个人的步数丢进一个 set 中,则相邻两个人步数的差值,就是前一个人的答案。这是因为,在差值那一段,没有任何人去占用石子,所以每次经过 S S S 时间轮到他的时候,都可以拿到石子,但是一旦他走到了下一个人占有的位置,显然下一个人先走到了这个位置,因此前一个人也就结束了。

对于最后一个人,特殊处理即可(环长减去已经拿走的石子数量)。

9.3 代码

ll start[maxn];
ll a[maxn], t[maxn];
ll ans[maxn];
vector<ll> ve[maxn];
set<ll> se[maxn];
unordered_map<ll, int> mp, mp2;
unordered_map<ll, bool> vis;
long long gcd(long long a, long long b)
{
    if(a % b == 0)
        return b;
    else
        return gcd(b, a % b);
}
long long ex_gcd(long long a, long long b, long long &x, long long &y)
{
    if (b == 0)
    {
        x = 1;
        y = 0;
        return a;
    }
    else
    {
        long long r = ex_gcd(b, a % b, y, x);
        y -= x * (a / b);
        return r;
    }
}
long long mod(long long number, long long k)
{
    if(number < 0)
        return (-((-number) % k) + k) % k;
    else
        return number % k;
}
ll solve(ll a, ll b, ll c) //ax = b mod c
{
    ll x, y;
    ex_gcd(a, c, x, y);
    ll d = gcd(a, c);
    if(b % d)   return false;
    x = mod(mod(x, c) + c, c);
    return mod(x * b / d, c / d);
}
int main()
{
    int n;
    ll T;
    scanf("%lld%d", &T, &n);
    ll sum = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        sum += a[i];
        if (i > 1) {
            t[i] = t[i - 1] + a[i];
        }
    }
    ll gcd = __gcd(T, sum);
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        ll x = t[i] % gcd;
        ll y = t[i] % T;
        if (vis[y]) continue;
        vis[y] = true;
        if (mp2.count(x) > 0)   ve[mp2[x]].pb(i);
        else {
            ve[++cnt].pb(i);
            mp2[x] = cnt;
            start[cnt] = x;
        }
    }
    for (int i = 1; i <= cnt; i++) {
        ll pos = start[i];
        for (int j = 0; j < ve[i].size(); j++) {
            ll val = t[ve[i][j]] % T;
            ll res = solve(sum, (val - pos + T) % T, T);
            se[i].insert(res);
            mp[res] = ve[i][j];
        }
        set<ll>::iterator iter;
        ll temp = 0;
        for (iter = se[i].begin(); iter != se[i].end(); ++iter) {
            if (iter == prev(se[i].end())) ans[mp[*iter]] = T / gcd - temp;
            else {
                ll current = *iter;
                ll nxt = *next(iter);
                ans[mp[current]] = nxt - current;
                temp += ans[mp[current]];
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        if (i > 1)  printf(" ");
        printf("%lld", ans[i]);
    }
    return 0;
}

10. Codeforces - 722F - Cyclic Cipher(同余方程的性质 + 双指针)

题目链接:https://codeforces.com/problemset/problem/722/F

难度: 2800 2800 2800

10.1 题意

n n n 个带周期的无穷序列,第 i i i 个序列 a i a_i ai 的周期为 T i T_i Ti,每个序列的第 1 1 1 a i , 1 a_{i, 1} ai,1 到第 T i T_i Ti a i , T i ∈ [ 1 , m ] a_{i,T_i} \in [1, m] ai,Ti[1,m] 给定,且两两不相同。

对于每种可能的元素取值 v ( 1 ≤ v ≤ m ) v(1 \le v \le m) v(1vm),找到一个下标 j ≤ 1 0 100 j \le 10^{100} j10100 且序列 { a 1 , j , a 2 , j , ⋯   , a n , j } \left\{ a_{1,j}, a_{2,j}, \cdots, a_{n,j} \right\} {a1,j,a2,j,,an,j} 中连续的 v v v 组成的区间最长,只对每个 v v v 输出区间长度。

数据范围: 1 ≤ n , m ≤ 1 0 5 , 1 ≤ T i ≤ 40 , ∑ i = 1 n T i ≤ 2 ⋅ 1 0 5 1 \le n,m \le 10^5, 1 \le T_i \le \textbf{40}, \sum_{i=1}^n T_i \le 2 \cdot 10^5 1n,m105,1Ti40,i=1nTi2105

10.2 解题过程

首先,我们可以对每一个 v v v 独立考虑。

设一段合法区间为 [ l , r ] [l,r] [l,r],则首先要满足的是 v v v 必须出现在 [ l , r ] [l,r] [l,r] 中的所有行中。

之后我们就要判定这些 v v v 是否可能存在于同一列中。

v v v 在第 i i i 行中的出现位置为 p i ∈ [ 0 , T i − 1 ] p_i \in [0, T_i-1] pi[0,Ti1],则对于任意的 i , j ∈ [ l , r ] , i ≠ j i,j\in[l,r], i \ne j i,j[l,r],i=j,都必须满足
p i + k i ⋅ T i = p j + k j ⋅ T j p_i + k_i \cdot T_i = p_j + k_j \cdot T_j pi+kiTi=pj+kjTj
这个方程有解的充要条件为
gcd ⁡ ( T i , T j ) ∣ ( p i − p j ) \gcd(T_i, T_j) | (p_i-p_j) gcd(Ti,Tj)(pipj)

如果我们暴力求解的话,很容易 TLE \texttt{TLE} TLE,注意到若 T i = T j T_i=T_j Ti=Tj,则必须满足 p i = p j p_i=p_j pi=pj,换句话说,对于特定的 T T T 会唯一确定一个 p p p。而我们又注意到:
1 ≤ T i ≤ 40 1 \le T_i \le \textbf{40} 1Ti40
因此我们可以利用这个条件,存储每一种 T T T 所对应的 p p p,我们只需判定已经出现过的 T T T 是否都满足要求,这样判定的时间复杂度就变成了 O ( 40 ) O(40) O(40)

同时,这种方式的利用需要借助于双指针,即固定左指针,让右指针移动到冲突位置为止,记录最大答案。

总的时间复杂度为: O ( 40 ⋅ ∑ i = 1 n T i ) O(40 \cdot \sum_{i=1}^n T_i) O(40i=1nTi)

10.3 代码

int T[maxn], ans[maxn];
int cnt[45], pos[45];
vector<int> a[maxn];
vector<pii> b[maxn];
bool check(int t, int x) {
    for (int i = 1; i <= 40; i++) {
        if (cnt[i] && (pos[i] - x) % __gcd(t, i)) return false;
    }
    pos[t] = x;
    cnt[t]++;
    return true;
}
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i++) {
        scanf("%d", &T[i]);
        for (int j = 0; j < T[i]; j++) {
            int x;
            scanf("%d", &x);
            a[i].emplace_back(x);
            b[x].emplace_back(pii(i, j));
        }
    }
    for (int i = 1; i <= m; i++) {
        memset(cnt, 0, sizeof(cnt));
        memset(pos, 0, sizeof(pos));
        for (int l = 0, r = 0; l < b[i].size(); l++) {
            while (r < b[i].size() && b[i][r].first - b[i][l].first == r - l) {
                if (!check(T[b[i][r].first], b[i][r].second)) break;
                r++;
            }
            ans[i] = max(ans[i], r - l);
            cnt[T[b[i][l].first]]--;
        }
        printf("%d\n", ans[i]);
    }
    return 0;
}

11. POJ - 3530 - A Modular Arithmetic Challenge(辗转相除法的应用)

题目链接:http://poj.org/problem?id=3530

11.1 题意

给定四个正整数 M , D , L , R M,D,L,R M,D,L,R,求最小非负整数 x x x 使得 L ≤ ( D x  mod  M ) ≤ R L \le (Dx \text{ mod } M) \le R L(Dx mod M)R,或者确定解不存在。

数据范围: 1 ≤ M , D , L , R ≤ 2 ⋅ 1 0 9 1 \le M,D,L,R \le 2 \cdot 10^9 1M,D,L,R2109

11.2 解题过程

L ≤ ( D x  mod  M ) ≤ R L \le (Dx \text{ mod } M) \le R L(Dx mod M)R

可以转化为

L ≤ D x − M y ≤ R D x − R ≤ M y ≤ D x − L − R ( mod  D ) ≤ ( M  mod  D ) y ( mod  D ) ≤ − L ( mod  D ) L \le Dx-My \le R\\ Dx-R \le My \le Dx-L\\ -R(\text{mod }D) \le (M \text{ mod }D)y(\text{mod }D) \le -L(\text{mod }D)\\ LDxMyRDxRMyDxLR(mod D)(M mod D)y(mod D)L(mod D)

最后化简出来的形式与最初的形式一致,所以我们可以类比辗转相除法的思路,递归求解。


solve(ll   m,   ll   d,   ll   l,   ll   r) \texttt{solve(ll m, ll d, ll l, ll r)} solve(ll m, ll d, ll l, ll r)
用于求解最小非负整数 x x x 使得 L ≤ ( D x  mod  M ) ≤ R L \le (Dx \text{ mod } M) \le R L(Dx mod M)R,在递归求解时,我们会调用

 ll y = solve(d, m % d, ((-r) % d + d) % d, ((-l) % d + d) % d);

这里解出的 y y y 就是最后一个不等式中的 y y y,因此直接将这个 y y y 代回原不等式即可利用下界求出最小的 x x x

如果存在 x x x 使得 L ≤ D x ≤ R L \le Dx \le R LDxR,证明在一圈 x ∈ [ 0 , M − 1 ] x \in [0,M-1] x[0,M1] 之内跳 x x x 次即可跳到该区间,否则就需要 − M y -My My 的起点偏移量(显然这样会使 x x x 更大),此时直接 O ( 1 ) O(1) O(1) 求解即可,无需向下递归。这一情况等价于 ⌊ R D ⌋ ⋅ D ≥ L \lfloor \frac{R}{D} \rfloor \cdot D \ge L DRDL,所以我们使用这个条件判断即可。

注意利用下界 L L L 求解时,需要使用以 L − 1 L-1 L1 为上界时的解再加上 1 1 1

另外,递归处理时,如果遇到 L > R L>R L>R 或者 D = 0 D=0 D=0 或者递归解出的 y = − 1 y=-1 y=1 的情况,直接返回 − 1 -1 1

时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)

11.3 错误点

  1. 求解 − x m o d    d -x \mod d xmodd 时,应写成 (-x) % d,而不可以写成 − x -x % d x
  2. 最初要将 R R R M − 1 M-1 M1 取 min,使其合法化,否则会 WA

11.4 代码

ll solve(ll m, ll d, ll l, ll r) {
    if (l > r || d == 0)  return -1;
    if (r / d * d >= l) return (l - 1) / d + 1;
    ll x = solve(d, m % d, ((-r) % d + d) % d, ((-l) % d + d) % d);
    if (x == -1)    return -1;
    return (l + m * x - 1) / d + 1;
}
int main()
{
    int t;
    ll m, d, l, r;
    scanf("%d", &t);
    while (t--) {
        scanf("%lld%lld%lld%lld", &m, &d, &l, &r);
        r = min(r, m - 1);
        printf("%lld\n", solve(m, d, l, r));
    }
    return 0;
}

12. Vijos - 1504 - 强大的区间(辗转相除法的应用)

题目链接:https://vijos.org/p/1504

12.1 题意

给定两个十进制小数 a , b a,b a,b,求最小的正整数 k k k,使得存在整数 x x x 满足 a ⋅ k ≤ x ≤ b ⋅ k a \cdot k \le x \le b \cdot k akxbk

题目保证 a , b a,b a,b 的有效数字小于 300 300 300 位。

12.2 解题过程

首先,我们发现将 [ a , b ] [a,b] [a,b] 整体向左平移(以 1 1 1 为最小单位),并不影响答案,因为我们只关心小数点后面的部分。设平移之后的区间为 [ a ′ , b ′ ] [a',b'] [a,b]

现在问题就是找到最小的 k k k,使得 a k ≤ x ≤ b k ak \le x \le bk akxbk 区间中至少含一个整数。

如果 a ′ ≤ 0 a' \le 0 a0 或者 b ′ ≤ 1 b' \le 1 b1,则答案就是 1 1 1,最小的 x x x 值为 ⌈ a ⌉ \lceil a \rceil a

否则,我们对不等式进行如下变换:
a k ≤ x ≤ b k a ≤ k x ≤ b 1 b ≤ k x ≤ 1 a x b ≤ k ≤ x a ak \le x \le bk\\ a \le \frac{k}{x} \le b\\ \frac{1}{b} \le \frac{k}{x} \le \frac{1}{a}\\ \frac{x}{b} \le k \le \frac{x}{a} akxbkaxkbb1xka1bxkax

之后,我们会发现得到了一个问题规模更小的相同问题,因此可以递归求解。
递归时,第一层(即在 main 函数中调用的那层)返回最小的 k k k 值向上取整,即最终答案;
其它层返回当前层区间包含的最小的 x x x 值。回溯时,该层区间包含的最小的 x x x 值是下一层区间包含的最小的 x x x 值的 a a a 倍再向上取整。

时间复杂度: O ( 300 log ⁡ n ) O(300 \log n) O(300logn)

12.3 代码

本题采用 Java 实现,可在 Java 1.8 环境下通过编译。

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Scanner;
public class Main {
    private static BigDecimal solve(BigDecimal a, BigDecimal b, int depth) {
        BigDecimal c = a;
        c = c.setScale(0, BigDecimal.ROUND_DOWN);
        BigDecimal newa = a.subtract(c);
        BigDecimal newb = b.subtract(c);
        if (newa.compareTo(BigDecimal.ZERO) == 0) {
            if (depth == 1) {
                return BigDecimal.ONE.setScale(0, BigDecimal.ROUND_UP);
            } else {
                return a.setScale(0, BigDecimal.ROUND_UP);
            }
        }
        if (newb.compareTo(BigDecimal.ONE) >= 0) {
            if (depth == 1) {
                return BigDecimal.ONE.setScale(0, BigDecimal.ROUND_UP);
            } else {
                return a.setScale(0, BigDecimal.ROUND_UP);
            }
        }
        BigDecimal x = solve(BigDecimal.ONE.divide(newb, 300, BigDecimal.ROUND_UP)
                , BigDecimal.ONE.divide(newa, 300, BigDecimal.ROUND_DOWN)
                , depth + 1);
        if (depth == 1) {
            return x.setScale(0, BigDecimal.ROUND_UP);
        } else {
            return a.multiply(x).setScale(0, BigDecimal.ROUND_UP);
        }
    }
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        BigDecimal a = cin.nextBigDecimal();
        BigDecimal b = cin.nextBigDecimal();
        BigDecimal ans = solve(a, b, 1);
        System.out.println(ans.setScale(0, BigDecimal.ROUND_UP));
    }
}

15. 51Nod - 1187 - 寻找分数(类欧几里得算法)

题目链接:http://www.51nod.com/Challenge/Problem.html#problemId=1187

难度: 6 6 6 级题

15.1 题意

给出 a , b , c , d ( 1 ≤ a , b , c , d ≤ 1 0 9 ) a,b,c,d(1 \le a,b,c,d \le 10^9) a,b,c,d(1a,b,c,d109),找到一个分数 p q \frac{p}{q} qp,使得 a b < p q < c d \frac{a}{b} < \frac{p}{q} < \frac{c}{d} ba<qp<dc,并且使 q q q 最小;如果 q q q 相同,输出 p p p 最小的。

共有 T ( 1 ≤ T ≤ 1 0 4 ) T(1 \le T \le 10^4) T(1T104) 组数据。

15.2 解题过程

本题需要分类讨论:

  1. a = 0 a=0 a=0 时,形式为
    0 < p q < c d q > p ⋅ d c 0 < \frac{p}{q} < \frac{c}{d}\\ q > p \cdot \frac{d}{c} 0<qp<dcq>pcd
    显然,此时的答案为 p = 1 , q = ⌊ d c ⌋ + 1 p=1,q=\lfloor \frac{d}{c} \rfloor + 1 p=1,q=cd+1
  2. a ≥ b a \ge b ab 时,可以将左右边界同时向左移动到 a b < 1 \frac{a}{b} < 1 ba<1,相当于求解
    a  mod  b b < p q − ⌊ a b ⌋ < c d − ⌊ a b ⌋ \frac{a\text{ mod }b}{b} < \frac{p}{q} - \lfloor \frac{a}{b} \rfloor < \frac{c}{d} - \lfloor \frac{a}{b} \rfloor ba mod b<qpba<dcba
    之后再将 p p p 加上 q ⋅ ⌊ a b ⌋ q \cdot \lfloor \frac{a}{b} \rfloor qba 即可得到答案,因此递归求解即可。
  3. a < b a < b a<b c > d c > d c>d 时,答案显然,我们只需让 p q = 1 \frac{p}{q}=1 qp=1,即可得到最优解。
  4. 其他情况,证明左右边界都不超过 1 1 1,按照上一个题类似的思路,递归求解
    d c < q p < b a \frac{d}{c} < \frac{q}{p} < \frac{b}{a} cd<pq<ab
    即可。

时间复杂度: O ( T ⋅ log ⁡ n ) O(T \cdot \log n) O(Tlogn)

15.3 代码

void solve(ll a, ll b, ll c, ll d, ll &p, ll &q) {
    if (a == 0) {
        q = d / c + 1;
        p = 1;
        return;
    }
    if (a >= b) {
        solve(a % b, b, c - (a / b) * d, d, p, q);
        p += (a / b) * q;
        return;
    }
    if (c > d) {
        p = 1;
        q = 1;
        return;
    }
    solve(d, c, b, a, q, p);
    return;
}
int main()
{
    int t;
    ll a, b, c, d;
    scanf("%d", &t);
    while (t--) {
        scanf("%lld%lld%lld%lld", &a, &b, &c, &d);
        ll p, q;
        solve(a, b, c, d, p, q);
        ll gcd = __gcd(p, q);
        printf("%lld/%lld\n", p / gcd, q / gcd);
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值