题解:P4145 上帝造题的七分钟 2 / 花神游历各国
珂朵莉最可爱了!
简化题意
有一个有 \(n\) 个数的数,要进行 \(m\) 此操作,操作有两种:
- 区间开根;
- 区间求和。
前置知识
珂朵莉树(比下面更详细)、树状数组区间修改查询(本文中未嵌套)。
建树操作 —— void build(int a[], int n)
void build(int a[], int n) 表示建一棵表示 \(a\) 数组的树。具体为找出一段连续的值用一个节点表示。
void build(int a[], int n) {
int x = a[1], pos = 1;
// x 为值,pos 为一段连续的值的左端点
for (int i = 2; i <= n; i++) {
if (a[i] != x) {
t.insert({pos, i - 1, x});
pos = i; x = a[i];
// 找到不同的位置,插入节点
}
}
t.insert({pos, n, a[n]});
// 插入剩下的元素
}
节点分裂操作 —— set<ODT_node>::iterator split(int x)
set<ODT_node>::iterator split(int x) 表示把一个包含 \(x\) 的节点按 \(x\) 分裂成两个节点并返回分裂后靠右节点的指针,如下:
具体过程为先二分得出需要分裂的节点,然后删除要分裂的节点,插入分裂后的两个节点。
set<ODT_node>::iterator split(int x) {
auto it = t.lower_bound({x, 0, 0});
// 找出要分裂的节点的下一个节点
if (it != t.end() && it->l == x) return it;
if (it == t.begin()) return t.end();
// 若找不到要分裂的节点返回尾指针,防止越界
it -- ;
// 找出要分裂的节点
int l = it->l, r = it->r, val = it->val;
// 记录信息,防止之后的操作改变指针
t.erase(it); t.insert({l, x - 1, val});
return t.insert({x, r, val}).first;
// 删除要分裂的节点,插入分裂后的两个节点
}
区间合并操作 —— set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir)
set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) 表示把一 \(it\) 左右两边的与它相同的区间合并并返回合并后的指针,\(dir = 1\) 合并左边,反之,如图:
set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) {
if (it == t.end()) return t.end();
// 如果 it 指向的是最后一个节点,则不需要合并
// 合并左边
if (dir) {
if (it == t.begin()) return t.end();
// 如果 it 指向的是第一个节点,则不需要合并
auto prev_it = prev(it);
// 左边的指针
if (prev_it->val == it->val) {
int l = prev_it->l, r = it->r, val = it->val;
// 预先记录信息
it = t.erase(prev_it);
if (it != t.end()) t.erase(it);
// 删除原节点
return t.insert({l, r, val}).first;
// 添加合并后的节点
}
return it;
}
// 合并右边(同上)
auto next_it = next(it);
if (next_it == t.end()) return t.end();
if (next_it->val == it->val) {
int l = it->l, r = next_it->r, val = it->val;
t.erase(it);
if (next_it != t.end()) t.erase(next_it);
return t.insert({l, r, val}).first;
}
return it;
}
区间推平操作 —— void assign(int l, int r, int val)
void assign(int L, int R, int Val) 表示把 \(L\) 到 \(R\) 区间内的数赋值为 \(Val\),如下:
具体过程为在 \(L\) 和 \(R + 1\) 处分裂,删除中间的一段被赋值的区间的原节点,最后重新插入被赋值的区间。
void assign(int l, int r, int val) {
auto ir = split(r + 1), il = split(l);
// 分裂操作端点(必须先分裂 r 再分裂 l 不然迭代器会失效)
t.erase(il, ir);
// 删除中间的一段被赋值的区间的原节点
t.insert({l, r, val});
// 重新插入被赋值的区间
it = merge(it, 1); merge(it, 0);
// 合并端点
}
其他的区间修改操作 —— void fun(int l, int r, ……)
珂朵莉树修改操作都可以按一下模板操作(类比分块)。
在 \(L\) 和 \(R + 1\) 处分裂,修改中间的一段被操作的区间的节点。
void fun(int l, int r, ……) {
auto ir = split(r + 1), il = split(l);
// 分裂操作端点
for (auto it = il; it != ir; it ++ )
// 对中间的节点操作
merge(ir, 1); merge(il, 0);
// 合并端点
}
区间查询操作 —— int fun(int l, int r)
珂朵莉树查询操作都可以按一下模板操作(y也类比分块)。
在 \(L\) 和 \(R + 1\) 处分裂,计算中间的一段被查询的区间的贡献。
int fun(int l, int r) {
auto ir = split(r + 1), il = split(l);
// 分裂操作端点
for (auto it = il; it != ir; it ++ )
// 对中间的节点贡献
merge(ir, 1); merge(il, 0);
// 合并端点
}
题目思路1
看到数据范围:“数列中的数大于 \(0\),且不超过 \(10^{12}\)”,可以发现 \(\left \lfloor \sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{10^{12}}}}}}} \right \rfloor =1\) 即进行 \(6\) 此开根操作一定会把数字变成 \(1\),又因为 \(\sqrt{1}=1\) 所以经过若干次操作后就会出现有一大堆零的子序列。 珂朵莉树可以合并相同的数字于是使用珂朵莉树。细节见代码。
code1
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N];
struct ODT_node {
int l, r;
mutable int val;
bool operator < (const ODT_node x) const {return l < x.l;}
};
struct ODT {
set<ODT_node> t;
void build(int a[], int n) {
int x = a[1], pos = 1;
for (int i = 2; i <= n; i++) {
if (a[i] != x) {
t.insert({pos, i - 1, x});
pos = i; x = a[i];
}
}
t.insert({pos, n, a[n]});
}
set<ODT_node>::iterator split(int x) {
auto it = t.lower_bound({x, 0, 0});
if (it != t.end() && it->l == x) return it;
it--;
int l = it->l, r = it->r, val = it->val;
t.erase(it);
t.insert({l, x - 1, val});
return t.insert({x, r, val}).first;
}
set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) {
if (it == t.end()) return t.end();
if (dir) {
if (it == t.begin()) return t.end();
auto prev_it = prev(it);
if (prev_it->val == it->val) {
int l = prev_it->l, r = it->r, val = it->val;
it = t.erase(prev_it);
if (it != t.end()) t.erase(it);
return t.insert({l, r, val}).first;
}
return it;
}
auto next_it = next(it);
if (next_it == t.end()) return t.end();
if (next_it->val == it->val) {
int l = it->l, r = next_it->r, val = it->val;
t.erase(it);
if (next_it != t.end()) t.erase(next_it);
return t.insert({l, r, val}).first;
}
return it;
}
int ask_sum(int l, int r) {
auto ir = split(r + 1), il = split(l);
int res = 0;
for (auto it = il; it != ir; it++) {
res += (it->r - it->l + 1) * it->val;
}
merge(il, 1); merge(ir, 0);
return res;
}
// 以上代码解释见博客
void sqrt(int l, int r) {
auto ir = split(r + 1), il = split(l);
for (auto it = il; it != ir; it++) {
it->val = (int)std::sqrt(it->val);
}
// 套用区间操作模板
for (auto it = il; it != ir && it != t.end(); ) {
auto temp = it;
it = merge(it, 1);
if (it == temp) it ++ ;
}
// 合并相同的区间(因为有很多 1 的区间所以数据不随机也可以跑的飞快)
}
} tree;
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
tree.build(a, n);
cin >> m;
for (int i = 1, op, x, y; i <= m; i++) {
cin >> op >> x >> y;
if (x > y) swap(x, y);
if (op == 0) tree.sqrt(x, y);
else cout << tree.ask_sum(x, y) << '\n';
}
return 0;
}
当你高高兴兴的提交代码后就会发现被 hack 了,如图:
题目思路2
考虑为什么 TLE。我们构造一组数据,输入序列为 \(1\sim n\),操作每次都为 \(1\sim n\) 查询区间和,珂朵莉树直接退化成 \(\Theta(n^2)\)。所以我们考虑优化区间求和。什么东西可以区间修改区间求和?线段树?不太麻烦了,可以使用树状数组。
我们在建树的时候也初始化树状数组,区间操作的时候更新树状数组,查询时直接使用树状数组的区间求和操作即可。
code2
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N];
struct ODT_node {
int l, r;
mutable int val;
bool operator < (const ODT_node x) const { return l < x.l; }
};
struct BIT {
int t1[N], t2[N];
inline void _in_add(int x, int val) {
for (int i = x; i <= n; i += i & -i) {
t1[i] += val;
t2[i] += x * val;
}
}
inline void add(int l, int r, int val) {
_in_add(l, val);
_in_add(r + 1, -val);
}
inline void add(int x, int val) {
add(x, x, val);
}
inline int _in_ask(int x) {
int res = 0;
for (int i = x; i > 0; i -= i & -i) {
res += (x + 1) * t1[i] - t2[i];
}
return res;
}
inline int ask(int l, int r) {
return _in_ask(r) - _in_ask(l - 1);
}
inline int ask(int x) {
return ask(x, x);
}
};
// 代码解释见博客
struct ODT {
set<ODT_node> t;
BIT ad;
void build(int a[], int n) {
t.clear();
if (n == 0) return;
int x = a[1], pos = 1;
for (int i = 2; i <= n; i++) {
if (a[i] != x) {
// 先添加当前区间到 BIT
ad.add(pos, i - 1, x);
t.insert({pos, i - 1, x});
pos = i;
x = a[i];
}
}
ad.add(pos, n, a[n]);
t.insert({pos, n, a[n]});
}
set<ODT_node>::iterator split(int x) {
auto it = t.lower_bound({x, 0, 0});
if (it != t.end() && it->l == x) return it;
if (it == t.begin()) return t.end();
--it;
if (it->r < x) return t.end();
int l = it->l, r = it->r, val = it->val;
t.erase(it);
t.insert({l, x - 1, val});
return t.insert({x, r, val}).first;
}
set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) {
if (it == t.end()) return t.end();
if (dir) {
if (it == t.begin()) return t.end();
auto prev_it = prev(it);
if (prev_it->val == it->val) {
int l = prev_it->l, r = it->r, val = it->val;
it = t.erase(prev_it);
if (it != t.end()) t.erase(it);
return t.insert({l, r, val}).first;
}
return it;
}
auto next_it = next(it);
if (next_it == t.end()) return t.end();
if (next_it->val == it->val) {
int l = it->l, r = next_it->r, val = it->val;
t.erase(it);
if (next_it != t.end()) t.erase(next_it);
return t.insert({l, r, val}).first;
}
return it;
}
int ask_sum(int l, int r) {
// 直接查询返回
return ad.ask(l, r);
}
void sqrt(int l, int r) {
auto ir = split(r + 1), il = split(l);
for (auto it = il; it != ir; ++it) {
// 先减去旧值,再更新新值并添加
ad.add(it->l, it->r, -it->val);
it->val = (int)std::sqrt(it->val);
ad.add(it->l, it->r, it->val);
}
for (auto it = il; it != ir && it != t.end(); ) {
auto temp = it;
it = merge(it, 1);
if (it == temp) it ++ ;
}
}
} tree;
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
tree.build(a, n);
cin >> m;
while (m--) {
int op, x, y;
cin >> op >> x >> y;
if (x > y) swap(x, y);
if (op == 0) {
tree.sqrt(x, y);
} else {
cout << tree.ask_sum(x, y) << '\n';
}
}
return 0;
}
结后语
珂朵莉树跑的比线段树和分块都快。

浙公网安备 33010602011771号