离散化算法

离散化算法

近期笔者在学习数据结构:哈希表时发现于离散化算法非常像,因此先总结离散化算法之后再总结哈希表

一、核心原理

1.问题背景

当处理的数据**取值范围非常大(>1e9)但实际出现的不重复数据量很少时(<=1e5)**时,直接使用数据作为数组下标会出现以下问题

  • 空间浪费:数组要开1e9大小,超过内存限制(1e9int 需要4GB,内存不足)
  • 无法操作:内存无法分配,导致程序无法进行

2.核心思想

离散化是将大范围、但是稀疏的离散数据映射到小范围且连续的整数索引(如1,2,…m,m为不重复数据个数)的预处理技术。核心思想是压缩坐标,保留数据的相对大小和出现关系,丢弃无用的大范围间隔

3.核心步骤

离散化完整流程可以总结为4步:

  1. 收集数据:收集所有需要用到的关键数据(如插入的坐标,查询的区间边界等)
  2. 排序去重:对数据进行排序,删除其中的重复元素,得到唯一,有序的数据列表
  3. 建立映射:通过二分查找,将每个数据映射到对应的连续索引
  4. 处理任务:用映射后的索引替代原来的下标,进行后续操作。

二、代码实现与解析

以该例题区间和为例讲解代码
题目链接:https://www.acwing.com/problem/content/804/
题目描述
题目描述

1. 初始数据结构定义

const int N = 3e5+10;  // 最大不重复关键字个数(3e5足够应对大部分场景)
typedef pair<int,int> PII;  // 存储插入操作({x, c})和查询操作({l, r})

int n, m;               // n:插入操作数;m:查询操作数
int a[N];               // 离散化后的数组(存储映射后的索引对应的累加值)
vector<PII> add, query;  // add:插入操作列表;query:查询操作列表
vector<int> alls;       // 存储所有需要离散化的关键字

2.收集关键字

收集所有会用到的坐标————插入的x、查询的lr

// 读入插入操作,收集x
for(int i = 0; i < n; i++) 
{
    int x, c;
    scanf("%d%d", &x, &c);
    alls.push_back(x);  // 收集插入坐标x
    add.push_back({x, c});  // 记录插入操作
}

// 读入查询操作,收集l和r
for(int i = 0; i < m; i++) 
{
    int l, r;
    scanf("%d%d", &l, &r);
    query.push_back({l, r});  // 记录查询操作
    alls.push_back(l);        // 收集查询左边界l
    alls.push_back(r);        // 收集查询右边界r
}

收集lr的原因
查询的区间边界lr可能不在插入的x中,但是需要通过离散化映射到索引,才能计算区间和

3.排序去重

alls排序并删除重复元素得到唯一、有序的关键字列表

sort(alls.begin(), alls.end());  // 排序:使关键字有序,为二分查找打基础
alls.erase(unique(alls.begin(), alls.end()), alls.end());  // 去重
  • unique(alls.begin(),alls.end()):将重复的元素移到数组末尾,返回不重复元素的尾部迭代器
  • erase():删除末尾的重复元素,使alls存储所有不重复的关键字(有序)

4.建立映射(find函数)

通过二分查找,将关键字x映射到连续索引(下标从1开始)

int find(int x) 
{
    int l = 0, r = alls.size() - 1;
    while(l < r) 
    {
        int mid = l + r >> 1;  // 等价于 (l+r)/2,避免溢出
        if(alls[mid] >= x) r = mid;  // 找第一个 >= x 的位置
        else l = mid + 1;
    }
    return l + 1;  // 映射为1-based索引(a[0]留作前缀和的哨兵)
}
  • 二分时间复杂度:O(logm)(m为不重复关键字个数,m<=3e5,因此logm约等于19效率极高)
  • 下标从1开始的优势:前缀和计算时s[r]-s[l-1] 直接对应区间[l,r]的和,无需考虑l = 0的边界问题

5.处理任务(插入+查询)

用映射后的索引替代原关键字,进行插入和区间查询工作:

插入:累加值到映射后的索引:
for(int i = 0; i < add.size(); i++) 
{
    int x = add[i].first;    // 原插入坐标
    int c = add[i].second;   // 累加值
    int k = find(x);         // 映射为1-based索引
    a[k] += c;               // 在映射后的索引位置累加c
}
前缀和预处理
for(int i = 1; i <= alls.size(); i++)
{
    a[i] += a[i-1];  // 前缀和数组:a[i] 表示前i个索引的累加和
}
  • 前缀和时间复杂度:O(m)(m为不重复关键字个数)
查询区间和
for(int i = 0; i < query.size(); i++) 
{
    int l = query[i].first;  // 原查询左边界
    int r = query[i].second; // 原查询右边界
    // 映射为1-based索引,计算区间和:a[r映射] - a[l映射-1]
    int k1 = find(l);
    int k2 = find(r);
    printf("%d\n", a[k2] - a[k1-1]);
}

三、关键要点

  1. 映射唯一:排序去重使得每个关键字对应唯一索引,使得映射无歧义
  2. 下标从1开始,方便前缀和计算,避免l = 0a[l-1]越界
  3. 关键字完整性:必须收集所有可能用到的坐标(包括插入的x和查询的lr),否则映射会缺失导致查询错误
  4. 时间复杂度
    • 收集关键字:O(n+m)
    • 排序去重:O(mlogm),(m为关键字总数)
    • 插入+查询:O((n+m)logm)
      整体复杂度O((n+m)logm)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值