专栏文章

离散化

个人记录参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@minyu7t3
此快照首次捕获于
2025/12/02 10:35
3 个月前
此快照最后确认于
2025/12/02 10:35
3 个月前
查看原文

离散化

蒟蒻笔记 - 1

介绍

离散\LARGE 离散,顾名思义是非连续的,如果拿数据结构做解释的话,离散可以映射为所有非连续的线性表,比如链表。
那么离散化是什么,我们为什么要离散化?
假如现在给你一堆数字,它们是无序的,可能相同的,且极差(最大值最小值最大值-最小值)很大,我们需要统计每个数出现的次数。
我们不妨先从小数据分析:
nn 为数组长度,aia_i 为数组元素。
  1. ai105a_i \le 10^5 时,我们可以开一个桶,这个问题就变成了计数排序
    CPP
    int const N = 1e5 + 7;
    int a[N], t[N];
    for(int i = 1; i <= n; i++){
    	cin >> a[i];
        t[a[i]]++;
    }
    
    这种做法虽然简单易写,但是处理不了大数据下的统计;
    那肯定有同学想到用哈希的做法,或者 STL 的 map 和 unordered_map;
  2. ai1010,n105a_i \le 10^{10},\, n \le 10^5 时,显然桶是不行了,因为元素很大,但是哈希可以啊:
    CPP
    unordered_map<int, int> hs;
    vector<int> v;
    for(int i = 1; i <= n; i++){
    	int a;
        cin >> a;
        hs[a] ++;
        v.push_back(a);
    }
    
    STL 提供的 unordered_map 为我们提供了 Hash表,简单且便于操作,可总是依赖 STL 不行,毕竟 它们的常数时间都很大,更不用说 list 了。
  3. ai1010,n105a_i \le 10^{10},\, n \le 10^5 时,除了哈希,我们还能用一种优化操作,离散化:
    建立一个离散数组,我们需要这些步骤
    1. 收集所有需要离散化的值
      我们随意列些值;
      CPP
      a[10] = {10, 7000, 42, 233, 233, 114514, 1919810, 1, -1000, -2718}
      
      这个数组里的元素,符合无序的,可能相同的,且极差很大
      显然我们不能拿原数组进行离散化,因为还比较混乱;
    2. 排序
      将原数组排序结果复制在 bb 数组;
      CPP
      sort(a, a + 10);
      
      这时的数组 bb 变为,aa 数组不变:
      CPP
      [-2718, -1000, 1, 10, 42, 233, 233, 7000, 114514, 1919810];
      
      这时我们便可以看出数组的单调性。
    3. 去重
      离散数组不能有重复的元素,它更像 集合(set),
      这时的数组 bb :
      CPP
      [-2718, -1000, 1, 10, 42, 233, 7000, 114514, 1919810];
      
    4. 建立映射关系(从原始值到离散化后的索引)
      这里就是离散化最重要的一步,映射
      CPP
      int t[N];
      for(int i = 0; i < n; i++) // pos为去重后数组的长度
      	t[i] = lower_bound(b, b + pos, a[i]) - b;
      

实践

让我们来看一下整个流程和STL模版下的代码:
CPP
原始数据: 10 7000 42 233 233 114514 1919810 1 -1000 -2718 

排序去重后: -2718 -1000 1 10 42 233 7000 114514 1919810 

离散化结果: 3 6 4 5 5 7 8 2 1 0 

映射关系:
原始值: -2718   -> 离散索引: 0
原始值: -1000   -> 离散索引: 1
原始值: 1       -> 离散索引: 2
原始值: 10      -> 离散索引: 3
原始值: 42      -> 离散索引: 4
原始值: 233     -> 离散索引: 5
原始值: 7000    -> 离散索引: 6
原始值: 114514  -> 离散索引: 7
原始值: 1919810 -> 离散索引: 8
代码:
CPP
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int a[N], b[N], t[N]; // a:原始数组 b:排序数组 t:离散化结果
int n;                // 数据量

void discretize() {
    // 1. 复制到排序数组
    for(int i = 0; i < n; i++) b[i] = a[i];
    
    // 2. 排序
    sort(b, b + n);
    
    // 3. 去重 (返回去重后的尾地址)
    int pos = unique(b, b + n) - b;
    
    // 4. 建立映射
    for(int i = 0; i < n; i++) {
        t[i] = lower_bound(b, b + pos, a[i]) - b;
    }
    
    /* 输出示例
    printf("映射关系:\n");
    for(int i = 0; i < pos; i++) {
        printf("原始值 %d -> 离散值 %d\n", b[i], i);
    }
    */
}
因为离散过程中排序为主,所以时间复杂度为:O(nlogn)\large O(nlogn)

适用场景对比

方法时间复杂度空间复杂度适用场景
桶排序O(n)O(max-min)值域小
哈希表O(n)平均O(n)需快速查找
离散化O(n log n)O(n)大值域+范围查询

例题

洛谷 P1496 火烧赤壁

这道题是很经典的区间查询问题,注意它的范围 231a<b<2311n2×104−2^{31}≤a<b<2^{31}\, 且 \,1≤n≤2×10^4
元素很大,个数很小
思路:将元素进行离散化,也就是区间的左右端点,放在数组 ee 中,首先收集所有区间的端点坐标,排序并去重;
建立离散化映射。然后使用差分数组标记每个离散化后的区间是否被覆盖:遇到区间起点+1,终点-1。
最后扫描所有离散化后的区间,通过前缀和计算当前覆盖次数,统计被覆盖区间的实际长度之和。这种方法将问题转化为离散点处理,避免了直接处理大范围坐标,时间复杂度为O(nlogn)\large O(n log n),完美适用于 n2×104n≤2×10⁴ 的数据规模

洛谷 P1955 程序自动分析

并查集 + 离散化
如果不知道并查集是什么,可以留意我的笔记,稍后我会出一篇

评论

0 条评论,欢迎与作者交流。

正在加载评论...