同余分析
同余类分析法 的基本思想是:利用同余关系将全体整数(或问题的状态空间)划分为若干个互不相交的等价类。
- 分类标准:选定一个基准数 \(M\),根据每个数 \(x\) 满足 \(x \equiv r \pmod{M}\) 的余数 \(r\) 进行归类。
- 分析逻辑:如果一个性质对于某个余数 \(r\) 成立,那么通过给 \(x\) 不断加减 \(M\)(即在同一个同余类内平移),往往能推导出该类中所有(或大部分)元素的性质。
例题:P3951 [NOIP 2017 提高组] 小凯的疑惑
要求的是最大的表示不出来的价值,那么这个价值加一是能被表示出来的。
如果一个价值 \(n\) 能被表示出来,那么 \(n+b\) 一定可以被表示。
将价值按除以 \(b\) 的余数分成 \(b\) 组,以 \(a=3, \ b=7\) 为例:

除了最后一列,每一列最小的可被表示的价值实际上是 \(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\),判断其是否满足:
- \(t\) 为整秒,即 \(t \bmod 10^9 = 0\)。
- 分针角度 \(m' \equiv 12t \pmod N\)。
- 秒针角度 \(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\) 滴答,关系变为:
通过作差消去未知量 \(K\):
由于 \(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\),使得:
- \(\sum\limits_{i=0}^{m-1} (x_i' + x_{i \bmod n}) = x\)
- \(\sum\limits_{i=0}^{m-1} (y_i' + y_{i \bmod n}) = y\)
- \(\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\}\)。
- 确定 \(q\) 的范围:根据 \(s_1(a-qS_x) \ge 0\) 和 \(s_2(b-qS_y) \ge 0\) 确定 \(q\) 的合法区间 \([L,R]\)。
- 简化不等式:\(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)\)。
- 求解线性不等式:设 \(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;
}

浙公网安备 33010602011771号