同余分析

同余类分析法 的基本思想是:利用同余关系将全体整数(或问题的状态空间)划分为若干个互不相交的等价类。

  • 分类标准:选定一个基准数 \(M\),根据每个数 \(x\) 满足 \(x \equiv r \pmod{M}\) 的余数 \(r\) 进行归类。
  • 分析逻辑:如果一个性质对于某个余数 \(r\) 成立,那么通过给 \(x\) 不断加减 \(M\)(即在同一个同余类内平移),往往能推导出该类中所有(或大部分)元素的性质。

例题:P3951 [NOIP 2017 提高组] 小凯的疑惑

要求的是最大的表示不出来的价值,那么这个价值加一是能被表示出来的。

如果一个价值 \(n\) 能被表示出来,那么 \(n+b\) 一定可以被表示。

将价值按除以 \(b\) 的余数分成 \(b\) 组,以 \(a=3, \ b=7\) 为例:

image

除了最后一列,每一列最小的可被表示的价值实际上是 \(ka + 0b\) 的形式,由于 \(a\)\(b\) 互质,那么根据裴蜀定理,\(0a \sim (b-1)a\) 一定都覆盖得到,因此这些数中最大的那个就是 \((b-1)a\),而要求的是最大的不能被表示的,再减 \(b\) 即为答案。因此答案实际上就是 \(ab-a-b\)

参考代码
#include <cstdio>
int main()
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%lld\n", 1ll * a * b - a - b);
    return 0;
}

例题:CF1244C The Football Season

在足球联赛中,某个队伍一共参加了 \(n \ (1 \le n \le 10^{12})\) 场比赛,累积得分为 \(p \ (0 \le p \le 10^{17})\) 分。积分规则为:胜一场得 \(w\) 分,平一场得 \(d\) 分,输一场不得分,\(1 \le d \lt w \le 10^5\)。要求求出一组非负整数解 \((x, y, z)\),分别表示胜、平、负的比赛场数。若存在多组合法的非负整数解,输出任意一组即可;若无解,输出 \(-1\)

直接考虑暴力枚举 \(x\)\(y\) 的话,由于 \(n \le 10^{12}\),直接在 \([0,n]\) 范围内枚举显然会引发超时。

注意到题目给出了一个非常关键的约束条件:\(w \gt d \ge 1\),且 \(w \le 10^5\)

考虑同余与等价替换性质,假设存在一组非负整数解 \((x,y,z)\) 满足 \(y \ge w\)。可以将 \(y\) 拆解为 \(y = k \cdot w + y'\),其中 \(k = \lfloor \frac{y}{w} \rfloor \ge 1\)\(0 \le y' \lt w\)

\(y\) 代入得分方程,则有 \(x \cdot w + y \cdot d = x \cdot w + (k \cdot w + y') \cdot d = (x + k \cdot d) \cdot w + y' \cdot d = p\)

定义新的解为 \(x' = x + k \cdot d\) 以及 \(y' = y \bmod w\),比较两次解对应的场数总和 \((x' + y')\)\((x + y)\)\((x' + y') - (x + y) = (x + k \cdot d + y') - (x + k \cdot w + y') = k \cdot (d - w)\)。因为题目保证 \(d \lt w\),即 \(d - w \lt 0\),且 \(k \ge 1\),所以 \(x' + y' \lt x + y \le n\)。这意味着\(w\) 场平局替换为 \(d\) 场胜利,总得分保持不变,但胜与平的总场数 \((x+y)\) 严格减少

由于 \(x'+y' \lt x+y\),新的失败场数 \(z' = n - (x' + y') \gt n - (x + y) = z \ge 0\) 必然依然是非负数。

因此可以得出重要结论:如果方程存在任何非负整数解,则必然存在至少一组满足 \(0 \le y \lt w\) 的合法解

基于上述结论,只需在范围 \(y \in [0,w-1]\) 内进行枚举即可,时间复杂度为 \(O(w)\)

参考代码
#include <iostream>
using namespace std;
using ll = long long;
int main()
{
    ll n, p, w, d;
    cin >> n >> p >> w >> d;
    // 根据数学推导,若有解,必定存在一组解满足 0 <= y < w
    for (ll y = 0; y < w; y++) {
        ll r = p - y * d;
        // 如果剩余分数小于 0,后续 y 更大时更不可能成立,直接中断
        if (r < 0) break;
        // 检查剩余分数能否由整场胜利贡献
        if (r % w == 0) {
            ll x = r / w;
            ll z = n - x - y;
            // 检查失败场数 z 是否非负
            if (z >= 0) {
                cout << x << " " << y << " " << z << "\n";
                return 0;
            }
        }
    }
    // 未找到合法解
    cout << "-1\n";
    return 0;
}

习题:P13029 [GCJ 2021 #1B] Broken Clock

给定一个坏掉的无刻度圆盘时钟,上面有三根指针(时针、分针、秒针),外观完全相同且无法区分。已知条件:

  • 钟表一周被等分为 \(N = 360 \times 12 \times 10^{10} = 43,200,000,000,000\) 个“滴答”。
  • 初始时刻为午夜 \(00:00:00\),所有指针均指向正上方,旋转角为 \(0\)
  • 随时间 \(t\)(纳秒)流逝,指针转动速度分别为:时针每纳秒旋转 \(1\) 滴答,分针每纳秒旋转 \(12\) 滴答,秒针每纳秒旋转 \(720\) 滴答。
  • 整体钟表可能被任意顺时针旋转了角度 \(K\) 滴答。
  • 当前时间严格小于 \(12\) 小时,即 \(0 \le t \lt N\)

给出三根指针当前的读数 \(A,B,C \ (0 \le A \le B \le C \lt N)\)(相对于某个任意轴的角度,以顺时针方向的“滴答”为单位),要求计算出对应的精确时间:小时 \(h\)、分钟 \(m\)、秒 \(s\)、纳秒 \(n\)

解题思路

由于三根指针无法区分,需要通过枚举输入的三角度的全排列,分别假设它们作为时针 \(h'\)、分针 \(m'\)、秒针 \(s'\)

测试集 1

在测试集 1 中,钟表未经过旋转。设经过的时间为 \(t\) 纳秒,则时针的读数直接满足 \(h' \equiv t \pmod N\)。因为有 \(0 \le t \lt N\),所以直接有 \(t = h'\)

同时测试集 1 保证 \(t\) 是整秒,即 \(t \bmod 10^9 = 0\)。因此对于每种指针全排列,直接将假定的时针角度作为候选时间 \(t\),判断其是否满足:

  1. \(t\) 为整秒,即 \(t \bmod 10^9 = 0\)
  2. 分针角度 \(m' \equiv 12t \pmod N\)
  3. 秒针角度 \(s' \equiv 720t \pmod N\)
参考代码 1
#include <iostream>
#include <algorithm>
using namespace std;
using ll = long long;
const ll N = 43200000000000ll;
const ll S = 1000000000;
const ll M = 60000000000ll;
const ll H = 3600000000000ll;
ll a[3];
void print(ll t) {
    cout << (t / H) << " " << (t / M % 60) << " " << (t / S % 60) << " " << t % S << "\n";
}
void solve(int id) {
    cout << "Case #" << id << ": ";
    cin >> a[0] >> a[1] >> a[2];
    do {
        ll t = a[0]; // 无旋转时,假定的时针位置即为时间 t
        if (t % S != 0) continue; // 必须为整秒
        ll m = 12 * t % N, s = 720 * t % N;
        if (m != a[1] || s != a[2]) continue;
        print(t);
        return;
    } while (next_permutation(a, a + 3));
}
int main()
{
    int t; cin >> t;
    for (int i = 1; i <= t; i++) {
        solve(i);
    }
    return 0;
}
测试集 2

在测试集 2 中,表盘存在旋转 \(K\) 滴答,关系变为:

\[\begin{cases} h' \equiv t + K \pmod N \\ m' \equiv 12t + K \pmod N \\ s' \equiv 720t + K \pmod N \end{cases} \]

通过作差消去未知量 \(K\)

\[\begin{cases} m' - h' \equiv 11t \pmod N \\ s' - h' \equiv 719t \pmod N \end{cases} \]

由于 \(t\) 为整秒,而 \(12\) 小时内总秒数只有 \(12 \times 3600 = 43200 \text{ 秒}\)。因此,可以枚举整秒数,计算候选时间,代入同余方程 \((m' - h') \bmod N \overset{?}{=} 11t \bmod N\)\((s' - h') \bmod N \overset{?}{=} 719t \bmod N\) 检验。

参考代码 2
#include <iostream>
#include <algorithm>
using namespace std;
using ll = long long;
const ll N = 43200000000000ll;
const ll S = 1000000000;
const ll M = 60000000000ll;
const ll H = 3600000000000ll;
ll a[3];
void print(ll t) {
    cout << (t / H) << " " << (t / M % 60) << " " << (t / S % 60) << " " << t % S << "\n";
}
ll mod(ll x) {
    return (x % N + N) % N;
}
void solve(int id) {
    cout << "Case #" << id << ": ";
    cin >> a[0] >> a[1] >> a[2];
    do { 
        // 枚举 12 小时内的所有整秒数 (43200 种可能)
        for (ll s = 0; s < 43200; s++) {
            ll t = s * S;
            if (mod(a[1] - a[0]) == mod(11 * t) && mod(a[2] - a[0]) == mod(719 * t)) {
                print(t);
                return;
            }
        }
    } while (next_permutation(a, a + 3));
}
int main()
{
    int t; cin >> t;
    for (int i = 1; i <= t; i++) {
        solve(i);
    }
    return 0;
}
测试集 3

在测试集 3 中,时间 \(t\) 可以是任意整纳秒,可能取值多达 \(4.32 \times 10^{13}\) 种,直接枚举 \(t\) 会超时,需要更优化的代数推导。

设时间 \(t = h \cdot H + n\),其中 \(h \in [0, 11]\) 表示当前小时数(仅 \(12\) 种可能),\(H = 3.6 \times 10^{12}\)\(1\) 小时的纳秒数,\(n \in [0, H)\) 表示当前小时内的纳秒偏移量。

由于一圈总长度 \(N = 12H\),考虑时针与分针的角度差 \(h' - m' \equiv -11t \pmod N\)

\(t = h \cdot H + n\) 代入,\(-11t = -11(h \cdot H + n) = -11h \cdot H - 11n\)

注意到 \(N = 12H\) 意义下,\(-11h \cdot H \equiv 12h \cdot H - 11h \cdot H = h \cdot H \pmod N\)。因此,\(h' - m' \equiv h \cdot H - 11n \pmod N\)。移项整理可得,\(11n \equiv h \cdot H - (h' - m') \pmod N\)

\(d = (h' - m') \bmod N\),并定义 \(x = (h \cdot H - d) \bmod N\),方程简化为 \(11n \equiv x \pmod N\)。因为 \(n \lt H\),则 \(11n \lt 11H \lt N\)。所以在 \([0,N)\) 范围内,不需要考虑模 \(N\) 循环带来的多解,直接判断 \(x\) 是否能被 \(11\) 整除即可。

\(x\) 能被 \(11\) 整除,求出候选纳秒偏移量 \(n = x / 11\)。检查是否满足 \(n \lt H\),若满足,得到候选总时间 \(t = h \cdot H + n\)。代入秒针差值条件 \((s' - h') \bmod N \overset{?}{=} 719t \bmod N\) 进行校验,若通过即为唯一解。

参考代码 3
#include <iostream>
#include <algorithm>
using namespace std;
using ll = long long;
const ll N = 43200000000000ll;
const ll S = 1000000000;
const ll M = 60000000000ll;
const ll H = 3600000000000ll;
ll a[3];
void print(ll t) {
    cout << (t / H) << " " << (t / M % 60) << " " << (t / S % 60) << " " << t % S << "\n";
}
ll mod(ll x) {
    return (x % N + N) % N;
}
void solve(int id) {
    cout << "Case #" << id << ": ";
    cin >> a[0] >> a[1] >> a[2];
    do { 
        // 枚举小时数 h (0 到 11)
        for (int h = 0; h < 12; h++) {
            ll d = mod(a[0] - a[1]); // d = (h' - m') mod N
            ll x = mod(h * H - d); // x = (h*H - d) mod N
            if (x % 11 != 0) continue; // 必须能被 11 整除
            ll n = x / 11;
            if (n >= H) continue; // n 必须小于 1 小时的纳秒数
            ll t = h * H + n;
            // 校验秒针相对时针的角度差
            if (mod(a[2] - a[0]) != mod(719 * t)) continue;
            print(t);
            return;
        }
    } while (next_permutation(a, a + 3));
}
int main()
{
    int t; cin >> t;
    for (int i = 1; i <= t; i++) {
        solve(i);
    }
    return 0;
}

习题:P10217 [省选联考 2024] 季风

给定 \(n, k,x,y\) 以及 \(n\) 天的季风向量 \((x_0,y_0), \dots, (x_{n-1},y_{n-1})\),季风向量以 \(n\) 为周期循环。每一天小 X 可以自行移动一步 \((x_i', y_i')\),满足 \(|x_i'|+|y_i'| \le k\)。求最小的非负整数天数 \(m\),使得小 X 在季风和自身移动的共同作用下,从 \((0,0)\) 到达 \((x,y)\)
即寻找最小的 \(m \ge 0\),使得:

  1. \(\sum\limits_{i=0}^{m-1} (x_i' + x_{i \bmod n}) = x\)
  2. \(\sum\limits_{i=0}^{m-1} (y_i' + y_{i \bmod n}) = y\)
  3. \(\forall 0 \le i \lt m, \ |x_i'|+|y_i'| \le k\)

\(T \ (1 \le T \le 5 \times 10^4)\) 组测试数据,保证 \(\sum n \le 10^6\)\(0 \le |x|,|y|,|x_i|,|y_i|,k \le 10^8\),坐标和 \(k\) 均为整数,移动步长 \(x_i', y_i'\) 为任意实数。

解题思路

\(m=qn+r\),其中 \(0 \le r \lt n\) 为余数,\(q \ge 0\)。前 \(m\) 天季风带来的总位移为 \(W_x(m) = q \sum\limits_{i=0}^{n-1} x_i + \sum\limits_{i=0}^{r-1} x_i\)\(W_y(m) = q \sum\limits_{i=0}^{m-1} y_i + \sum\limits_{i=0}^{r-1} y_i\)

\(S_x, S_y\) 为一个完整周期的季风位移和,\(P_x(r), P_y(r)\) 为前 \(r\) 天的位移前缀和。则小 X 需要通过自身移动覆盖的剩余位移为 \(\Delta x = x - (q S_x + P_x(r))\)\(\Delta y = y - (q S_y + P_y(r))\)

由于每一天小 X 的移动满足 \(|x_i'|+|y_i'| \le k\),且移动步长为实数,那么在 \(m\) 天内,小 X 能够覆盖的总位移 \((\Delta x, \Delta y)\) 当且仅当 \(|\Delta x| + |\Delta y| \le mk\),即 \(|x - P_x(r) - q S_x| + |y - P_y(r) - q S_y| \le (qn+r)k\)

由于 \(n\) 的范围较小,可以枚举 \(m\)\(n\) 的余数 \(r \in [0,n-1]\)。对于固定的 \(r\),设 \(a=x-P_x(r)\)\(b=y-P_y(r)\),需要找到最小的整数 \(q \ge 0\) 使得 \(|a-q S_x| + |b - q S_y| \le q(nk) + rk\)

为了去掉绝对值符号,需要根据 \(a-qS_x\)\(b-qS_y\) 的正负号将 \(q\) 的取值范围划分为若干区间。

对于固定的 \(r\) 和一组符号 \((s_1,s_2)\),其中 \(s_1,s_2 \in \{1,-1\}\)

  1. 确定 \(q\) 的范围:根据 \(s_1(a-qS_x) \ge 0\)\(s_2(b-qS_y) \ge 0\) 确定 \(q\) 的合法区间 \([L,R]\)
  2. 简化不等式\(s_1(a-qS_x)+s_2(b-qS_y) \le qnk+rk\),整理得 \(s_1a+s_2b-rk \le q(s_1S_x+s_2S_y+nk)\)
  3. 求解线性不等式:设 \(D = s_1S_x + s_2S_y + nk\)\(R' = s_1a+s_2b-rk\)
    • \(D \gt 0\)\(q \ge \lceil R'/D \rceil\),取 \(\max(L,\lceil R'/D \rceil)\) 作为最小的 \(q\)
    • \(D=0\):若 \(R' \le 0\),则 \(q=L\) 为最小解;否则该区间无解。
    • \(D \lt 0\)\(q \le \lfloor R'/D \rfloor\),若 \(L \le \lfloor R'/D \rfloor\),则 \(q=L\) 为最小解;否则无解。

对于每组测试数据,需要枚举 \(r \in [0,n-1]\),每个 \(r\) 内部处理 4 种符号模式,总时间复杂度为 \(O \left( \sum n \right)\)

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
const ll INF = 4e18;
ll px[N], py[N];
ll myfloor(ll a, ll b) {
    if (b < 0) {
        a = -a; b = -b;
    }
    if (a >= 0) return a / b;
    return (a - b + 1) / b;
}
ll myceil(ll a, ll b) {
    if (b < 0) {
        a = -a; b = -b;
    }
    if (a >= 0) return (a + b - 1) / b;
    return a / b;
}
// sign*(a-q*s)>=0
bool calc(int sign, ll a, ll s, ll& low, ll& high) {
    if (s == 0) {
        if (sign == 1 && a < 0) return false;
        if (sign == -1 && a > 0) return false;
        return true;
    }
    if (sign == 1) { // a-q*s>=0 -> q*s<=a
        if (s > 0) {
            if (a < 0) return false;
            high = min(high, myfloor(a, s));
        } else { // s<0: q*s<=a -> q>=a/s
            low = max(low, myceil(a, s));
        }
    } else { // a-q*s<=0 -> q*s>=a
        if (s > 0) {
            low = max(low, myceil(a, s));
        } else { // s<0: q*s>=a -> q<=a/s
            high = min(high, myfloor(a, s));
        }
    }
    return true;
}
void solve() {
    int n, k, x, y;
    scanf("%d%d%d%d", &n, &k, &x, &y);
    for (int i = 1; i <= n; i++) {
        int xi, yi; 
        scanf("%d%d", &xi, &yi);
        px[i] = px[i - 1] + xi;
        py[i] = py[i - 1] + yi;
    }
    ll sx = px[n], sy = py[n];
    ll ans = INF;
    for (int r = 0; r < n; r++) {
        ll a = x - px[r], b = y - py[r];
        ll best = INF;
        // |a - q*sx| + |b - q*sy| <= qnk + rk
        for (int s1 : {-1, 1}) {
            for (int s2 : {-1, 1}) {
                ll low = 0, high = INF;
                // s1*(a-q*sx)>=0
                if (!calc(s1, a, sx, low, high)) continue;
                // s2*(b-q*sy)>=0
                if (!calc(s2, b, sy, low, high)) continue;
                if (low > high) continue;
                ll d = s1 * sx + s2 * sy + 1ll * n * k;
                ll r1 = s1 * a + s2 * b - 1ll * r * k;
                if (d > 0) {
                    ll q = myceil(r1, d);
                    if (q > high) continue;
                    if (q < low) q = low;
                    best = min(best, q * n + r);
                } else if (d == 0) {
                    if (r1 <= 0) {
                        best = min(best, low * n + r);
                    }
                } else { // d<0: q<=r1/d
                    if (myfloor(r1, d) < low) continue;
                    best = min(best, low * n + r);
                }
            }
        }
        ans = min(ans, best);
    }
    if (ans == INF) printf("-1\n");
    else printf("%lld\n", ans);
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) solve();
    return 0;
}
posted @ 2026-06-10 17:34  RonChen  阅读(26)  评论(0)    收藏  举报