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

目录

一、位运算

[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 小时前
不用服务器,不用配环境,我10分钟上线了一个AI Agent
人工智能·后端·算法
问君能有几多愁~7 小时前
C++ 数据结构复习笔记
数据结构·c++·笔记
至乐活着7 小时前
深入解析跳表SkipList:原理、实现与性能优化实战
数据结构·算法·跳表·skiplist·java实现
r_oo_ki_e_7 小时前
Java Map 集合学习笔记
java·笔记·学习
Jerry8 小时前
LeetCode 383. 赎金信
算法
ai产品老杨8 小时前
H264 H265视频分析常见问题和排查清单
人工智能·算法·音视频
Jerry8 小时前
LeetCode 454. 四数相加 II
算法
笨鸟先飞的橘猫9 小时前
skynet——sharetable学习
学习·skynet
liuyicenysabel9 小时前
大模型学习笔记 · 第八篇 · 进阶:偏好对齐与多卡训练
人工智能·笔记·学习
可编程芯片开发9 小时前
基于CPS-SPWM链式STATCOM系统在电压不平衡环境下控制策略的simulink建模与仿真
算法