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)+1∣x∈N} 的元素个数。
数据范围: 1 ≤ n ≤ 1 0 9 1 \le n\le 10^9 1≤n≤109。
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)+1∣x∈N} 的元素和为 f k f_k fk,求有多少种不同的 f k f_k fk,升序输出。
数据范围: 2 ≤ n ≤ 1 0 9 2 \le n \le 10^9 2≤n≤109。
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(nlogn)。
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+k⋅dx)modn,(y+k⋅dy)modn)∣k∈N} 中特殊点的数量最大。
数据范围: 1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1≤n≤106, 1 ≤ m ≤ 1 0 5 1 \le m \le 10^5 1≤m≤105, 1 ≤ d x , d y ≤ n 1 \le dx,dy \le n 1≤dx,dy≤n, 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+k⋅dxmodn} 和 { y + k ⋅ d y m o d n } \left\{ y + k \cdot dy \mod n \right\} {y+k⋅dymodn} 均通过 { 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+k⋅dy≡yimodn 反推出 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,⋯,an−1,统计有多少个以 l l l 开始,周期为 s s s 的后缀满足 0 ≤ l < n , 1 ≤ s < n 0 \le l <n,1 \le s < n 0≤l<n,1≤s<n 且对于任意的 k ∈ Z k \in \textbf{Z} k∈Z,都有 a k ≥ a l + k a_k \ge a_{l+k} ak≥al+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 1≤n≤2⋅105,1≤ai≤106。
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} k∈Z,都有 a k ≥ a l + k a_k \ge a_{l+k} ak≥al+k。
利用同余的性质,这个条件可以转化为对于任意的 l ′ ≥ l l'\ge l l′≥l,都满足
a l ′ ≥ a l ′ + k ⋅ gcd ( s , n ) ( mod n ) a_l' \ge a_{l' + k\cdot \gcd(s,n) (\text{mod }n)} al′≥al′+k⋅gcd(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\} al′≥max{al′+k⋅gcd(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′+k⋅gcd(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′+k⋅gcd(s,n)(mod n)}。
根据上面的推导,结合题中条件,会发现对于以 l l l 开始,周期为 s s s 的后缀,只有满足下面条件,才是一个合法的后缀:
对于任意的 l ′ ∈ [ l , l + s − 1 ] l' \in [l, l + s - 1] l′∈[l,l+s−1],都有
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,n−1] 遍历 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(n⋅d(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 n−1 的置换 a [ 0.. n − 1 ] a[0..n-1] a[0..n−1], b [ 0.. n − 1 ] b[0..n-1] b[0..n−1], c [ 0.. n − 1 ] c[0..n-1] c[0..n−1] 使得任意的 i ∈ [ 0 , n − 1 ] i \in [0,n-1] i∈[0,n−1] 都满足 a i + b i ≡ c i ( mod n ) a_i + b_i \equiv c_i (\text{mod } n) ai+bi≡ci(mod n),或者确定解不存在。
数据范围: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1≤n≤105。
5.2 解题过程
a i + b i ≡ c i ( mod n ) a_i + b_i \equiv c_i (\text{mod } n) ai+bi≡ci(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=1∏nai(mod n)}
为一个
0
0
0 到
n
−
1
n-1
n−1 的置换,或者确定解不存在。
数据范围: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1≤n≤105。
6.2 解题过程
首先, a [ n ] a[n] a[n] 必须为 n n n,否则会使生成的序列出现多个后缀 0 0 0。
因此,我们只需要考虑 a [ 1.. n − 1 ] a[1..n-1] a[1..n−1],设 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]。
分几种情况:
-
n
n
n 为质数时,
[
1
,
n
−
1
]
[1,n-1]
[1,n−1] 均可求出逆元,所以可以如此构造
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=1∏ia[i]=prefix[i−1]⋅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[i−1])
注意到 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] 唯一。
因此按照上述方式构造即可。
- n = 1 n=1 n=1 时,答案就是 1 1 1。
-
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) (n−1)!=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 x1≤x≤x2 且 y 1 ≤ y ≤ y 2 y_1 \le y \le y_2 y1≤y≤y2 的整数解数量。
题目保证所有数字的绝对值不超过 1 0 8 10^8 108。
7.2 解题过程
如果不考虑边界情况的话,实际上就是要先解出同余方程 a x ≡ c ( mod b ) ax \equiv c(\text{mod } b) ax≡c(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)b⋅tyt=y0−gcd(a,b)a⋅t
之后将
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 错误点
- 没有特判 a = 0 a=0 a=0 或 b = 0 b=0 b=0 的情况。
- 处理左边界时需要向上取整,处理右边界时需要想下去中。
- 向上取整和向下取整都需要分正负两种情况讨论。
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=(xi⋅hi,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 2≤m≤106,0≤hi,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=ti′−ti。
之后只要求解同余方程:
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,证明这个值不存在。
- t 1 = t 2 > 0 t_1=t_2>0 t1=t2>0 时,如果直接解同余方程的话,答案一定是非法的 0 0 0。而此情况下,答案就是 t 1 t_1 t1。
- 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 的基础上的。
- 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,因此输出无解。
- 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∣(t1−t2) 时才有解,此时的解为 t 1 t_1 t1。
- T 1 ≠ 0 T_1 \ne 0 T1=0 且 T 2 = 0 T_2 = 0 T2=0 与 4 同理。
- 根据自己的解同余方程的写法,规定
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 t1≥t2,那就等着 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∣(t2−t1),那样的话只靠 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∣(t2−t1) 的情况下多一份的 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 i−1 个观察员之后 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 1≤T≤109,2≤n le2⋅105,1≤ai≤109。
9.2 解题过程
自己的第二道 2900 2900 2900 的题目了,真的太难补了!
首先,我们可以重新表述这个问题:有 T T T 个石子,从 [ 0 , T − 1 ] [0,T-1] [0,T−1] 编号,第一个人第一次取 0 0 0 号石子,之后第 i i i 个人会取第 i − 1 i-1 i−1 个人之后的第 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=1∑naiT[1]=0T[i]=j=2∑iai
其中,
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]+k⋅S(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(1≤v≤m),找到一个下标 j ≤ 1 0 100 j \le 10^{100} j≤10100 且序列 { 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 1≤n,m≤105,1≤Ti≤40,∑i=1nTi≤2⋅105。
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,Ti−1],则对于任意的
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+ki⋅Ti=pj+kj⋅Tj
这个方程有解的充要条件为
gcd
(
T
i
,
T
j
)
∣
(
p
i
−
p
j
)
\gcd(T_i, T_j) | (p_i-p_j)
gcd(Ti,Tj)∣(pi−pj)
如果我们暴力求解的话,很容易
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}
1≤Ti≤40
因此我们可以利用这个条件,存储每一种
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(40⋅∑i=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 1≤M,D,L,R≤2⋅109。
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)\\ L≤Dx−My≤RDx−R≤My≤Dx−L−R(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 L≤Dx≤R,证明在一圈 x ∈ [ 0 , M − 1 ] x \in [0,M-1] x∈[0,M−1] 之内跳 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 ⌊DR⌋⋅D≥L,所以我们使用这个条件判断即可。
注意利用下界 L L L 求解时,需要使用以 L − 1 L-1 L−1 为上界时的解再加上 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 错误点
- 求解
−
x
m
o
d
d
-x \mod d
−xmodd 时,应写成
(-x) % d
,而不可以写成 − x -x % d −x。 - 最初要将
R
R
R 对
M
−
1
M-1
M−1 取 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 a⋅k≤x≤b⋅k。
题目保证 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 ak≤x≤bk 区间中至少含一个整数。
如果 a ′ ≤ 0 a' \le 0 a′≤0 或者 b ′ ≤ 1 b' \le 1 b′≤1,则答案就是 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}
ak≤x≤bka≤xk≤bb1≤xk≤a1bx≤k≤ax
之后,我们会发现得到了一个问题规模更小的相同问题,因此可以递归求解。
递归时,第一层(即在 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(1≤a,b,c,d≤109),找到一个分数 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(1≤T≤104) 组数据。
15.2 解题过程
本题需要分类讨论:
-
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>p⋅cd
显然,此时的答案为 p = 1 , q = ⌊ d c ⌋ + 1 p=1,q=\lfloor \frac{d}{c} \rfloor + 1 p=1,q=⌊cd⌋+1。 -
a
≥
b
a \ge b
a≥b 时,可以将左右边界同时向左移动到
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<qp−⌊ba⌋<dc−⌊ba⌋
之后再将 p p p 加上 q ⋅ ⌊ a b ⌋ q \cdot \lfloor \frac{a}{b} \rfloor q⋅⌊ba⌋ 即可得到答案,因此递归求解即可。 - a < b a < b a<b 且 c > d c > d c>d 时,答案显然,我们只需让 p q = 1 \frac{p}{q}=1 qp=1,即可得到最优解。
- 其他情况,证明左右边界都不超过
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(T⋅logn)。
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;
}