首页 > 其他分享 >【进阶】树状数组的高阶应用

【进阶】树状数组的高阶应用

时间:2024-08-04 19:28:53浏览次数:15  
标签:进阶 树状 int 离线 数组 include 高阶 nowr

1. 离线树状数组

介绍

有一类经典问题:给定一个序列,每次询问一个区间内的元素种类数。

这种题的做法有很多:莫队、分块、主席树……在不强制在线的情况下,他们的效率都较低,有一种效率高、空间小的离线做法:离线树状数组

例题:

P1972 [SDOI2009] HH的项链

题目大意

给定一个长度为 \(n\) 的序列 \(a\),\(m\) 次询问,每次询问一个区间 \([l, r]\) 内的元素种类数。

数据范围:\(n, m\le 10^6\)。

思路

\(10 ^ 6\) 的数据范围直接劝退大多数根号数据结构,主席树还能跑,但效率也挺低的,而且空间占用极大,这里就可以用离线树状数组来做。

我们首先可以推出来一个结论:对于若干个右端点都为 \(r\) 的查询区间 \([l, r]\),那么对于多次出现的数我们只关心靠右的那个数,因为只用统计有几个数出现过,反正都被算过一次了,那么靠右位置的完全可以取代左边位置的。

我们先把所有操作离线下来,钦定一个扫描顺序。根据上面的结论,应该把所有操作按照右端点升序排列。

根据上述方式,维护一个数组 \(c\),当我们将 \(1\sim nowr\) 的数加入计算后,在每个数最后出现的位置打上一个 \(1\) 的标记,表示这个位置上出现了一个新的数。

那么对 \(c\) 求一遍前缀和得到 \(sum\),\(sum_i\) 就表示 \(1\sim i\) 中出现了多少个数,对于每个询问 \([l, r]\) 答案就是 \(sum_r - sum_{l - 1}\)。

树状数组刚好能支持单点修改,区间求和,所以直接用树状数组维护数组 \(c\) 即可。

时间复杂度 \(O(n\log n)\)(假定 \(n,m\) 同级)。

\(\texttt{Code:}\)

#include <iostream>
#include <algorithm>

#define lowbit(x) x & -x

using namespace std;

const int N = 1000010;
int n, m;
int a[N], pre[N]; //pre[i] 表示 i 上一次出现的位置

struct BIT{ //封装的树状数组
    int c[N];
    void add(int x, int y) {
        for(; x < N; x += lowbit(x)) c[x] += y;
    }
    int ask(int x) {
        int res = 0;
        for(; x; x -= lowbit(x)) res += c[x];
        return res;
    }
}tr;

struct node{
    int id, l, r;
    bool operator< (const node &o) const {
        return r < o.r;
    }
}q[N];
int ans[N];

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    scanf("%d", &m);
    int l, r;
    for(int i = 1; i <= m; i++) {
        scanf("%d%d", &l, &r);
        q[i] = {i, l, r};
    }
    sort(q + 1, q + m + 1);
    int nowr = 1;
    for(int i = 1; i <= m; i++) {
        while(nowr <= q[i].r) { //一直加数直到达到当前查询的右端点
            if(pre[a[nowr]]) tr.add(pre[a[nowr]], -1);
            tr.add(nowr, 1);
            pre[a[nowr]] = nowr; //nowr 将要右移,当前应成为过去
            nowr++;
        }
        ans[q[i].id] = tr.ask(q[i].r) - tr.ask(q[i].l - 1);
    }
    for(int i = 1; i <= m; i++)
        printf("%d\n", ans[i]);
    return 0;
}

P4113 [HEOI2012] 采花

题目大意:

给定一个长度为 \(n\) 的序列 \(a\),\(m\) 次询问,每次询问一个区间 \([l, r]\) 中出现次数 \(\ge 2\) 的数。

数据范围:\(n, m\le 2\times 10^6\)。

思路

从数据范围可看出本题及其毒瘤,貌似只有离线树状数组和离线权值线段树能过。

和上一道题的大体思路差不多,但由于这次要维护出现次数 \(\ge 2\) 的数,所以需要 \(pre1\) 和 \(pre2\) 两个数组分别储存 \(i\) 上一次出现的位置和上上次出现的位置。

和上一题不同的是,标记应该打在 \(pre1_{a_{nowr}}\) 上,而不是 \(nowr\) 上。因为当一个区间同时包含 \(pre1_{a_{nowr}}\) 和 \(nowr\) 时才能算数。

\(\texttt{Code:}\)

#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 2000010;

int n, vmax, m;
int a[N];
int pre1[N], pre2[N];

struct BIT{
    int c[N];
    #define lowbit(x) x & -x
    inline void add(int x, int y) {
        for(; x <= n; x += lowbit(x)) c[x] += y;
    }
    inline int ask(int x) {
        int res = 0;
        for(; x; x -= lowbit(x)) res += c[x];
        return res;
    }
}tr;

struct node{
    int id, l, r;
    inline bool operator< (const node &o) const {
        return r < o.r;
    }
}q[N];
int ans[N];

int main() {
    scanf("%d%d%d", &n, &vmax, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    int l, r;
    for(int i = 1; i <= m; i++) {
        scanf("%d%d", &l, &r);
        q[i] = {i, l, r};
    }
    sort(q + 1, q + m + 1);
    int nowr = 1;
    for(int i = 1; i <= m; i++) {
        while(nowr <= q[i].r) {
            if(pre2[a[nowr]]) {
                tr.add(pre2[a[nowr]], -1);
                pre2[a[nowr]] = pre1[a[nowr]];
                tr.add(pre1[a[nowr]], 1);
                pre1[a[nowr]] = nowr;
            }
            else if(pre1[a[nowr]]) {
                pre2[a[nowr]] = pre1[a[nowr]];
                tr.add(pre1[a[nowr]], 1);
                pre1[a[nowr]] = nowr;
            }
            else pre1[a[nowr]] = nowr;
            nowr++;
        }
        ans[q[i].id] = tr.ask(q[i].r) - tr.ask(q[i].l - 1);
    }
    for(int i = 1; i <= m; i++)
        printf("%d\n", ans[i]);
    return 0;
}

P10814 【模板】离线二维数点

这里

P2163 [SHOI2007] 园丁的烦恼

这里

标签:进阶,树状,int,离线,数组,include,高阶,nowr
From: https://www.cnblogs.com/Brilliant11001/p/18342111

相关文章

  • 多线程-进阶2
     博主主页: 码农派大星.  数据结构专栏:Java数据结构 数据库专栏:MySQL数据库JavaEE专栏:JavaEE关注博主带你了解更多数据结构知识1.CAS1.1CAS全称:Compareandswap比较内存和cpu寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)......
  • RAPTOR:索引树状RAG和递归推理检索系统
    RAPTOR论文和源码论文:https://arxiv.org/pdf/2401.18059代码:https://github.com/parthsarthi03/raptorRAPTOR(RECURSIVEABSTRACTIVEPROCESSINGFORTREE-ORGANIZEDRETRIEVAL),如这一论文标题中所述,这一方法是把文档按照树状结构进行索引组织,再使用递归推理来进行检索。RAP......
  • 数组案例练习进阶版---查找数组中的元素
    今天,我们来做一个进阶版的练习,输入一个数字,来判断他在数组中是否存在:这样的话,首先我们就需要有一个能帮助我们输入的工具,那么在Java中它长成什么样子呢?首先我们必须在主方法的第一行写上这样一串代码:Scannerinput=newScanner(System.in); 这样我们就创建了一个输入......
  • 算法进阶指南第一题 a^b
    【模板】快速幂题目描述给你三个整数a,b,pa,b,p......
  • 【Redis 进阶】哨兵 Sentinel(重点理解流程和原理)
    Redis的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工进行主从切换,同时大量的客户端需要被通知切换到新的主节点上,对于上了一定规模的应用来说,这种方案是无法接受的,于是Redis从2.8开始提供了RedisSentinel(哨兵)加个来解决这个问题。一、基本概念由于对Red......
  • MySQL进阶(查询、备份与恢复)
    一、多表联合查询在关系型数据库中,表与表之间是有联系的,所以在实际应用中,经常使用多表查询。多表查询就是同时查询两个或两个以上的表。在MySQL中,多表查询主要有交叉连接、内连接、外连接、分组查询与子查询等5种。1、交叉连接(CROSSJOIN)笛卡尔积交叉连接(CROSSJO......
  • MySQL数据分析进阶(八)存储过程
    ※食用指南:文章内容为‘CodeWithMosh’SQL进阶教程系列学习笔记,笔记整理比较粗糙,主要目的自存为主,记录完整的学习过程。(图片超级多,慎看!)【中字】SQL进阶教程|史上最易懂SQL教程!10小时零基础成长SQL大师!!https://www.bilibili.com/video/BV1UE41147KC/?spm_id_from=333.1007.0.......
  • 神经网络训练(二):基于残差连接的图片分类网络(进阶篇②)
    目录日常·唠嗑3基于ResNet18的优化3.1初步构思3.1.1数据预处理3.1.2批量大小3.1.3参数初始化3.1.4optimizer3.1.5学习速率3.2hyper-parameter测试3.2.1批量大小日常·唠嗑       昨天写完了神经网络训练(二):基于残差连接的图片分类......
  • 2024年暑假关于线段树和树状数组的小知识点
    1.线段树的树形结构使得存储其的数组应开4N,其中N为元素个数2.多用宏定义使代码更简单3.树状数组求逆序对一般会写成add(a[i],1);quiry(a[i]-1);这会导致当元素值域包含0时传入-1导致死循环,可以在quiry函数判断合法性;一种比较好的写法是干脆add时add(a[i]+1,1),然后直接查......
  • 谷粒商城实战笔记-115-全文检索-ElasticSearch-进阶-bool复合查询
    文章目录1,must2,mustnot3,should1,must{"query":{"bool":{"must":[{"match":{"gender":"M"}},{"matc......