【题解】LibreOJ #6281. 数列分块入门 5
题目链接:LibreOJ #6281. 数列分块入门 5
题面
题目描述
给定一个长为 \(n\) 的数列 \(a_1\dots a_n\),以及 \(n\) 个操作,操作涉及区间开方,区间求和。
输入格式
第一行输入一个数字 \(n\)。
第二行输入 \(n\) 个数字,第 \(i\) 个数字为 \(a_i\),以空格隔开。
接下来输入 \(n\) 行询问,每行输入四个数字 \(\rm{opt},l,r,c\),以空格隔开。
若 \(\rm opt=0\),表示将位于 \(\left[l, r\right]\) 的之间的数字都开方。对于区间中每个 \(a_i\left(l\leq i\leq r\right)\longleftarrow \lfloor \sqrt a_i \rfloor\)
若 \(\rm opt=1\),表示询问位于 \(\left[l, r\right]\) 的所有数字的和。
输出格式
对于每次询问,输出一行一个数字表示答案。
样例(很水)
Input
4
1 2 2 3
0 1 3 1
1 1 4 4
0 1 2 2
1 1 2 4
Output
6
2
数据范围与提示
对于 \(100\%\) 的数据,\(1\leq n\leq 50000,-2^{31}\leq {\rm others}\)、\({\rm ans}\leq 2^{31}-1\)。
写博客时的吐槽
\(\rm \LaTeX\)是真nm难打啊。。
思路
审题
与平时最简单的数列分块入门的区别在于,这个数列分块入门五并不是加上or减去一个数,而是开方。这两者的区别在于,加上或者减去一个数与这个数本身无关,可以直接相加减,可以直接做记号表示这个块加上或减去了某个数。
加上或减去某个数时,在完整块中,这一个块每一个数增加或减少的值都相同,就可以很容易地给这一整个块做标记。
但是做开方运算时(其实乘方、乘除都很坑),每一个块增加或者减小的值不相等,就导致了不能把这一整个块增加或减小的值做一个标记,这就相当于把分块废掉了。
解法
这是一道题目,总不可能没有解法吧。
我们用到了下面这个\(\rm sol()\)函数来解决这个问题。
void sol(int x) {
if (flag[x]) return; //如果这个块被标记了,就废了,直接返回,节省时间
//标记原则后面会讲
flag[x] = true; //先把这个块标记
sum[x] = 0; //将sum[]数组初始化为零
for (int i = (x - 1) * len + 1; i <= x * len; i++) { //循环块内每一个值
a[i] = sqrt(a[i]), sum[x] += a[i]; //对块内每一个数都做开方运算,再把他们的和存进sum[]数组里面
if (a[i] > 1) flag[x] = false; //如果这一个值还大于1,也就是还能够进行开方,这个块就还能继续开放,把flag[]标记去除
}
return;
}
从上面的函数中可以看出,我们把已经不能再开方的块进行跳过,大大节约了程序开方的时间。这就是代码的关键。
剩下的地方就和普通的数列分块差不多了。
代码
#include //getchar()和putchar()要用
#include //取块大小和开方时需要用到sqrt()函数
int b[50005], a[50005], sum[255];
bool flag[255];
int len;
int read() {//快速读入,因为LibreOJ默认吸氧,所以没有了前面的inline
int X = 0; bool flag = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') flag = 0; ch = getchar();}
while (ch >= '0' && ch <= '9') {X = (X << 1) + (X << 3) + ch - '0'; ch = getchar();}
if (flag) return X;
return ~ (X - 1);
}
void write(int X) {//快速输出,同上
if (X < 0) {putchar('-'); X = ~ (X - 1);}
int s[50], top = 0;
while (X) {s[++top] = X % 10; X /= 10;}
if (!top) s[++top] = 0;
while (top) putchar(s[top--] + '0');
putchar('\n');
return;
}
void sol(int x) {
if (flag[x]) return; //如果这个块被标记了,就废了,直接返回,节省时间
flag[x] = true; //先把这个块标记
sum[x] = 0; //将sum[]数组初始化为零
for (int i = (x - 1) * len + 1; i <= x * len; i++) { //循环块内每一个值
a[i] = sqrt(a[i]), sum[x] += a[i]; //对块内每一个数都做开方运算,再把他们的和存进sum[]数组里面
if (a[i] > 1) flag[x] = false; //如果这一个值还大于1,也就是还能够进行开方,这个块就还能继续开放,把flag[]标记去除
}
return;
}
void add(int l, int r) {
for (int i = l; i <= (b[l] * len < r ? b[l] * len : r); i++) { //从左往右暴力循环左侧不完整块
sum[b[l]] -= a[i];
a[i] = sqrt(a[i]);
sum[b[l]] += a[i];
}
if (b[l] != b[r]) //如果l和r不在同一块内
for (int i = r; b[i] == b[r]; i--) { //同右往左暴力循环右侧不完整块
//循环条件这里我原来一直写i == b[r]就错了,要注意,下面query()函数也是
sum[b[r]] -= a[i];
a[i] = sqrt(a[i]);
sum[b[r]] += a[i];
}
for (int i = b[l] + 1; i <= b[r] - 1; i++) sol(i); //中间完整块使用sol()函数解决
return;
}
int query(int l, int r) { //查询函数,与普通分块差不多
int ans = 0;
for (int i = l; i <= (b[l] * len < r ? b[l] * len : r); i++) ans += a[i];//左侧
if (b[l] != b[r]) for (int i = r; b[i] == b[r]; i--) ans += a[i];//右侧
for (int i = b[l] + 1; i <= b[r] - 1; i++) ans += sum[i];//中间
return ans;
}
int main() {
int n;
n = read();
len = sqrt(n);
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) {
b[i] = (i - 1) / len + 1;
sum[b[i]] += a[i];
}
for (int i = 1; i <= n; i++) {
bool opt; int l, r;
opt = read(); l = read(); r = read(); read();
opt ? write(query(l, r)) : add(l, r);
//三目运算符,如果opt==1就add(),else就query()
}
return 0;
}