13376 字
67 分钟
【ACM 算法题单】BIT相关问题

BIT逐位拆解问题#

在许多涉及位运算的题目中,一个非常常见的思考方式是 逐位拆解 。由于二进制表示天然具有独立性,一个整数的每一位往往可以单独分析,而不会与其他位产生复杂耦合。因此,在面对位运算问题时,第一步通常就是将问题从 “整数层面” 转化为 “二进制位层面” ,分别分析每一位的贡献。很多看似复杂的题目,一旦拆解到每一位之后,结构往往会变得非常清晰。

位运算问题之所以适合逐位分析,是因为大多数位运算操作都具有 按位独立性 。例如 ANDORXOR 等运算操作,本质上都是对每一位分别进行计算。因此,如果题目中出现的是这些运算的组合,往往可以将问题拆解为:第 k 位最终会变成什么值 。一旦我们能独立判断每一位的最优结果,就可以把所有位的结果重新组合得到答案。

位运算贪心构造#

位运算贪心构造的核心,在于利用二进制权重的指数级分布特性进行确定性决策。这类题目主要分为两种题型。第一种涉及 元素修改或重组操作 ,其求解关键是从动态操作中抽象出静态的 固有贡献 。题目提供的操作往往具有极强的方向性与不可逆性。例如,若操作为将元素 aia_i 与常数 xx 执行按位与(AND)运算,我们必须敏锐识别出位权空间的 不可触达区域:如果常数 xx 的某位为 11 ,则该位在操作扰动下将始终保持初始状态,无法被实质性改变。这种由 算子属性 决定的硬性约束,就是构造时必须预先锁定的 底层素材

在识别并锁定固有贡献后,接下来的决策逻辑往往紧跟着针对剩余空间的 高位贪心 。由于固有贡献属于无法更改的既定事实,我们应将其视为决策背景,而将贪心策略集中于那些 尚未被约束锁定、仍具备操作自由度 的位信息上。基于 2i>j=0i12j2^i > \sum_{j=0}^{i-1} 2^j 的位权特性,高位 11 的价值严格大于低位总和。因此,在排除固有贡献的干扰后,我们必须将剩余位资源视为最稀缺资产,通过 高位决策 将其优先分配给能产生最大边际收益的位。这种在自由空间内实现的 位权压制 ,确保了我们在不触碰硬性约束的前提下,实现对全局目标函数上限的精准控制。

第二种题型则涉及 不带修改的操作 ,通常要求从既定数组中选数以构造特定数值,其核心在于应用 高位贪心过滤机制 。由于无法改变元素的内部位结构,决策过程被抽象为一种逐位的 存亡筛选 。以 子集最大 OR 为例,利用其 只增不减 的特性,贪心策略通常趋于全集累加,以纳入所有可能的位贡献。而在处理 子集最大 AND 时,基于其 只减不增 的单调性,决策演变为严密的 排除法:从最高位向低位逐一审视,若当前位为 11 的元素集合足以支撑目标需求,则果断剔除在该位为 00 的所有干扰元素。这种单向且不可逆的决策路径,利用高位结果为低位划定了更精确的搜索边界。

然而,当这种基于单调性的高位决策逻辑碰撞上 异或(XOR)运算 时,情况会发生质的变化。子集最大 XOR 问题无法简单地通过贡献重组或存亡过滤来解决。异或运算相同为 00 且不同为 11自反特性 ,彻底破坏了数值增长的单调性。引入一个新元素可能会让原本已经确立的高位 11 瞬间塌陷为 00 。这种不确定性使得常规的贪心策略失效,迫使我们寻找更深层的 代数结构 来支撑决策。

异或运算的按位贪心#

从上面的分析可以知道,异或的贪心构造题往往比较复杂,其核心难点在于处理位与位之间的 相互耦合与干扰 。在常规的 AND 或 OR 运算中,我们对某一位的操作通常不会产生导致高位结果反转的副作用。但在异或空间里,每一个元素的加入都可能引发全局数值的剧烈震荡。为了在缺乏单调性的环境中重新贯彻 高位优先 的贪心原则,我们必须通过 人工构造 的方式,将原本无序且相互制约的原始数据,转化为一套严密的、互不干扰的独立结构。

这种结构化转化的本质是实现 决策解耦 。我们需要确保在确定了最高位的最优解后,后续对低位的任何尝试和优化,都绝对不会对已经固定的高位产生任何负面影响。这种从 依赖数据天然属性人工构建结构化独立性 的思想转变,是解决复杂异或最优化问题的通用范式。通过这种方式,我们将原本处于震荡中的动态决策,转化为一种在稳定结构上的线性搜索,从而在混沌中锁定每一位的高位收益。

1、线性基:子集最大异或和#

在处理异或相关的最优化问题时,若直接面对包含 nn 个元素的原始集合 SS ,其子集异或组合的数量级将达到 2n2^n 。当数据规模较大时,这种指数级的搜索空间会导致计算成本不可接受。为了高效处理这一空间,我们需要寻找一个 极小线性无关组(即基底 BB )。该基底必须满足两个核心特征:首先是 空间等效性 ,即原集合 SS 能产生的任何异或结果,均可由 BB 的子集异或得到;其次是 线性无关性 ,即 BB 中不存在任何可以通过其他元素组合得到的冗余元素。通过这种方式,我们将复杂的原始数据压缩为了一个规模仅为 O(log(maxS))O(\log(\max S)) 的精简集合,从而在根本上降低了后续决策的复杂度。

以集合 S={12,9,14,11}S = \{12, 9, 14, 11\} 为例,通过观察二进制位可以发现,1111 实际上可以由 1291412 \oplus 9 \oplus 14 组合得到。这意味着在异或空间的维度上,1111 并没有提供任何超出 {12,9,14}\{12, 9, 14\} 范围的新信息。我们的目标是剔除这类 线性相关 的冗余元素,找到能够支撑起相同空间的基底 B={12,9,14}B = \{12, 9, 14\} 。然而,在实际计算中,直接从原数组中筛选出特定的子集往往效率低下且逻辑复杂。因此,我们采取 “曲线救国” 的策略:转而寻找一个与原集合 逻辑等价 的集合 BB'

这里的 “等价” 基于两个核心代数推论:第一,用 aba \oplus b 的结果替代原集合中的 aabb ,不会改变集合所能张成的异或空间;第二,如果集合中存在 ab=0a \oplus b = 0 ,则说明其中一个元素是多余的,将其舍弃不会对非零异或和的组成产生任何影响。基于此,我们不必执着于保留原数组中的数字,而是可以通过不断的异或变换,构造出一组形态更加标准的等价元素。

为了将上述等价变换转化为可执行的算法,我们引入了 高斯消元 的思想。我们维护一个数组 dd ,其中 d[i]d[i] 存储最高位在第 ii 位的基底。当新元素 xx 尝试进入线性基时,它会从最高位向低位逐一扫描。如果 xx 的第 ii 位为 11d[i]d[i] 已经存在,则执行 x=xd[i]x = x \oplus d[i] 。这一步利用了替换不变性,旨在消除 xx 在该位上的影响力并继续向下探索。如果 xx 的最终结果被削减为 00 ,根据冗余舍弃性,我们将其视为无效信息丢弃;若它在某位停下并填补了空缺,则它成为了该位新的掌控者。

这种通过消元得到的阶梯化集合 BB' 具有极佳的 贪心特性 。由于每个基底 d[i]d[i] 的最高位被严格限制在第 ii 位,且其高位全部为 00 ,这就消除了不同元素之间的位权干扰。在求解最大异或和时,我们可以维护一个变量 resres ,并从高到低执行状态转移:

res=max(res,resd[i])res = \max(res, res \oplus d[i])

这一步的数学依据非常严谨:如果 resres 的第 ii 位已经是 11 ,异或 d[i]d[i] 必然会使该位变为 00 ,由于高位已经确定,这一步操作一定会让数值减小;反之,如果 resres 的第 ii 位是 00 ,异或 d[i]d[i] 之后第 ii 位变为 11 ,无论低位如何变化,数值一定会增大。这种贪心策略之所以成立,本质上是因为二进制权值具有绝对的 高位压制特性

2i>j=0i12j2^i > \sum_{j=0}^{i-1} 2^j

这意味着在任何时刻,保住更高位的 11 永远比优化后面所有位产生的总和更具价值。

2、字典树:点对最大异或和#

在处理 “两数异或” 问题时,字典树(01-Trie)是实现按位贪心策略的结构化利器。不同于线性基通过改变数值形态来压缩空间,01-Trie 完整地保留了原始数据的位信息,并将其映射到一棵深度固定的二叉树中。当我们需要在集合中找到一个数 aja_j 使得它与给定的 aia_i 异或结果最大时,其本质是在这棵代表数值空间的决策树上,进行一场基于高位优先原则的动态路径搜索。

01-Trie 将集合中所有数的二进制表示看作从根节点出发的路径。从最高位(通常是第 3030 位或 6060 位)到最低位,左子树代表 00 ,右子树代表 11 。这种结构将原本无序的数值集合转化为了一棵深度为 log(maxV)\log(\max V) 的树,使得每一条从根到叶子的路径都唯一对应一个原始数值。

在查询过程中,我们利用异或运算 “不同为 11 ” 的特性,对 aia_i 的二进制位从高到低进行扫描。对于 aia_i 的第 kk 位,如果该位的值为 bitbit ,我们的核心贪心策略是:只要条件允许,永远优先向 !bit!bit 的分支移动 。这种决策具有即时性和不可逆性:如果在当前层存在与 aia_i 某位相反的路径,我们必须选择该路径。因为即使这条路径会导致后续低位全部被迫选择相同位(异或结果为 00 ),高位贡献的 11 依然在数值上绝对优于低位所有贡献的总和。

假设当前处理到第 kk 位,我们期望寻找的分支是 aik & 1a_i \gg k \ \& \ 1 的取反值。如果该分支存在,异或结果在这一位将产生确定的贡献:

contribution=1kcontribution = 1 \ll k

此时,查询指针移动至对立分支,并继续向下递归。若该分支不存在,我们则被迫走向与 aia_i 当前位相同的分支,此时该位的异或贡献为 00 。通过这种从高到低的逐位锁定,我们最终能在 O(logmaxV)O(\log \max V) 的时间内精准定位到那个能产出最大火花的配对数。

这种模型在处理 “最大异或子数组” 问题时展现了卓越的转化能力。根据异或运算的自反性( xx=0x \oplus x = 0 ),任意区间 [L,R][L, R] 的异或和可以转化为两个前缀异或和的异或:

XorSum(L,R)=PreXor[R]PreXor[L1]XorSum(L, R) = PreXor[R] \oplus PreXor[L-1]

通过这一性质,求 “子数组最大异或和” 的问题便等价于 “在所有的前缀异或和中找到一对异或值最大的点” 。我们将所有 PreXorPreXor 依次插入 01-Trie,每插入一个新前缀,就在树中查询与其配对的最优历史前缀。这种方法将原本 O(n2)O(n^2) 的暴力枚举优化至 O(nlogM)O(n \log M) ,实现了时间复杂度与数值范围的完美平衡。

最大异或乘积#

题目链接

Problem Statement#

给你三个整数 abn ,要求你从所有满足 0x<2n0 \le x < 2^n 的整数 xx 中选择一个,使得表达式

(ax)×(bx)(a \oplus x) \times (b \oplus x)

的值最大。其中按位异或运算 \oplus 是对两个整数的二进制位逐位运算,当对应位不同时结果为 11 ,相同为 00

由于结果可能非常大,请你将最终的最大值对 109+710^9+7 取余后返回。

Constraints#

  • 0a,b<2500 \leq a, b < 2^{50}
  • 0n500 \leq n \leq 50
  • 所有输入值均为整数

Input#

输入包含一行:

abna \quad b \quad n

Output#

输出一个整数,表示满足条件的最大值 (ax)×(bx)(a \oplus x) \times (b \oplus x)109+710^9+7 取余后的结果。

Sample Input 1#

12 5 4

Sample Output 1#

98

Sample Input 2#

6 7 5

Sample Output 2#

930

Sample Input 3#

1 6 3

Sample Output 3#

12

题目要点解析#

本题要求在 0x<2n0 \leq x < 2^n 的范围内选择一个整数 xx ,使得 (ax)(bx)(a \oplus x)(b \oplus x) 取得最大值。由于 xx 仅影响最低 nn 位,因此问题天然适合采用逐位分析的方法。对于位运算题目而言,常见的处理方式是拆位分析 ,因为按位运算本质上是逐位独立定义的。当我们决定采用拆位思路时,首先应当思考是否可以通过贪心提前确定某些位的操作,从而将问题规模逐步缩减。这种 “先确认显然最优的局部决策” 的思考方式,往往能够显著简化整体结构。

我们引入两个变量:

A=ax,B=bxA = a \oplus x, \quad B = b \oplus x

问题等价于最大化:

A×BA \times B

为了理解如何选择 xx ,我们从每一位出发分析 aia_ibib_i 的关系:

  • 当某一位满足 ai=bi=1a_i = b_i = 1 时,如果令 xi=1x_i = 1 ,则该位在 AABB 中都会变为 00 ;若令 xi=0x_i = 0 ,则保持为 11 。显然,同时减少两个 11 会降低乘积,因此这一位的最优策略是保持不变,即令 xi=0x_i = 0

  • 当某一位满足 ai=bi=0a_i = b_i = 0 时,如果令 xi=1x_i = 1 ,则该位在 AABB 中都会变为 11 ;若令 xi=0x_i = 0 ,则保持为 00 。显然,同时增加两个 11 会提升乘积,因此这一位的最优策略是保持不变,即令 xi=1x_i = 1

经过上述两类情况的分析后,可以发现这些位的贡献已经完全确定,它们构成了一个不可改变的 “固定部分” 。真正影响优化结构的,是那些满足 aibia_i \ne b_i 的位。在这些不同位上,若某位原本是 ai=1,bi=0a_i = 1, b_i = 0 ,则当 xi=0x_i = 0 时保持原状,当 xi=1x_i = 1 时二者交换。因此,在这些位上,xx 的作用本质上是决定该位权值分配给 AA 还是 BB 。需要注意的是,无论如何分配,该位的权值 2i2^i 总是存在于两者之一,因此这些位上的总权值是固定的,只是可以在两数之间重新分配。

设固定部分分别为 CAC_ACBC_B ,所有不同位的权值总和为 TT 。则可以写成:

A=CA+xA = C_A + xB=CB+(Tx)B = C_B + (T - x)

其中 xx 表示分配给 AA 的那部分权值。于是乘积为:

(CA+x)(CB+Tx)(C_A + x)(C_B + T - x)

这是一个关于 xx 的二次函数。将其展开可以得到负的二次项系数,因此函数开口向下,存在唯一最大值。根据二次函数的基本性质可知,在总资源 TT 固定的情况下,当两数尽可能接近时,乘积达到最大。因此,最优策略是在可交换位上尽量平衡两数的大小。由于二进制数中高位权重大于低位,在具体实现时应当从高位到低位进行贪心决策,每当遇到一个可以分配的位,就优先将其分配给当前数值较小的一方,从而保持两数尽可能接近。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

数组末尾最小值#

题目链接

Problem Statement#

给你两个整数 nx 。你需要构造一个长度为 nn 的正整数数组 nums ,满足:

  • 对所有 0i<n10 \leq i < n - 1 ,都有 nums[i + 1] > nums[i]
  • 数组 nums 中所有元素的按位 AND 运算结果等于 xx

请返回数组 nums 的最后一个元素 nums[n - 1]最小可能值

Constraints#

  • 1n,x1081 \leq n, x \leq 10^8
  • 输出的最后一个元素必为正整数
  • 所有输入均为整数

Input#

输入包含一行:

nxn \quad x

Output#

输出一个整数,表示满足条件的数组 nums 的最小可能的最后一个元素 nums[n - 1]

Sample Input 1#

3 4

Sample Output 1#

6

Sample Input 2#

2 7

Sample Output 2#

15

题目要点解析#

本题依旧是一道典型的位运算构造问题,因此自然应当从拆位的角度进行分析。对于位运算题目,我们通常优先考虑逐位拆解,并结合贪心思想,尽可能先确定那些可以直接确定的 “固定贡献” ,从而将问题转化为剩余部分的结构优化。

题目要求构造一个严格递增的正整数数组 nums ,并满足所有元素的按位与结果等于 xx 。设数组为:

nums0,nums1,,numsn1nums_0, \, nums_1, \, \dots, \, nums_{n-1}

并要求:

nums0&nums1&&numsn1=xnums_0 \, \& \, nums_1 \, \& \, \cdots \, \& \, nums_{n-1} = x

按位与运算的性质决定了:某一位最终为 11 ,当且仅当 所有数在该位上都为 11 。因此,如果 xx 的某一位是 11 ,那么数组中每一个元素在这一位上都必须是 11 。这些位是完全被强制确定的,不存在任何选择空间。接下来考虑 xx 中为 00 的位。对于这些位,只需保证在所有元素中至少有一个数在该位上为 00 ,即可保证最终按位与结果为 00 。但由于我们希望数组严格递增且最后一个数尽量小,因此最优策略是尽量以最小幅度递增构造数组。

关键观察在于,既然所有数都必须包含 xx11 位,那么数组中的每个元素都可以写成:

numsi=xyinums_i = x \, \| \, y_i

其中 yiy_i 只在 xx00 的位上可以自由取值。于是问题转化为:构造一个严格递增的序列 y0<y1<<yn1y_0 < y_1 < \cdots < y_{n-1} ,并使得最终的 numsn1nums_{n-1} 最小。

为了让 numsn1nums_{n-1} 尽可能小,就应当让 yiy_i 本身尽可能小且按最小步长递增。显然,最优构造方式是令:

yi=iy_i = i

但需要注意的是,这里的 ii 不是直接填入原数,而是将 ii 的二进制表示嵌入到 xx00 的那些位中。也就是说,我们将自然数序列 0,1,2,3,,n10, 1, 2, 3, \dots, n-1 的二进制表示依次填入 xx00 的位置上,从低位到高位对应填充。

这样构造得到的序列天然严格递增,同时保证所有元素都包含 xx 的固定 11 位,因此按位与结果恰好为 xx 。由于我们使用的是从 00 开始的最小连续整数序列,因此最终得到的 numsn1nums_{n-1} 也是所有合法构造中的最小值。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

平方和最大操作#

题目链接

Problem Statement#

给你一个 0-索引的整数数组 nums 和一个正整数 k。你可以对数组执行如下操作任意次:

  • 从数组中任选两个不同的下标 ij,同时将

    • nums[i] 更新为 (nums[i] AND nums[j])
    • nums[j] 更新为 (nums[i] OR nums[j])

其中 ANDOR 分别表示 按位与按位或 操作。

你可以在执行若干次(或不执行)操作之后,从最终的数组中选出 k 个元素 并计算它们的平方和。请你返回最大可能的平方和,由于结果可能很大,返回值对 109+710^9 + 7 取余。

Constraints#

  • 1knums.length1051 \leq k \leq nums.length \leq 10^5
  • 1nums[i]1091 \leq nums[i] \leq 10^9
  • 所有输入均为整数

Input#

输入包含两行:

  • 第一行包含两个整数 nnkk ,其中 nn 表示数组长度,kk 表示选取个数。
  • 第二行包含 nn 个整数,表示数组的每个元素。

nkn \quad k

nums0nums1numsn1nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}

Output#

输出一个整数,表示在执行任意合法操作后,选出 kk 个元素的最大平方和,并对 109+710^9+7 取余。

Sample Input 1#

4 2
2 6 5 8

Sample Output 1#

261

Sample Input 2#

4 3
4 5 4 7

Sample Output 2#

90

题目要点解析#

这道题依然是一类典型的位运算重排问题,因此自然应当从拆位分析入手。对于涉及 AND、OR 之类按位操作的题目,我们通常优先逐位研究其变化规律,寻找是否存在守恒量或固定贡献。一旦发现某种 “不可改变的结构” ,问题往往就会从复杂的操作模拟,转化为更本质的资源分配问题。

题目给定的操作为:任选两个数 a,ba, b ,执行:

aa&b,baba \leftarrow a \, \& \, b, \quad b \leftarrow a \, \| \, b

为了理解其本质,我们只需考察单个位上的变化情况。设某一位上两数分别为 (x,y)(x,y) 。如果是 (0,0)(0,0)(1,1)(1,1) ,显然操作后不发生变化;如果是 (0,1)(0,1) ,按位与得到 00 ,按位或得到 11 ,结果仍为 (0,1)(0,1) ;只有在 (1,0)(1,0) 时,会被调整为 (0,1)(0,1)

从这四种情况可以看出一个核心事实:每一位上的 11 的总数量保持不变。操作只是在不同元素之间 “转移” 该位上的 11 ,而不会创造或消灭 11 。因此,设第 bb 位上最初共有 cntbcnt_b11 ,那么无论如何操作,最终这一位上仍然有且仅有 cntbcnt_b11 。这一步实际上已经揭示了问题的本质:我们可以在数组元素之间自由重新分配每一位上的 11 ,但各个位的 11 的总数是守恒的。于是原问题等价于——在每一位上拥有 cntbcnt_b 个权值为 2b2^b 的资源,可以任意分配到各个元素中。

接下来考虑目标函数。我们最终要从数组中选出 kk 个元素,使得它们的平方和最大:

maxi=1kai2\max \sum_{i=1}^{k} a_i^2

平方函数是严格凸函数,这意味着在总和一定的情况下,将数值集中在少数元素上,比平均分配能获得更大的平方和,也就是:

(x+y)2>x2+y2(if x,y>0)(x+y)^2 > x^2 + y^2 \quad (\text{if } x,y>0)

因此,既然每一位上的贡献可以自由分配,那么最优策略必然是尽可能把各个位上的 11 叠加到同一批元素上,从而构造出尽可能大的数。具体实现时,可以按如下贪心思路进行:统计所有 cntbcnt_b ,然后重复 kk 轮构造。每一轮构造一个新的数,对于所有满足 cntb>0cnt_b > 0 的位,将对应的 2b2^b 加入当前数,并令:

cntbcntb1cnt_b \leftarrow cnt_b - 1

这样得到的第一个数最大,第二个数次大,以此类推。由于每一轮都尽可能收集所有剩余的高位贡献,数值会呈现逐层递减的结构,而平方和则因高度集中而被最大化。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

最大异或和操作#

题目链接

Problem Statement#

给你一个 0-索引 的整数数组 nums 。在一次操作中,你可以选择任意一个 非负整数 x 和一个下标 i ,然后将 nums[i] 更新为:

nums[i]=nums[i]&(nums[i]x)nums[i] = nums[i] \, \& \, (nums[i] \oplus x)

其中 AND 是按位与运算,XOR 是按位异或运算。

请返回在执行任意次数(包括 0 次)操作后,数组中所有元素按位异或(XOR)之后能获得的 最大可能值

Constraints#

  • 1nums.length1051 \leq nums.length \leq 10^5
  • 0nums[i]1080 \leq nums[i] \leq 10^8
  • 所有输入均为整数

Input#

输入包含两行:

  • 第一行包含一个整数 nn 表示数组长度。
  • 第二行包含 nn 个整数表示数组的元素。

nn

nums0nums1numsn1nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}

Output#

输出一个整数,表示在经过任意次数操作后,数组所有元素按位 XOR 的最大可能值。

Sample Input 1#

3 2 4 6

Sample Output 1#

7

Sample Input 2#

1 2 3 9 2

Sample Output 2#

11

题目要点解析#

这道题的关键在于先理解给定操作到底能做什么。题目允许我们进行如下操作:

nums[i]=nums[i]&(nums[i]x)nums[i] = nums[i] \, \& \, (nums[i] \oplus x)

注意到最外层是一个按位与运算,左边的 nums[i] 本身就相当于一个 “掩码” 。这意味着,无论我们怎样选择 xx ,运算结果都不可能在原本为 00 的位上变成 11 。换句话说,这个操作只能把某些 11 变成 00 ,而不能凭空增加新的 11 。再看中间的异或部分。由于 xx 可以是任意非负整数,因此 nums[i] ^ x 可以在每一位上自由控制翻转与否。结合外层的按位与,可以发现我们实际上可以 “选择性地关闭” nums[i] 中的任意若干个 11 。也就是说,经过若干次操作之后,nums[i] 可以变成它的任意一个子集,但绝对不能增加新的 11

因此,本题的本质就变成了:我们可以对每个元素,删除它的一些 11 位,但不能新增 11 位。最终目标是让整个数组的异或值最大。

接下来从异或的角度分析。一个整数的异或结果在某一位上为 11 ,当且仅当该位上所有元素中 11 的个数为奇数。既然我们可以删除某些 11 ,那么在每一位上,我们唯一能做的事情就是减少 11 的数量。为了让最终异或结果最大,我们希望在尽可能高的位上得到 11 。而某一位能否最终为 11 ,取决于这一位在所有元素中是否 “至少出现过一次” 。如果某一位在原数组中从未出现过 11 ,那么由于我们不能增加 11 ,这一位最终必然为 00

而如果某一位原本出现过至少一次 11 ,那么我们就可以通过删除多余的 11 ,使这一位的 11 的总数变成奇数,从而保证最终异或结果在这一位为 11 。因为只要这一位原本有至少一个 11 ,我们就可以保留一个、删除其余全部,使其数量为 11(奇数)。因此,对于每一位,只要原数组中存在至少一个元素在该位为 11 ,我们就一定可以让最终异或结果在这一位为 11 ;如果原本没有,则永远无法得到 11

这说明最终答案恰好等于所有元素的按位或结果:

nums[0]nums[1]nums[n1]nums[0] \, \| \, nums[1] \, \| \, \cdots \, \| \, nums[n-1]

因为按位或正好表示 “每一位是否至少出现过一次 1” 。

综上所述,这道题虽然操作形式复杂,但本质上只是一个位运算性质的转化问题。操作只能删除 11 ,不能增加 11 ;而异或最大化只关心每一位是否能保留奇数个 11 。最终答案就是原数组的按位或值,时间复杂度为 O(n)O(n)

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

最短or路径问题#

题目链接

Problem Statement#

给定一个由 11NN 编号的无向连通图,共有 NN 个顶点和 MM 条边。图中不包含自环,但可能包含重边。第 ii 条边连接顶点 uiu_i 和顶点 viv_i ,并带有一个非负整数权值 wiw_i

对于任意一条从顶点 11 到顶点 NN简单路径(不重复经过同一顶点),定义该路径的代价为路径上所有边权的按位 OR 运算结果,即:

wi1OR wi2ORORwikw_{i_1} \, \mathrm{OR}\ w_{i_2} \, \mathrm{OR} \, \cdots \, \mathrm{OR} \, w_{i_k}

其中每个 wijw_{i_j} 是路径所包含的边的权值。

请找出所有从顶点 11 到顶点 NN 的简单路径中,使上述 OR 结果最小的那个值,并输出该最小值。

Constraints#

  • 2N2×1052 \leq N \leq 2 \times 10^5
  • N1M2×105N - 1 \leq M \leq 2 \times 10^5
  • 1ui<viN1 \leq u_i < v_i \leq N
  • 0wi<2300 \leq w_i < 2^{30}

Input#

输入格式如下:

NMN \quad M

u1v1w1u_1 \quad v_1 \quad w_1

u2v2w2u_2 \quad v_2 \quad w_2

\ldots

uMvMwMu_M \quad v_M \quad w_M

Output#

输出一个整数,表示从顶点 11 到顶点 NN 的所有简单路径中,按位 OR 运算结果的最小可能值。

Sample Input 1#

4 5
1 2 1
1 3 4
2 3 2
2 4 4
3 4 3

Sample Output 1#

3

Sample Input 2#

3 5
1 2 1
1 2 2
1 2 3
1 2 4
2 3 4

Sample Output 2#

4

Sample Input 3#

8 12
4 5 16691344
5 7 129642441
2 7 789275447
3 8 335307651
3 5 530163333
5 6 811293773
3 8 333712701
1 2 2909941
2 3 160265478
5 7 465414272
1 3 903373004
6 7 408299562

Sample Output 3#

468549631

题目要点解析#

或位运算

异或数列博弈论#

题目链接

题目要点解析#

异或位运算

最大异或对构造#

题目链接

题目要点解析#

异或位运算

位运算和式变换#

在一些涉及 位运算的求和问题 中,直接按照题意枚举所有元素往往是不可行的,因为数据范围通常非常大。这时,一个非常常见的技巧是对表达式进行 和式变换(sum transformation):将原本关于整数的运算,转化为 按位统计贡献 的形式。

其核心思想来源于位运算的 按位独立性 。例如在计算大量异或、与、或运算的和时,可以将问题拆解为:每一位对最终答案的贡献 。对于某一位 kk ,只需要统计在所有数中这一位为 0011 的数量,然后计算能够使该位为 11 的组合数量,再乘上对应的权值 2k2^k 。这样就把一个复杂的双重求和问题,转化为对每一位进行一次简单计数。

例如在求解下面这个累加和:

i=1nj=1m(ij)\sum_{i=1}^{n}\sum_{j=1}^{m}(i\oplus j)

我们可以对每一位 kk 单独计算贡献。如果 1n1 \sim n 中该位为 11 的数量为 aa ,为 00 的数量为 nan-a ,而 1m1 \sim m 中该位为 11 的数量为 bb ,为 00 的数量为 mbm-b ,那么只有 0 与 1 配对 才会使该位异或结果为 11 ,因此该位的贡献数量为:

[a(mb)+b(na)]×2k\Big[a(m - b) + b(n - a)\Big] \times 2^k

通过这种方式,原本需要枚举 nmnm 个配对的问题,可以被转化为 按位统计 + 数学计数 的过程,复杂度通常只需要 O(logn)O(\log n)O(logm)O(\log m) 。这种思路在许多位运算求和问题中都非常常见,是逐位拆解方法的重要应用场景。从更深层次来看,这种方法之所以成立,尤其在异或问题中表现突出,是因为 异或运算本质上等价于二进制下的模 2 加法(无进位加法)。在每一位上,其运算规则为:

00=0,01=1,10=1,11=00 \oplus 0 = 0,\quad 0 \oplus 1 = 1,\quad 1 \oplus 0 = 1,\quad 1 \oplus 1 = 0

因此,可以把整数看作由 0/10/1 组成的二进制向量,而异或运算就是对这些向量进行 逐位的模 2 加法 。由于不存在进位,不同二进制位之间完全独立,这也使得 “按位统计贡献” 的和式变换方法在异或问题中具有天然优势。

在其他类似的问题中,我们还可以将 “寻找满足某种异或关系的数对” 转化为寻找对应的 目标值 。这种结构与经典的两数之和思想非常相似:在遍历数组时,可以用哈希表记录已经出现过的元素,并查询当前元素对应的目标值 aixa_i \oplus x 是否存在。如果存在,我们就找到了一组满足条件的数对。

数组异或序列和#

题目链接

题目要点解析#

子段和的异或和#

题目链接

题目要点解析#


BIT性质分析问题#

除了常见的逐位拆解思路之外,还有一类位运算问题并不适合直接按位分析,而是需要通过挖掘运算本身的 特殊性质 来解决。这类题目的关键往往不在于逐位枚举,而在于观察某些操作在整体结构上的变化规律。一旦发现这些规律,问题往往可以被大幅简化,甚至转化为一个完全不同类型的模型。

因此,在处理位运算问题时,除了考虑逐位分析之外,也可以尝试从 运算性质 的角度进行思考。例如某些运算具有单调性、可逆性或抵消性质,这些结构特征往往会对状态变化产生强约束。通过利用这些性质,可以快速排除大量不可能的情况,从而显著降低问题的复杂度。

AOR相关性质#

按位与和按位或运算都具有非常明显的 单调结构 。按位与运算会使数值 单调不增:某一位一旦在某次运算中变为 00 ,之后再进行与运算时就不可能恢复为 11 。因此,在只包含按位与操作的过程中,整体数值只会不断减小,很多较大的状态从一开始就不可能被达到。这一性质在区间问题、动态维护以及状态压缩中经常被利用,例如区间按位与的不同结果数量通常是非常有限的。

与之相对,按位或运算则具有 单调不减 的特征:某一位一旦变为 11 ,之后通常无法再恢复为 00 。因此,在只包含按位或操作的过程中,数值会不断增大,状态变化也会逐渐趋于稳定。利用这一性质,可以快速判断某些状态是否可达,并且在许多问题中可以证明不同结果的数量是受限的,从而避免枚举所有可能的状态。

此外,按位与和按位或还具有 幂等性 ,即:

a&a=aaa=aa \, \& \, a = a \quad a \, \| \, a = a

这意味着重复执行同样的操作不会产生新的结果。在某些操作序列问题中,这种性质会导致过程在有限步之后 收敛到稳定状态 ,从而使得原本需要大量模拟的过程可以被显著简化。

多数目子数组#

题目链接

Problem Statement#

给你一个由非负整数组成的数组 nums 。我们定义子数组 nums[l..r]得分 为:

nums[l]&nums[l+1]&&nums[r]nums[l] \, \& \, nums[l+1] \, \& \, \cdots \, \& \, nums[r]

其中 &\& 表示按位 与 操作。

现在你需要将数组分割成若干个 连续子数组 ,满足:

  • 数组中的每个元素恰好属于一个子数组;
  • 所有子数组得分之和 最小

在满足上述条件的前提下,返回能构造出的子数组 最多个数

这里的子数组必须是连续的数组片段。

Constraints#

  • 1nums.length1051 \leq nums.length \leq 10^5
  • 0nums[i]1060 \leq nums[i] \leq 10^6
  • 数组 nums 中的所有元素均为整数

Input#

输入包含两行:

  • 第一行包含一个整数 bb ,表示数组的长度。
  • 第二行包含 nn 个整数,表示数组的元素。

nn

nums0nums1numsn1nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}

Output#

输出一个整数,表示在满足最小得分和的前提下,能够划分出的最大子数组数量。

Sample Input 1#

1 0 2 0 1 2

Sample Output 1#

3

Sample Input 2#

5 7 1 3

Sample Output 2#

1

题目要点解析#

这道题的核心在于理解按位与运算的单调性质。对于一段区间而言,随着参与按位与的元素个数增加,结果只会保持不变或变小,而不可能变大。换句话说,区间越长,其按位与的结果越不利于增大总和。因此,在考虑如何划分子数组时,应当先分析整个数组的整体按位与结果。

设整个数组的按位与为:

T=nums0&nums1&&numsn1T = nums_0 \, \& \, nums_1 \, \& \, \cdots \, \& \, nums_{n-1}

显然,任意子数组的按位与结果都不会小于 TT 。因为整体按位与已经包含了所有元素的约束,是能够达到的最小值。也就是说,所有合法划分的子数组得分之和,至少为若干个不小于 TT 的数之和。

如果 T>0T > 0 ,那么问题立刻变得简单。因为无论如何划分,每个子数组的按位与结果都不小于 TT ,一旦拆分成多个子数组,得分之和就至少为:

T+T+T + T + \cdots

显然会严格大于单个整体区间的得分 TT 。既然题目要求总得分最小,那么此时最优策略只能是不拆分数组,即整个数组作为一个子数组,答案为 11

接下来考虑 T=0T = 0 的情况。既然整体按位与已经为 00 ,说明存在某些位在不同元素之间互相 “抵消” ,最终使得整段区间的按位与结果为零。由于按位与运算是单调递减的,我们希望尽可能多地构造出按位与为 00 的子数组。因为一旦某个子数组的得分为 00 ,它对总和的贡献就是最小的。

因此,在 T=0T = 0 的情况下,我们的目标转化为:将数组划分为尽可能多的连续区间,使得每个区间的按位与结果为 00 。这样总得分仍然为 00 ,同时子数组数量最多。具体实现时,可以从左到右维护一个当前区间的按位与值,初始设为一个全 11 的掩码或直接从第一个元素开始累积。每次将当前元素与进来,当结果变为 00 时,就立即在此处切断,计数加一,并重新开始下一段区间的累积。由于按位与只会变小,一旦为 00 就不可能再恢复,因此此时切分是最优且贪心正确的。

综上,本题的关键在于先计算整体按位与,分情况讨论。当整体按位与大于 00 时答案为 11 ;当整体按位与为 00 时,通过贪心切分每一个前缀按位与首次变为 00 的位置,可以得到最多的子数组数量。这种思路的本质是利用按位与的单调性,将问题转化为 “尽可能多地制造零贡献区间” 的结构化贪心问题。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

按位或最大数组#

题目链接

Problem Statement#

给你一个长度为 nn0-索引 数组 nums ,数组由非负整数组成。对于每个下标 ii0i<n0 \leq i < n ),你需要找出从位置 ii 开始的 最短非空子数组 ,使得这个子数组的 按位 OR 运算值 是从位置 ii 到末尾所有可能子数组中所能获得的 最大 OR 值

换句话说,记 Bi,jB_{i,j} 为子数组 nums[i..j] 的按位 OR 值,你要找出最小的 jj 满足:

nums[i]ORnums[i+1]ORORnums[j]=maxik<n(nums[i]ORORnums[k])nums[i] \, \text{OR} \, nums[i{+}1] \, \text{OR} \, \cdots \, \text{OR} \, nums[j] = \max_{i \leq k < n} \left( nums[i] \, \text{OR} \, \cdots \, \text{OR} \, nums[k] \right)

按位 OR 运算定义为:对二进制数的每一位进行 OR,如果任一输入在该位是 11 ,则输出为 11

请你返回一个整数数组 answer ,长度为 nn ,其中 answer[i] 是从位置 ii 开始,取得最大按位 OR 值所需的最短子数组长度。子数组必须是连续的非空片段。

Constraints#

  • n==nums.lengthn == nums.length
  • 1n1051 \leq n \leq 10^5
  • 0nums[i]1090 \leq nums[i] \leq 10^9
  • 所有输入均为整数

Input#

输入包含两行:

  • 第一行包含一个整数 bb ,表示数组的长度。
  • 第二行包含 nn 个整数,表示数组的元素。

nn

nums0nums1numsn1nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}

Output#

输出一行整数数组 answer ,其中 answer[i] 表示从下标 ii 开始的最短子数组长度,该子数组的按位 OR 值等于从 ii 出发能达到的最大按位 OR 值。

Sample Input 1#

1 0 2 1 3

Sample Output 1#

3 3 2 2 1

Sample Input 2#

1 2

Sample Output 2#

2 1

题目要点解析#

这道题如果换成 “子数组最大累加和” ,我们往往只需要维护某种前缀或后缀的最优结构即可,因为加法具有可逆性和线性结构。但按位或运算并不具备这种性质,它是不可逆的,因此必须从按位或本身的结构特征入手分析。

首先注意按位或的单调性,设:

Ai,j=nums[i]nums[i+1]nums[j]A_{i,j} = nums[i] \, \| \, nums[i+1] \, \| \, \cdots \, \| \, nums[j]

当我们固定 ii ,不断增大 jj 时,Ai,jA_{i,j} 只可能变大或保持不变,不可能变小。因为某一位一旦在或运算中被置为 11 ,之后就不可能再变回 00 。因此对于每个起点 ii ,所谓 “最大按位或值” ,实际上就是扩展到数组末尾得到的值:

Ai,n1A_{i,n-1}

问题就转化为:对于每个 ii ,找到最小的 jj ,使得:

Ai,j=Ai,n1A_{i,j} = A_{i,n-1}

如果从每个 ii 暴力向右扩展,时间复杂度会达到 O(n2)O(n^2) 。但按位或还有一个更关键的性质:由于整数最多只有 3232 位,而每一位最多只会从 00 变成 11 一次,因此对于固定起点 ii ,随着 jj 的增加,Ai,jA_{i,j} 的不同取值不超过 3232 种。

换一个角度来看,我们从右向左动态维护 “所有后缀按位或结果” 。设当前处理到位置 ii ,我们知道从 i+1i+1 开始的所有不同的后缀或值。将 nums[i]nums[i] 与这些值分别进行一次或运算,再加上单独的 nums[i]nums[i] ,就得到了从 ii 开始的所有后缀或值。

这里有一个非常重要的细节:后缀按位或值在扩展过程中是单调不减的,因此生成的新或值序列也是单调的。由于单调性,相同的或值一定是连续出现的。换句话说,若在更新过程中产生了重复值,那么这些重复值必然是相邻的,可以在构造时直接去重。

因此在实现时,我们只需维护一个有序数组(或列表)表示不同的后缀或值。每次更新时按顺序计算新的或值序列,并在生成过程中去掉与前一个相同的值即可。由于总状态数不超过 3232 个,因此每一步的复杂度是常数级的。

在得到从 ii 开始的所有不同后缀或值后,最大值就是该列表中的最后一个元素。由于我们在维护时可以同时记录每个或值最远延伸到的下标,因此可以直接得到最短的 jj ,从而计算答案长度。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

题目相关拓展#

如果把本题中的 “最大 OR 值” 改成与 “最大 XOR 值” ,那么问题结构会发生明显变化。设我们定义后缀异或和:

Si=nums[i]nums[i+1]nums[n1]S_i = nums[i] \oplus nums[i+1] \oplus \cdots \oplus nums[n-1]

则任意区间 [i,j][i, j] 的异或值可以表示为:

nums[i]nums[j]=SiSj+1nums[i] \oplus \cdots \oplus nums[j] = S_i \oplus S_{j+1}

因此,如果我们希望在固定起点 ii 的情况下,使某个后缀子区间的异或值最大,本质上就是在所有可能的 Sj+1S_{j+1} 中,找到一个值,使得 SiSj+1S_i \oplus S_{j+1} 最大。这就转化成了一个经典问题:给定一个数,在一组数中找到与它异或结果最大的数 。这正是 “最大异或对” 问题。

解决这类问题的标准做法是使用二进制字典树(Trie)。具体思路如下:

我们从右向左遍历数组,动态计算后缀异或和 SiS_i 。在处理到位置 ii 时,Trie 中已经存储了所有 SjS_{j}(其中 j>ij > i )。此时我们在 Trie 中查询一个数,使得与当前 SiS_i 异或的结果最大。Trie 的构造方式是按二进制位从高位到低位插入。查询时,为了让异或结果最大,我们在每一位上尽量选择与当前位相反的分支(若存在)。这样贪心地从高位优先保证结果尽可能大,最终得到全局最优解。

由于整数最多 3232 位,每次插入和查询的复杂度都是 O(32)O(32) ,整体时间复杂度为 O(32n)O(32n) ,可以视为线性。

最长优雅子数组#

题目链接

Problem Statement#

给你一个由正整数组成的数组 nums 。当一个子数组中任意两个 不同位置 的元素按位与(AND)运算结果为 0 时,我们称这个子数组为 优雅子数组(nice subarray)。按位与运算定义为:对于两个整数,对各个二进制位进行 AND 操作,当对应位都为 1 时该位结果为 1 ,否则为 0

请你返回数组 nums最长的优雅子数组的长度

子数组指数组中的一个连续部分。注意,长度为 1 的子数组总是优雅的

Constraints#

  • 1nums.length1051 \leq nums.length \leq 10^5
  • 1nums[i]1091 \leq nums[i] \leq 10^9
  • 所有输入均为整数

Input#

输入包含两行:

  • 第一行包含一个整数 nn 表示数组长度。
  • 第二行包含 nn 个整数表示数组的元素。

nn

nums0nums1numsn1nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}

Output#

输出一个整数,表示数组中最长的优雅子数组的长度。

Sample Input 1#

1 3 8 48 10

Sample Output 1#

3

Sample Input 2#

3 1 5 11 13

Sample Output 2#

1

题目要点解析#

题目要求子数组中任意两个不同位置的元素按位与结果为 00 。等价地说,任意两个数在二进制表示中不能在同一位上同时为 11 。也就是说,这个子数组中的所有数字,在二进制层面上 11 的位置必须两两互不重叠。

从这个条件可以立刻得到一个重要结论:一个优雅子数组的长度不会超过二进制位数上限。因为一个整数最多只有 3232 位(在本题数据范围内甚至不到 3232 位),而每一位最多只能被一个元素占用。如果两个元素在同一位上都有 11 ,那么它们的按位与一定不为 00 ,子数组就不合法。因此,一个优雅子数组的最大长度最多是 3232 ,也就是 “每个数只贡献一个不同的 11 位” 的极端情况。

这个上界非常关键。数组长度可以达到 10510^5 ,但真正需要考虑的窗口长度最多只有 3232 。也就是说,对于每个起点,向右最多扩展 3232 步就可以确定该起点能形成的最长优雅子数组。最直接的做法是枚举左端点 ii ,然后从 ii 开始向右扩展窗口,维护当前窗口内所有元素的按位或值 mask 。当我们尝试加入一个新元素 nums[j] 时,只需要检查:

mask&nums[j]==0mask \, \& \, nums[j] == 0

如果成立,说明没有任何位冲突,可以加入窗口,并更新:

mask=masknums[j]mask = mask \, \| \, nums[j]

一旦不成立,就停止扩展当前起点。由于窗口长度最多 3232 ,每个起点最多检查 3232 次,总时间复杂度为 O(32n)O(32n) ,可以视为线性复杂度。

当然,也可以用更标准的滑动窗口写法。用双指针维护一个区间 [l, r) ,同时维护当前窗口的按位或值 mask 。当加入 nums[r] 时,如果产生冲突( mask & nums[r] != 0 ),就不断移动左指针,并把 nums[l]mask 中删除。由于窗口中每一位只会出现一次,删除操作可以通过下面这个公式来完成:

mask=^nums[l]mask \mathrel{\hat{=}} nums[l]

这样每个元素最多进窗口一次、出窗口一次,总复杂度同样是线性。

main.cpp
# include <bits/stdc++.h>
using namespace std;
int main(){
}

XOR相关性质#

与按位与、按位或不同,按位异或运算并不具备明显的单调性,因此很多问题往往无法通过简单的 “不断变大” 或 “不断变小” 的规律来进行分析。许多复杂的异或问题,如果只从数值变化的角度出发,往往难以找到清晰的思路。

不过,异或运算本身具有一些非常特殊的 代数结构性质 。如果一组数的整体异或结果为 xx ,其中某一部分的异或结果为 yy ,那么剩余部分的异或结果一定为 xyx \oplus y 。这一性质来源于异或的抵消性与可逆性:

aa=0,a0=aa \oplus a = 0, \quad a \oplus 0 = a

也就是说,在异或运算中,整体信息可以被自由拆分,任意一部分都可以通过整体结果与另一部分的结果反推出,这使得很多问题可以从 “整体与部分” 的关系入手进行分析。

这一性质在实际问题中有着非常经典的应用。例如,在一个序列中,如果除了某个元素出现奇数次之外,其余元素都出现偶数次,那么对整个序列进行一次异或运算后,所有出现偶数次的元素都会两两抵消,最终只会保留下那个出现奇数次的元素。因此可以在 O(n)O(n) 的时间内直接求出答案。这类问题本质上就是利用了 “整体异或 = 各部分异或之和” 的可分解结构,将复杂的统计过程转化为一次简单的整体运算。

具体讲解可以看一下左神的这个视频

不仅如此,异或在某些特定结构下还会表现出明显的 周期性 。例如定义前缀异或:

f(n)=123nf(n) = 1 \oplus 2 \oplus 3 \oplus \cdots \oplus n

可以得到如下规律:

f(n)={nnmod4=01nmod4=1n+1nmod4=20nmod4=3f(n) = \begin{cases} n & n \bmod 4 = 0 \\ 1 & n \bmod 4 = 1 \\ n+1 & n \bmod 4 = 2 \\ 0 & n \bmod 4 = 3 \end{cases}

也就是说,前缀异或的结果仅与 nmod4n \bmod 4 有关,呈现出周期为 44 的循环结构。对于任意区间 [l,r][l, r] ,都有:

alal+1ar=f(r)f(l1)a_l \oplus a_{l+1} \oplus \cdots \oplus a_r = f(r) \oplus f(l-1)

从而可以在 O(1)O(1) 的时间内完成区间异或的计算。

这种周期性的本质来源于异或是 按位的模 2 加法:每一位只关心出现次数的奇偶性,而在连续整数中,各二进制位的变化具有固定的循环规律,叠加之后就形成了整体的周期性表现。正因为如此,许多看似需要枚举的大规模异或问题,都可以通过发现周期规律或转化为前缀异或来快速求解。

字符串异或变换#

题目链接

题目要点解析#


参考文献列表#

  1. 【Luogu 题单】异或的艺术
【ACM 算法题单】BIT相关问题
https://xingguang641.com/posts/acm/acm-type/math-operators/bit-problem/
作者
星光
发布于
2026-03-03
许可协议
CC BY-NC-SA 4.0