0%

codeforces 2094H 题解

前言

之前听朋友说cry出题很抽象,一直没留意,直到今天做了这个题,迷迷糊糊看题解发现一头雾水,发现出题人是cry……

(不过感觉题目还是挺不错的,只是题解我看不懂)


题意

有一个程序:

1
2
3
4
5
6
7
function f(k, a, l, r):
ans := 0
for i from l to r (inclusive):
while k is divisible by a[i]:
k := k/a[i]
ans := ans + k
return ans

对于q个询问,求出f(k,a,l,r)的值

题解

题目理解

首先读给出的程序代码(应该是伪代码),了解一下大致流程,大概是说对于数组a,枚举从l到r的每个i,用ai更新k的值,并将ans的值加k。具体来说,当ai为k的因数时,k会不断的自除以ai,直到ai不再为k的因数。

比较重要的应该就是更新k的部分。显然只有ai为k的因数时k才会被更新,所以当ai不能整除k时,ans在原来值基础上直接+k即可。

实现

一个简单的思路是我们对于每次查询,将i从l到r枚举,不断暴力的更新k,但这个做法显然复杂度过高。

注意到q的范围为5e4,区间[l, r]长度最大为1e5,显然我们不能枚举到区间中的所有值。

那么我们需要注意哪些值呢?

注意到只有k的因数可以更新k,我们可以预处理出k的所有因数,并预处理出这些因数f所有出现的位置,存储在数组fac[f]中。显然对于k的某个因数f, fac[f]为一个单调递增的数组,那么我们可以通过二分查找第一个不小于l的索引,并判断该索引存在且不大于r,我们就可以用这个索引的元素更新到k。

但还有一点需要注意的就是k更新的顺序是由数组顺序决定的,而不是因数大小决定的。所以我们可以用一个小根堆维护k所有因数的合法索引,并用数组中的相应元素更新k。

对于这些因数索引以外的位置,记prel为上次的索引,top为当前取出的索引,有(top - prel - 1)长度的一段元素没有更新k,他们对ans的贡献为这个长度乘以prel时更新得到的k。

最后用最后的索引和r之间的一段元素用相同方式更新ans即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 1e5 + 7;
int n, qq;
int a[N];

vector<int> facs[N];
vector fac(N, vector<int>());
void solve() {
cin >> n >> qq;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
fac[a[i]].push_back(i);
}

int k, l, r;
for(int i = 1; i <= qq; ++i) {
cin >> k >> l >> r;
priority_queue<int, vector<int>, greater<>> q;
for(int f : facs[k]) {
auto pos = lower_bound(fac[f].begin(), fac[f].end(), l);
if(pos != fac[f].end() and *pos <= r) q.push(*pos);
}

int ans = 0, prel = l - 1;
while(k > 1 && q.size()) {
int top = q.top(); q.pop();
ans += (top - prel - 1) * k;
while(k % a[top] == 0) k /= a[top];
ans += k;
prel = top;
}
ans += k * (r - prel);

cout << ans << '\n';
}

for(int i = 1; i <= n; ++i) fac[a[i]].clear();
}

main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);

for(int i = 1; i < N; ++i) {
for(int j = i; j < N; j += i) {
facs[j].push_back(i);
}
}

int t; cin >> t;
while(t--) {
solve();
}

return 0;
}

一些细节

西安站前的一场vp(好像就是2024年的西安邀请赛)去学到了调和级数算时间复杂度,可以注意到这里的预处理正好时间复杂度是O(NlogN)的。每个k的因数数量大概是sqrtk级别的,所以solve函数主循环的复杂度大概为$O(q\sqrt{k}\log{n})$(做题之前压根猜不到的复杂度)

我很可爱,请给我钱qwq