【例 3】校门外的树(信息学奥赛一本通- P1537)

【题目描述】

原题来自:Vijos P1448

校门外有很多树,学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两种操作:

K=1,读入 l,r 表示在 l 到 r 之间种上一种树,每次操作种的树的种类都不同;

K=2,读入 l,r 表示询问 l 到 r 之间有多少种树。

注意:每个位置都可以重复种树。

【输入】

第一行 n,m 表示道路总长为 n,共有 m 个操作;

接下来 m 行为 m 个操作。

【输出】

对于每个 k=2 输出一个答案。

【输入样例】

复制代码
5 4
1 1 3
2 2 5
1 2 4
2 3 5

【输出样例】

复制代码
1
2

【提示】

数据范围与提示:

对于 20% 的数据,1≤n,m≤100;

对于 %60% 的数据,1≤n≤10^3,1≤m≤5×10^4 ;

对于 %100% 的数据,1≤n,m≤5×10^4 ,保证 l,r>0。

一、 题目分析

【题目模型】 道路总长为N,共有M个操作交替进行:

  • 操作 1: 在区间[l,r]种上一种全新的树(每次种类都不同)。

  • 操作 2: 查询区间[L,R]内,一共包含了多少树?

【核心:种类 = 线段相交】 题目故意用"种类"这个词来迷惑同学们。因为规定了"每次操作种的树种类都不同",所以每一次种树操作,在几何意义上就是画出了一条独一无二的线段 。 而"查询区间内有多少种树",本质上就是在问:在此之前画过的所有线段中,有多少条线段与当前的查询区间 [L,R] 发生了重叠(相交)?

二、 思考过程:正难则反的容斥原理

如果直接去数"有多少条线段和[L,R]相交",情况会极其复杂:线段可能被区间包含、可能跨越整个区间、也可能只在左边或右边蹭到一个点。

破局点:与其找"相交",不如找"绝对不可能相交"。

我们站在查询区间 [L,R] 的视角,所有的历史线段只可能分为三类:

  1. A 类(全在左边,错过了): 线段的右端点r<L(即 r≤L−1)。

  2. C 类(全在右边,错过了): 线段的左端点l>R。

  3. B 类(发生相交,命中了): 排除掉A和C,剩下的就是我们要找的答案。

推导数学公式: 我们不难发现:如果一条线段的左端点≤R ,那么它绝对不可能是C类树。所以,这批线段里只包含了A类和B类。 接着,我们只要把这批线段里的A类(即右端点 ≤L−1 的线段)剔除出去,剩下的就百分之百是B类(相交的线段)了。

得出容斥公式:

相交种类数=(左端点≤R 的总数)−(右端点≤L−1 的总数)

三、 算法设计:双树状数组

既然我们需要频繁地求"小于等于某个坐标的端点总数",这不就是标准的频次桶的前缀和问题吗?我们只需要建立两个树状数组:

  1. cl(起点桶): 维护每个位置作为"左端点"出现的频次。

  2. cr(终点桶): 维护每个位置作为"右端点"出现的频次。

操作流转:

  • 当种树[l,r]时:cl的位置l加上 1,给cr的位置r加上 1。(两个单点修改)

  • 当查询 [L,R] 时: 答案=query(cl,R)-query(cr,L-1)。(两个前缀和相减)

没有任何复杂的区间修改,没有任何Lazy Tag,仅仅靠着两个最简单的单点修改树状数组,就破解了这道思维难题。

四、 时空复杂度分析

  • 时间复杂度: 每次操作只需要对树状数组进行两次 O(logn) 的单点修改或前缀查询。总共 M 次操作,总体时间复杂度为O(MlogN)。在5×10^4 的数据规模下,运算量不到百万,极速 AC。

  • 空间复杂度: 仅开辟了两个长度为N的树状数组,空间复杂度O(N)。

五、 易错点总结

  1. 在写update函数时,我们经常用一个变量x传入目标坐标,但在内部的for(int i=x; i <=n; i+=lowbit(i)) 循环中,真正需要被累加更新的是游标i,千万不能写成对固定坐标x累加。 (即 c[i]+=val,绝不能是c[x]+=val)。

  2. 数据量达到数万级别,main 函数的第一行必须焊死 ios::sync_with_stdio(false); cin.tie(0);,否则容易被卡常数超时。

  3. 涉及树状数组的前缀累加,统一使用long long声明。

六、 完整代码

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long ll;
int n,m;
ll cl[500010];//cl[i]表示"以位置i为左端点"的区间数量前缀和树状数组
ll cr[500010];//cr[i]表示"以位置i为右端点"的区间数量前缀和树状数组

//返回x二进制表示下最低位1所代表的整数
int lowbit(int x){
    return x&(-x);
}

//树状数组更新 
void update(int id,int x,int val){
    if(id==1){//修改左端点数组
        for(int i=x;i<=n;i+=lowbit(i)){
            cl[i]+=val;
        }
    }
    else{//修改右端点数组
        for(int i=x;i<=n;i+=lowbit(i)){
            cr[i]+=val;
        }
    }
}

//查询 树状数组求前缀和(求指定频次桶的前x项总和)
ll query(int id,int x){
    ll ret=0;
    if(id==1){
        while(x){
            ret+=cl[x];
            x-=lowbit(x);//顺着层级向左上跳跃,拼接相邻区间
        }
    }
    else{
        while(x){
            ret+=cr[x];
            x-=lowbit(x);
        }
    }
    return ret;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    //总共执行m次操作
    while(m--){
        int k,l,r;
        cin>>k>>l>>r;
        if(k==1){//在l到r之间种上一种树
        //一条线段产生,其左端点为l,右端点为r
            update(1,l,1);
            update(2,r,1);
        }
        else{//询问l到r之间有多少种树
    //答案=(左端点在r及左侧的总数)-(右端点在l-1及左侧的总数)
            cout<<query(1,r)-query(2,l-1)<<"\n";
        }
    }
    return 0;
}
相关推荐
guguhaohao2 小时前
平衡二叉树(AVL),咕咕咕!
数据结构·c++·算法
一叶落4382 小时前
LeetCode 137. 只出现一次的数字 II —— 位运算解法
c语言·数据结构·算法·leetcode·哈希算法
阿豪只会阿巴2 小时前
咱这后续安排
c++·人工智能·算法·leetcode·ros2
像素猎人2 小时前
以数据结构之——树来体会深度优先搜索【dfs】和广度优先搜索【bfs】的妙用:学比特算法课的自用笔记
数据结构·c++·学习·dfs·bfs·深度优先搜索
逆境不可逃2 小时前
LeetCode 热题 100 之 215. 数组中的第K个最大元素 347. 前 K 个高频元素 295. 数据流的中位数
算法·leetcode·职场和发展
凤年徐2 小时前
优选算法——滑动窗口
c++·算法
DDzqss2 小时前
3.14打卡day35
算法
WHS-_-20222 小时前
mCore: Achieving Sub-millisecond Scheduling for 5G MU-MIMO Systems
java·算法·5g
浅念-2 小时前
C++11 核心知识点整理
开发语言·数据结构·c++·笔记·算法