• 「Wallace 笔记」使用 bitset 求解较高维偏序问题


    求解五维偏序

    给定 (n(le 3 imes 10^4)) 个五元组,对于每个五元组 ((a_i, b_i, c_i, d_i, e_i)),求存在多少个 (1le jle n) 满足 (a_i > a_j)(b_i > b_j)(c_i > c_j)(d_i > d_j)(e_i > e_j)。保证每一维都是 (1cdots n) 的排列。

    第一感觉

    传统的做法有 cdq 分治或 树套树,但是在本题中复杂度会高达 (O(nlog^4 n)),更何况这些做法需要嵌套,代码难度极大。

    如果用 K-D tree 则只能有 (O(n^{frac{2k - 1}{k}}))优秀 效率。

    于是这里介绍一种使用 bitset 的简单做法。


    用 01矩阵 表示大小关系

    我们先对于五元组的 (a) 维构造出一个 (n imes n) 的 01 方阵 (A)

    对于 (A)(i) 行第 (j) 列的元素 (A_{i, j}),若为 1 则表示 (a_i > a_j), 反之则表示 (a_i le a_j)

    换言之,方阵 (A) 存储着 (n) 个五元组在 (a) 维上的大小关系。

    同理有矩阵 (B, C, D, E)

    接下来我们考虑 (S = A And B And C And D And E)(And) 表示按位与)的实际意义。

    显然 (S_{i, j}) 就表示 第 (i) 个和第 (j) 个五元组在所有五维意义上的偏序关系。

    要求第 (i) 个的答案,只要求 (S_{i, 1} sim S_{i, n}) 间中 1 的个数即可。


    关于 bitset

    如何构造这个矩阵?这就需要强大的 bitset 了!

    首先我们来了解一下 bitset。这是 C++ 中的一种 STL。它类似于 bool 数组,每个位置只有两种值:0 或 1。

    bitset 的实现方式是压位,那么一个大小为 (n) 的 bitset 的空间复杂度为 (O(frac{1}{omega} n))。其中 (omega = 32)(64)(系统位数)。

    一些基本操作:

    bitset<N> f; // 定义一个大小为 N 的 bitset,下标范围为 [0, N)
    f.set(i); // 在下标 i 处置为 1
    f.reset(i); // 在下标 i 处置为 0 
    f.test(i); // 判断下标 i 处是否为 1
    f[i]; // 在下标 i 处取值 
    

    除了构造函数,其他操作的复杂度均为 (O(1))

    但还有功能更强大的:

    f.set(); // 全部置为 1
    f.reset(); // 全部置为 0 
    f = g; // bitset 赋值 
    f &= g; // 将 f 对 g 做按位与操作 
    f |= g; // 将 f 对 g 做按位或操作 
    f ^= g; // 将 f 对 g 做按位异或操作
    // 以及各种位运算操作 
    f.count(); // 计算 bitset 中 1 的个数
    

    这些操作都是 (O(frac{1}{omega} n)) 的时间复杂度。

    bitset 的优秀之处,就在于时空复杂度中 (dfrac{1}{omega}) 的优秀常数。


    关系矩阵的构造

    下面讲一讲 (S) 矩阵的构造算法。

    我们对于每一维,将五元组升序排序,然后用一个中间变量 tmp 表示当前维度下,满足当前范围的点的点集(tmp 就是一个 bitset)。

    当做到第 (i) 个五元组时,我们先在第 (i) 个五元组所对应的 bitset f[point[i].index]tmp 做按位与操作。

    然后在 tmp 的当前五元组的编号处置为 1。

    每一维都这样做下去即可。

    最后使用 count 函数统计答案即可。

    核心代码

    /*
     * Author : _Wallace_
     * Source : https://www.cnblogs.com/-Wallace-/
     * Problem : bitset 求解较高维偏序
     */
    for (register int k = 0; k < K; k++) {
    	cmp::set(k); // cmp 函数设置维度 
    	sort(point, point + n, cmp::f); // 排序 
    	
    	tmp.reset(); // 清空 tmp 
    	for (register int i = 0; i < n; i++) {
    		if (!k) f[point[i].index] = tmp; // 第一维特殊处理——直接赋值 
    		else f[point[i].index] &= tmp; // 按位与操作 
    		tmp.set(point[i].index); // 在当前五元组编号处置 1 
    	}
    }
    
    for (register int i = 0; i < n; i++)
    	cout << f[i].count() << endl; // 统计答案 
    

    不难发现上面的时空复杂度都是 (O(frac{1}{omega} n^2)) 的。虽说也是平方级别的算法,但由于 bitset 的优秀常数,在实际中运行效率很不错。

    习题

    HihoCoder #1513 小Hi的烦恼:https://hihocoder.com/problemset/problem/1513

    更高维的偏序问题

    给定 (n(le 5 imes 10^4))(k(le 7)) 元组,对于每个五元组 (T_i = (v_1, v_2, cdots, v_k)_i),求存在多少个 (1le jle n) 满足 (i succ j)。保证每一维都是 (1cdots n) 的排列。

    空间限制:64 MB

    此题开大的 (n) 的范围,维数,并加大了对空间的要求。

    很显然 (O(frac{1}{omega} n^2)) 的空间复杂度以及远远无法符合要求了。

    注意这里优化的是空间,时间复杂度不变。

    分块优化空间

    我们对于每一维进行值域上的分块,块长 (b = lceilsqrt{n} ceil)

    我们定义: bitset<N> dat[K][T]; dat[k][i] 表示在第 (k) 维,处于 (i) 块的值域范围内(即值域 (in [1, i imes b]) 的点的集合。

    这个可以在 (O(n^{1.5}k)) 的时间预处理。

    那么如何通过这个信息获取关于 (T_i) 的信息呢?

    仍然是分块的经典思想——整块取现成,散块暴力直接干。

    具体的,对于第 (k) 维为 (v) 的情况:

    • 整块:直接取出块 ([1, lfloorfrac{v}{b} floor]) 的信息(dat[k][p];)。
      - 时间复杂度:(O(frac{1}{omega} n))
    • 散块:暴力扫出值域在 ((lfloorfrac{v}{b} floor imes b + 1, v]) 的编号。
      - 时间复杂度:(O(sqrt{n}))

    最后对所有维度的 bitset 做按位与,使用 count 函数求解答案。这里时间复杂度为 (O(frac{1}{omega} nk))

    对所有 (n)(k) 元组都可以这样搞。

    总时间复杂度为 (O(frac{1}{omega}n^2 k)),似乎并没有优化。但空间效率得到了不错的提升——(O(frac{1}{omega} n^{1.5}k))

    核心代码

    /*
     * Author : _Wallace_
     * Source : https://www.cnblogs.com/-Wallace-/
     * Problem : bitset 求解较高维偏序
     */
    
    // rank[k][v] 表示 k 维中值为 v 的点的编号 
    for (register int k = 0; k < K; k++)
    	for (register int i = 1; i * b <= n; i++)
    		for (register int j = 1; j <= i * b; j++)
    			dat[k][i].set(rank[k][j]); // 分块预处理 
    
    for (register int i = 1; i <= n; i++) {
    	bitset<N> ans, tmp;
    	ans.set(); // 一开始设为全 1(按位与操作)
    	for (register int k = 0; k < K; k++) {
    		tmp.reset(); // 每一维都要重置 
    		int p = point[k][i] / b; // 计算整块的范围 
    		tmp |= dat[k][p]; // 整块取现成 
    		for (register int j = p * b + 1; j <= point[k][i]; j++)
    			tmp.set(rank[k][j]); // 暴力扫散块 
    		ans &= tmp; // 对每一维按位与 
    	}
    	cout << ans.count() - 1 << endl; // 统计答案 
    }
    

    习题

    HihoCoder #1236 Scores:http://hihocoder.com/problemset/problem/1236

    后记

    reference:

  • 相关阅读:
    已解决[Authentication failed for token submission,Illegal hexadecimal charcter s at index 1]
    远程快速安装redis和远程连接
    远程快速安装mysql
    Swiper的jquery动态渲染不能滑动
    微服务架构攀登之路(三)之gRPC入门
    微服务架构攀登之路(二)之RPC
    微服务架构攀登之路(一)之微服务初识
    Go语言中new和make的区别
    Go语言实战爬虫项目
    Go语言系列(十一)- 日志收集系统架构
  • 原文地址:https://www.cnblogs.com/-Wallace-/p/13293541.html
Copyright © 2020-2023  润新知