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\),则有如下两种转移方式:

  1. \(f_n=f_{n-1}+A^n\)
  2. \(f_n=Af_{n-1}+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}\)
  2. \(\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\) 为偶数,则有:

\[\sum\limits_{i=0}^{N-1}A^i=\sum\limits_{i=0}^{\frac{N}{2}-1}A^i+A^{\frac{N}{2}}\sum\limits_{i=0}^{\frac{N}{2}-1}A^i \]

于是转化为规模减半的子问题。对于奇数,预先把 \(A^{N-1}\) 加入答案转化为偶数,或者将答案 \(\times A + 1\) 即可。复杂度 \(O(\log N)\)

还有另一种分治方式。同样假设 \(N\) 为偶数:

\[\begin{aligned} \sum\limits_{i=0}^{N-1}A^i&=\sum\limits_{i=0}^{\frac{N-1}{2}}A^{2i}+\sum\limits_{i=0}^{\frac{N-1}{2}}A^{2i+1}\\ &=\sum\limits_{i=0}^{\frac{N-1}{2}}(A^2)^i+A\sum\limits_{i=0}^{\frac{N-1}{2}}(A^2)^i\\ &=(1+A)\sum\limits_{i=0}^{\frac{N-1}{2}}(A^2)^i \end{aligned} \]

对于奇数同样预先处理一位即可。这种做法的好处是代码较短,因为不需要额外乘一些需要同时维护的系数(如第一种分治的 \(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;
}

以上代码在实现上比较麻烦,以下讲几个实现上的技巧。

  1. 实际上,需要去重只是因为 \(4000\) 的界不够紧,\(4000\) 以内存在 \(5\) 位的情况。只要令该分界线恰好为 \(5,6\) 位的分界线即可。实现中可以枚举 \(b\) 的过程中判断 \(b^5\)\(n\) 的大小,超过则 break 即可。
  2. 二分还原时,一旦超过 \(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;
}
posted @ 2023-03-11 23:55  曹轩鸣  阅读(112)  评论(0编辑  收藏  举报