题意
给出一个运算,将一个正整数,运算后它的值变成它正因数的数量,比如10可以被1,2,5,10整除,那么10经过运算后就会变成4。
现在有一个长度为n的数组a,a中的每个数都不超过1e6,有m个问询,每个问询给出三个变量:t,l,r,当t = 1时,对区间[l, r]的所有数进行上述运算;当t = 2时,输出[l, r]区间内所有数的和。
思路
运算是不能够通过延时标记保存在大区间,进而递归到小区间的,这意味着每一个的区间修改都是由若干个单点修改组成。这时候我们就考虑到,这个运算会不会是数量比较小的,因为直觉上来讲,一个正整数的正因数并不会太多。于是使用类Eratosthenes筛法,计算出小于1e6所有正整数经过运算后的数字,即它们正因数的数量(操作方式见find_val()函数),这个操作的复杂度大概是maxlogmax。然后检查一下每个数最大运算的次数,发现不大于6次。这样我们就可以每次单点修改,维护区间最大值,最大值不大于2的区间我们就可以略过不修改,于是最多修改6*n次,每次logn的时间完成修改。
使用线段树,维护区间之和和区间最大值,实现上述操作即可。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int max_a = 1e6 + 5;
const int max_n = 3e5 + 10;
int divi[max_a];
struct st {
int l, r;
ll sum;
int mx;
}t[max_n << 2];
int n, m;
void build(int id, int l, int r) {
t[id].l = l, t[id].r = r;
if (l == r) {
int x; scanf("%d", &x);
t[id].sum = t[id].mx = x;
return;
}
int mid = (l + r) >> 1;
build(id * 2, l, mid);
build(id * 2 + 1, mid + 1, r);
t[id].sum = t[id * 2].sum + t[id * 2 + 1].sum;
t[id].mx = max(t[id * 2].mx, t[id * 2 + 1].mx);
}
ll get_sum(int id, int l, int r) {
if (l == t[id].l && r == t[id].r) return t[id].sum;
int mid = (t[id].l + t[id].r) >> 1;
if (r <= mid) {
return get_sum(id * 2, l, r);
}
else if (l > mid) {
return get_sum(id * 2 + 1, l, r);
}
else {
return get_sum(id * 2, l, mid) + get_sum(id * 2 + 1, mid + 1, r);
}
}
void change(int id, int l, int r) {
// printf("id:%d l:%d r:%d\n", id, l, r);
if (t[id].l == t[id].r) {
t[id].mx = t[id].sum = divi[t[id].sum];
return ;
}
if (t[id].mx <= 2)
return ;
int mid = (t[id].l + t[id].r) >> 1;
if (r <= mid)
change(id * 2, l, r);
else if (l > mid)
change(id * 2 + 1, l, r);
else {
change(id * 2, l, mid);
change(id * 2 + 1, mid + 1, r);
}
t[id].sum = t[id * 2].sum + t[id * 2 + 1].sum;
t[id].mx = max(t[id * 2].mx, t[id * 2 + 1].mx);
}
void find_val() {
for (register int i = 1; i < max_a; i++) {
for (register int j = i; j < max_a; j += i) {
divi[j] ++;
}
}
}
int main() {
find_val();
scanf("%d %d", &n, &m);
build(1, 1, n);
while(m--) {
int t, l, r;
scanf("%d %d %d", &t, &l, &r);
if (t == 1) change(1, l, r); //t = 1是更新
if (t == 2) printf("%I64d\n", get_sum(1, l, r)); //t = 2是访问
}
return 0;
}
总结
因为要复习线段树,畏难心理导致我拖延了很久才补掉这个题。但是其实真正通过看书复习了问询、单点修改这些基本操作之后,再根据思路,大概1个小时就敲出来了,实际code的时间也就十几分钟吧,时间主要花在改自己线段树实现上的bug。
然后顺便看了下延迟更新的操作,也不难,毕竟去年这个时候学的东西。所以特别是自己已经学过的算法,其实是上手很快的,即使现在忘得一干二净,看看书也是会比较快想起来的。下次不要太害怕接触不会敲的算法,因为这么看来,理论上即使比赛上现学也有做出来的机会。