ABC293解题报告
E. Geometric Progression
题意:求 \(\sum\limits_{i=0}^{N-1}A^i\pmod M\)。\(A,M\le 10^9,N\le 10^{12}\),不保证质数/互质。
做法一
直接算不好算,但我们可以写出一个递推的形式:设 \(f_n=\sum\limits_{i=0}^{n}A^i\),则有如下两种转移方式:
- \(f_n=f_{n-1}+A^n\)
- \(f_n=Af_{n-1}+1\)
可以通过矩阵乘法来加速这一过程:
- \(\begin{bmatrix}f_n\\A^n\end{bmatrix}=\begin{bmatrix}1&A\\0&A\end{bmatrix}\times\begin{bmatrix}f_{n-1}\\A^{n-1}\end{bmatrix}\)
- \(\begin{bmatrix}f_n\\1\end{bmatrix}=\begin{bmatrix}A&1\\0&1\end{bmatrix}\times\begin{bmatrix}f_{n-1}\\1\end{bmatrix}\)
使用矩阵快速幂即可将复杂度做到 \(O(\log N)\)。
By tokusakurai
int main() {
ll A, X, M;
cin >> A >> X >> M;
mint::set_mod(M);
using mat = Matrix<mint>;
mat a(2, 2);
a[0][0] = A;
a[0][1] = 1, a[1][1] = 1;
mat x(1, 2);
x[0][0] = 1;
x *= a.pow(X);
cout << x[0][1] << '\n';
}
做法二
考虑用分治来推式子。假设 \(N\) 为偶数,则有:
于是转化为规模减半的子问题。对于奇数,预先把 \(A^{N-1}\) 加入答案转化为偶数,或者将答案 \(\times A + 1\) 即可。复杂度 \(O(\log N)\)。
还有另一种分治方式。同样假设 \(N\) 为偶数:
对于奇数同样预先处理一位即可。这种做法的好处是代码较短,因为不需要额外乘一些需要同时维护的系数(如第一种分治的 \(A^{\frac{N}{2}}\))。
By Nachia
i64 powm(i64 a, i64 x, i64 m){
if(x == 1) return 1 % m;
i64 p = (a + 1) % m;
i64 q = powm(a * a % m, x/2, m);
q = q * p % m;
if(x % 2 == 1) q = (q * a + 1) % m;
return q;
}
int main(){
i64 A, X, M; cin >> A >> X >> M;
cout << powm(A, X, M) << endl;
return 0;
}
做法三
可以根号分块。如果 \(N\) 为完全平方数,可以首先 \(O(\sqrt{N})\) 计算出 \(S=\sum\limits_{i=0}^{\sqrt{N}-1}A^i\),答案即为 \(\sum\limits_{i=0}^{\sqrt{N}-1}A^{i\sqrt{N}}S\)。
如果 \(N\) 不是完全平方数,此时只求出了 \([0\sim \lfloor\sqrt{N}\rfloor^2)\) 的和,显然剩下的零散部分大小在 \(\sqrt{N}\) 级别,直接计算即可。复杂度 \(O(\sqrt{N})\)。
int A,X,M;
signed main(void) {
//freopen("m.in","r",stdin);
//freopen("m.out","w",stdout);
A=read();X=read()-1;M=read();
int as=1,ans=0;
for(int i=0;i<1000000;i++) {
ans=(ans+as)%M;
as=as*A%M;
}
int st=0,at=1,aq=0;
while(st+1000000-1<=X) {
aq=(aq+ans)%M;
ans=ans*as%M;
at=at*as%M;
st+=1000000;
}
while(st<=X) {
aq=(aq+at)%M;
at=at*A%M;
st++;
}
printf("%lld",aq);
return 0;
}
做法四
显然可以使用等比数列求和公式,得到答案为 \(\frac{A^N-1}{A-1}\)。但是关键在于 \(M\) 不是质数,无法直接求逆元。如果我们直接算 \(\frac{(A^N-1)\bmod M}{A-1}\),却又不一定是整数。
对于这种情况,有一个技巧:由于最终答案显然为整数,即 \(A^N-1\) 为 \(A-1\) 的倍数,所以只要令分子的模数也改为 \(M(A-1)\) 即可。答案即为 \(\frac{(A^N-1)\bmod(M(A-1))}{A-1}\bmod M\)。容易发现,此时分子取模后的结果仍然为 \(A-1\) 的倍数,所以可以无需逆元直接除。注意此时模数达到 long long
级别,运算需要开 __int128
。
在前十名中,大部分使用的是分治做法,其次是矩阵。Rank1 maspy 使用了此技巧,且用了 Python,则可以免去 __int128
等细节,代码极短。
By maspy
A, X, M = map(int, input().split())
if A == 1:
print(X % M)
else:
mod = M * (A - 1)
x = pow(A, X, mod) - 1
x //= (A - 1)
print(x % M)
F. Zero or One
题意:输入 \(n\),判断有多少种进制 \(b\),满足 \(n\) 在 \(b\) 进制下的表示只有 0/1。\(n\le 10^{18}\)。
合法的 \(b\) 最多可以到 \(n\),所以暴力枚举显然不行。但是可以发现,当 \(b\) 大一些的时候,位数会极少。例如,如果 \(b>4000\),则位数不会超过五位。
于是,我们先枚举 \(4000\) 以下的每个 \(b\),检查是否合法。对于剩下的,位数最多五位,而要求每一位为 0/1,所以“表示”最多只有 \(2^5\) 种。对于每一种表示,显然最多只有一个进制满足条件,检查是否存在即可。这里可以使用二分答案,二分每种进制,检查过大还是过小。
一开始想的是,算出 \(n\) 在该进制下的表示,与当前枚举到的 5 位的表示进行比较,但比较难写,于是可以反向考虑:算出当前枚举到的表示,在该进制下的数 \(n'\),如果 \(n'>n\) 则说明进制过大,\(n'<n\) 则说明进制过小,等于则找到答案。
需要注意的是,由于进制本身已到 long long
级别,在还原表示时还要取幂,所以一定会炸,需要一旦超过 \(n\) 就退出循环。还有,这两部分的答案可能有重复(\(4000\) 一下也可能存在 \(5\) 位的解),需要去重。
By cxm1024
#define int __int128
void Solve(int test) {
int n = read();
set<int> s;
for (int i = 2; i <= 4000; i++) {
int x = n;
bool flag = 1;
while (x) {
if (x % i > 1) flag = 0;
x /= i;
}
if (flag) s.insert(i);
}
for (int i = 1; i < (1 << 5); i++) {
int l = 2, r = n;
while (l <= r) {
int mid = (l + r) / 2;
int x = 0, now = 1;
for (int j = 0; j < 5; j++, now *= mid) {
if ((i >> j) == 0) break;
if (now > n && (i >> j)) {x = n + 1; break;}
if (i & (1 << j)) {
x += now;
if (x > n) break;
}
}
if (x > n) r = mid - 1;
else if (x < n) l = mid + 1;
else {
s.insert(mid);
break;
}
}
}
cout << s.size() << endl;
}
以上代码在实现上比较麻烦,以下讲几个实现上的技巧。
- 实际上,需要去重只是因为 \(4000\) 的界不够紧,\(4000\) 以内存在 \(5\) 位的情况。只要令该分界线恰好为 \(5,6\) 位的分界线即可。实现中可以枚举 \(b\) 的过程中判断 \(b^5\) 与 \(n\) 的大小,超过则
break
即可。 - 二分还原时,一旦超过 \(n\) 就
break
的操作较为繁琐,有一些细节,所以可以直接每一步与 \(n+1\) 取 \(\min\) 来免去这一操作。
By maspy
void solve() {
LL(N);
ll ANS = 0;
// 5 乗を使う
FOR(B, 2, N + 1) {
if (B * B * B * B * B > N) break;
vi F;
ll x = N;
while (x) {
F.eb(x % B);
x /= B;
}
if (MAX(F) <= 1) ++ANS;
}
// 4 乗以下だけを使う
FOR(s, 2, 1 << 5) {
auto f = [&](i128 B) -> i128 {
i128 pow = 1;
i128 val = 0;
FOR(i, 5) {
if (s >> i & 1) { val += pow; }
pow *= B;
chmin(pow, N + 1);
}
chmin(val, N + 1);
return val;
};
ll B = binary_search([&](ll B) -> bool { return f(B) <= N; }, 0, N + 1);
if (1 < B && f(B) == N) { ++ANS; }
}
print(ANS);
}
G. Triple Index
题意:有一个长度为 \(n\) 的数组,每次询问 \([l,r]\) 内 \((i,j,k)\) 的个数,使 \(i<j<k,a_i=a_j=a_k\)。\(n,q\le 2\times 10^5\)。
很难直接处理,于是离线,于是莫队板子。
By cxm1024
#include <bits/stdc++.h>
using namespace std;
#define int long long
int a[200010], t[200010], len = 450;
struct node {
int l, r, num;
} ask[200010];
int ans[200010];
signed main() {
int n, q;
scanf("%lld%lld", &n, &q);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for (int i = 1; i <= q; i++) {
scanf("%lld%lld", &ask[i].l, &ask[i].r);
ask[i].num = i;
}
sort(ask + 1, ask + q + 1, [&](node x, node y) {
if (x.l / len != y.l / len)
return x.l / len < y.l / len;
if (x.l / len % 2 == 0) return x.r < y.r;
else return x.r > y.r;
});
int l = 1, r = 0, now = 0;
auto add = [&](int x) {
t[x]++;
if (t[x] >= 3) now += (t[x] - 1) * (t[x] - 2) / 2;
};
auto del = [&](int x) {
t[x]--;
if (t[x] >= 2) now -= t[x] * (t[x] - 1) / 2;
};
for (auto [ll, rr, num] : ask) {
while (r < rr) add(a[++r]);
while (l > ll) add(a[--l]);
while (r > rr) del(a[r--]);
while (l < ll) del(a[l++]);
ans[num] = now;
}
for (int i = 1; i <= q; i++)
printf("%ld\n", ans[i]);
return 0;
}