题解:P4145 上帝造题的七分钟 2 / 花神游历各国

珂朵莉最可爱了!

更好的阅读体验

简化题意

有一个有 \(n\) 个数的数,要进行 \(m\) 此操作,操作有两种:

  1. 区间开根;
  2. 区间求和。

前置知识

珂朵莉树(比下面更详细)树状数组区间修改查询(本文中未嵌套)

建树操作 —— 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\) 分裂成两个节点并返回分裂后靠右节点的指针,如下:

flowchart LR A(l, r, val) --> D[(split x)] D --> B(l, x - 1, val) D --> C(x, r, val)

具体过程为先二分得出需要分裂的节点,然后删除要分裂的节点,插入分裂后的两个节点。

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\) 合并左边,反之,如图:

flowchart LR A(l1, r1, val) --> D[(merge it)] C(l2, r2, val) --> D D --> B(l1, r2, val)
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\),如下:

flowchart LR a(l, r, val) --> D[(assign L, R, Val)] b(……) --> D c(l, r, val) --> D g(l, r, val) --> D e(……) --> D f(l, r, val) --> D D --> h(l, r, val) D --> i(……) D --> j(l, L - 1, val) D --> k(L, R, Val) D --> l(R + 1, r, val) D --> m(……) D --> n(l, r)

具体过程为在 \(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 了,如图:

image-c14b9afe1a4319b7522ff6f53b318d33.md.png

题目思路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;
}

结后语

珂朵莉树跑的比线段树和分块都快。

posted @ 2025-08-15 18:16  _Charllote  阅读(34)  评论(0)    收藏  举报