深入理解计算机系统——位运算、树状数组

目录

一、位运算

[1. 相反数](#1. 相反数)

[2. LowBit:提取最低位的 1](#2. LowBit:提取最低位的 1)

[3. Letter:判断十六进制数含不含字母](#3. Letter:判断十六进制数含不含字母)

二、树状数组

[1. 树状数组是什么?](#1. 树状数组是什么?)

[2. 工作原理](#2. 工作原理)

[2.1 单点更新 add(i, delta)](#2.1 单点更新 add(i, delta))

[2.2 前缀和查询 sum(i)](#2.2 前缀和查询 sum(i))

[2.3 区间和](#2.3 区间和)


一、位运算

1. 相反数

对于一个有符号整数 x,其相反数可以通过 "取反加一" 得到:

cpp 复制代码
-x = ~x + 1
  • ~x:按位取反(包括符号位)

  • +1:加一操作

以 4 位为例
x = 5 (0101)~x = 1010~x+1 = 1011 → 即 -5 的补码。

2. LowBit:提取最低位的 1

LowBit(x) 返回一个整数,其二进制表示中只有 x 的最低位 1 被保留,其余位均为 0。

cpp 复制代码
unsigned LowBit(unsigned x) {
    return x & (-x);
}

数学推导

  • x 的二进制形式为 ...100...0(末尾有 k 个 0,即最低位 1 在第 k+1 位)。

  • ~x 将该 1 变为 0,后面的 0 全变为 1,前面的位取反。

  • ~x + 1 使得后面的 1 进位,恰好将原来那个 1 的位置恢复为 1,而更低位置 0。

  • 因此 x & (~x + 1) = 2^k

  • 而上述已经证明过 ~x+1 <=> -x。

3. Letter:判断十六进制数含不含字母

设一个十六进制数字的 4 位为x3x2x1x0(x3 是最高位,权值 8)。

字母范围 1010 ~ 1111 的二进制特点:

  • x3 = 1 (最高位必须为 1,因为 10~15 最高位都是 1)

  • (x2, x1) 不能同时为 0 ------ 因为如果 x2=0, x1=0 且 x3=1,则可能得到 1000 (8),1001(9),这是数字,不是字母。

所以逻辑表达式为:

cpp 复制代码
is_letter = b3 AND (b2 OR b1)

我们需要对 32 位整数中的每 4 位,也就是8个十六进制数 同时应用上述逻辑。

使用掩码分别取出每个十六进制数的 x3, x2, x1

  • x3(bit3)对应掩码 0x888888881000 1000 ... 1000

  • x2(bit2)对应掩码 0x444444440100 0100 ... 0100

  • x1(bit1)对应掩码 0x222222220010 0010 ... 0010

cpp 复制代码
	unsigned x1 = x & 0x22222222;   // 取出每 4 位中的第 2 位(从右数,位权 2)
	unsigned x2 = x & 0x44444444;   // 取出每 4 位中的第 3 位(位权 4)
	unsigned x3 = x & 0x88888888;   // 取出每 4 位中的第 4 位(位权 8)

为了进行逻辑运算,还需要将x3、x2、x1对应位上的数字都提取出来。

  • x3 右移 3 位,使每个半字节的 b3 移到 bit0

  • x2 右移 2 位,使 b2 移到 bit0

  • x1 右移 1 位,使 b1 移到 bit0

最终进行逻辑运算:

cpp 复制代码
	unsigned a = (x3 >> 3) & ((x2 >> 2) | (x1 >> 1));

a即是32位中每个十六进制数是否为字母的编码,1代表为字母,0代表为数字。

二、树状数组

1. 树状数组是什么?

树状数组是一个精巧的二进制分治结构,通过 lowbit 实现了快速跳跃。

其核心是维护一系列不同大小的块,使得任意前缀和都能由 O(log n) 个块拼成。

  • 单点更新add(index, delta) O(log n)

  • 前缀和查询sum(index) O(log n)

  • 通过前缀和差得到区间和sum(r) - sum(l-1)

2. 工作原理

假设原数组为 a[1..n]

树状数组 c[1..n] 的定义是:

c[i] = sum( a[i - lowbit(i) + 1] , ... , a[i] )

即从 i 向前数 lowbit(i) 个元素的和。

2.1 单点更新 add(i, delta)

要将 a[i] 增加 delta,需要更新所有 包含 a[i] 的 c[j]

这些 j 恰好是:i, i+lowbit(i), i+lowbit(i)+lowbit(i+lowbit(i)), ... ≤ n

原因

c[j] 覆盖的区间是 (j-lowbit(j), j]

j 包含 i,则 i > j-lowbit(j),即 j-i<lowbit(j)

  • 第一步:j₁ = i + lowbit(i)lowbit(i) < lowbit(j₁)

  • 第二步:j₂ = j₁ + lowbit(j₁)lowbit(i)+lowbit(j₁) < lowbit(j₂)

  • 依此类推,每一步累积的步长总和都小于下一步的 lowbit,从而保证 i 始终在新节点的左边界右侧。

cpp 复制代码
	void Add(int index,int val) {
		while (index<TArr.size()) {
			TArr[index] += val;
			index += LowBit(index);
		}
	}
    void update(int index, int val) {
		Add(index+1, val-Arr[index]);
		Arr[index] = val;
    }

2.2 前缀和查询 sum(i)

A[1] + ... + A[i]

C 的定义,C[i] 已经包含了从 i-lowbit(i)+1i 的和。

因此我们累加 C[i],然后令 i -= lowbit(i),继续累加,直到 i=0

cpp 复制代码
int sum(int i) {
    int res = 0;
    while (i > 0) {
        res += C[i];
        i -= lowbit(i);
    }
    return res;
}

2.3 区间和

cpp 复制代码
int range_sum(int l, int r) {
    return sum(r) - sum(l-1);
}
相关推荐
玛丽莲茼蒿5 小时前
Linux/Unix学习笔记(四)—— 进程管理
linux·学习·unix
richxu202510015 小时前
学完了江科大STM32,下一步该怎么学?
stm32·单片机·嵌入式硬件·学习
网络与设备以及操作系统学习使用者6 小时前
Linux与Windows核心差异深度解析
linux·运维·网络·windows·学习
Leo⁵6 小时前
基于 Git 的 Obsidian 笔记同步
笔记·git
大明者省6 小时前
网页开发标准与课程管理网站搭建
笔记
中屹指纹浏览器7 小时前
浏览器指纹与代理IP协同防护:原理、配置与企业级安全架构
经验分享·笔记
知识分享小能手7 小时前
Flask入门学习教程,从入门到精通,Flask智能租房——前期准备 知识点详解(5)
python·学习·flask
淳杰7 小时前
学习笔记 | playwright用法
笔记·学习
三品吉他手会点灯7 小时前
STM32F103 学习笔记-21-串口通信(第6节)-串口发送命令控制RGB灯
笔记·stm32·单片机·嵌入式硬件·学习
玄米乌龙茶1238 小时前
LLM成长笔记(十二):质量评估与可观测性
大数据·人工智能·笔记