BIT逐位拆解问题
在许多涉及位运算的题目中,一个非常常见的思考方式是 逐位拆解 。由于二进制表示天然具有独立性,一个整数的每一位往往可以单独分析,而不会与其他位产生复杂耦合。因此,在面对位运算问题时,第一步通常就是将问题从 “整数层面” 转化为 “二进制位层面” ,分别分析每一位的贡献。很多看似复杂的题目,一旦拆解到每一位之后,结构往往会变得非常清晰。
位运算问题之所以适合逐位分析,是因为大多数位运算操作都具有 按位独立性 。例如 AND 、OR 、XOR 等运算操作,本质上都是对每一位分别进行计算。因此,如果题目中出现的是这些运算的组合,往往可以将问题拆解为:第 k 位最终会变成什么值 。一旦我们能独立判断每一位的最优结果,就可以把所有位的结果重新组合得到答案。
位运算贪心构造
位运算贪心构造的核心,在于利用二进制权重的指数级分布特性进行确定性决策。这类题目主要分为两种题型。第一种涉及 元素修改或重组操作 ,其求解关键是从动态操作中抽象出静态的 固有贡献 。题目提供的操作往往具有极强的方向性与不可逆性。例如,若操作为将元素 与常数 执行按位与(AND)运算,我们必须敏锐识别出位权空间的 不可触达区域:如果常数 的某位为 ,则该位在操作扰动下将始终保持初始状态,无法被实质性改变。这种由 算子属性 决定的硬性约束,就是构造时必须预先锁定的 底层素材 。
在识别并锁定固有贡献后,接下来的决策逻辑往往紧跟着针对剩余空间的 高位贪心 。由于固有贡献属于无法更改的既定事实,我们应将其视为决策背景,而将贪心策略集中于那些 尚未被约束锁定、仍具备操作自由度 的位信息上。基于 的位权特性,高位 的价值严格大于低位总和。因此,在排除固有贡献的干扰后,我们必须将剩余位资源视为最稀缺资产,通过 高位决策 将其优先分配给能产生最大边际收益的位。这种在自由空间内实现的 位权压制 ,确保了我们在不触碰硬性约束的前提下,实现对全局目标函数上限的精准控制。
第二种题型则涉及 不带修改的操作 ,通常要求从既定数组中选数以构造特定数值,其核心在于应用 高位贪心过滤机制 。由于无法改变元素的内部位结构,决策过程被抽象为一种逐位的 存亡筛选 。以 子集最大 OR 为例,利用其 只增不减 的特性,贪心策略通常趋于全集累加,以纳入所有可能的位贡献。而在处理 子集最大 AND 时,基于其 只减不增 的单调性,决策演变为严密的 排除法:从最高位向低位逐一审视,若当前位为 的元素集合足以支撑目标需求,则果断剔除在该位为 的所有干扰元素。这种单向且不可逆的决策路径,利用高位结果为低位划定了更精确的搜索边界。
然而,当这种基于单调性的高位决策逻辑碰撞上 异或(XOR)运算 时,情况会发生质的变化。子集最大 XOR 问题无法简单地通过贡献重组或存亡过滤来解决。异或运算相同为 且不同为 的 自反特性 ,彻底破坏了数值增长的单调性。引入一个新元素可能会让原本已经确立的高位 瞬间塌陷为 。这种不确定性使得常规的贪心策略失效,迫使我们寻找更深层的 代数结构 来支撑决策。
异或运算的按位贪心
从上面的分析可以知道,异或的贪心构造题往往比较复杂,其核心难点在于处理位与位之间的 相互耦合与干扰 。在常规的 AND 或 OR 运算中,我们对某一位的操作通常不会产生导致高位结果反转的副作用。但在异或空间里,每一个元素的加入都可能引发全局数值的剧烈震荡。为了在缺乏单调性的环境中重新贯彻 高位优先 的贪心原则,我们必须通过 人工构造 的方式,将原本无序且相互制约的原始数据,转化为一套严密的、互不干扰的独立结构。
这种结构化转化的本质是实现 决策解耦 。我们需要确保在确定了最高位的最优解后,后续对低位的任何尝试和优化,都绝对不会对已经固定的高位产生任何负面影响。这种从 依赖数据天然属性 到 人工构建结构化独立性 的思想转变,是解决复杂异或最优化问题的通用范式。通过这种方式,我们将原本处于震荡中的动态决策,转化为一种在稳定结构上的线性搜索,从而在混沌中锁定每一位的高位收益。
1、线性基:子集最大异或和
在处理异或相关的最优化问题时,若直接面对包含 个元素的原始集合 ,其子集异或组合的数量级将达到 。当数据规模较大时,这种指数级的搜索空间会导致计算成本不可接受。为了高效处理这一空间,我们需要寻找一个 极小线性无关组(即基底 )。该基底必须满足两个核心特征:首先是 空间等效性 ,即原集合 能产生的任何异或结果,均可由 的子集异或得到;其次是 线性无关性 ,即 中不存在任何可以通过其他元素组合得到的冗余元素。通过这种方式,我们将复杂的原始数据压缩为了一个规模仅为 的精简集合,从而在根本上降低了后续决策的复杂度。
以集合 为例,通过观察二进制位可以发现, 实际上可以由 组合得到。这意味着在异或空间的维度上, 并没有提供任何超出 范围的新信息。我们的目标是剔除这类 线性相关 的冗余元素,找到能够支撑起相同空间的基底 。然而,在实际计算中,直接从原数组中筛选出特定的子集往往效率低下且逻辑复杂。因此,我们采取 “曲线救国” 的策略:转而寻找一个与原集合 逻辑等价 的集合 。
这里的 “等价” 基于两个核心代数推论:第一,用 的结果替代原集合中的 或 ,不会改变集合所能张成的异或空间;第二,如果集合中存在 ,则说明其中一个元素是多余的,将其舍弃不会对非零异或和的组成产生任何影响。基于此,我们不必执着于保留原数组中的数字,而是可以通过不断的异或变换,构造出一组形态更加标准的等价元素。
为了将上述等价变换转化为可执行的算法,我们引入了 高斯消元 的思想。我们维护一个数组 ,其中 存储最高位在第 位的基底。当新元素 尝试进入线性基时,它会从最高位向低位逐一扫描。如果 的第 位为 且 已经存在,则执行 。这一步利用了替换不变性,旨在消除 在该位上的影响力并继续向下探索。如果 的最终结果被削减为 ,根据冗余舍弃性,我们将其视为无效信息丢弃;若它在某位停下并填补了空缺,则它成为了该位新的掌控者。
这种通过消元得到的阶梯化集合 具有极佳的 贪心特性 。由于每个基底 的最高位被严格限制在第 位,且其高位全部为 ,这就消除了不同元素之间的位权干扰。在求解最大异或和时,我们可以维护一个变量 ,并从高到低执行状态转移:
这一步的数学依据非常严谨:如果 的第 位已经是 ,异或 必然会使该位变为 ,由于高位已经确定,这一步操作一定会让数值减小;反之,如果 的第 位是 ,异或 之后第 位变为 ,无论低位如何变化,数值一定会增大。这种贪心策略之所以成立,本质上是因为二进制权值具有绝对的 高位压制特性:
这意味着在任何时刻,保住更高位的 永远比优化后面所有位产生的总和更具价值。
2、字典树:点对最大异或和
在处理 “两数异或” 问题时,字典树(01-Trie)是实现按位贪心策略的结构化利器。不同于线性基通过改变数值形态来压缩空间,01-Trie 完整地保留了原始数据的位信息,并将其映射到一棵深度固定的二叉树中。当我们需要在集合中找到一个数 使得它与给定的 异或结果最大时,其本质是在这棵代表数值空间的决策树上,进行一场基于高位优先原则的动态路径搜索。
01-Trie 将集合中所有数的二进制表示看作从根节点出发的路径。从最高位(通常是第 位或 位)到最低位,左子树代表 ,右子树代表 。这种结构将原本无序的数值集合转化为了一棵深度为 的树,使得每一条从根到叶子的路径都唯一对应一个原始数值。
在查询过程中,我们利用异或运算 “不同为 ” 的特性,对 的二进制位从高到低进行扫描。对于 的第 位,如果该位的值为 ,我们的核心贪心策略是:只要条件允许,永远优先向 的分支移动 。这种决策具有即时性和不可逆性:如果在当前层存在与 某位相反的路径,我们必须选择该路径。因为即使这条路径会导致后续低位全部被迫选择相同位(异或结果为 ),高位贡献的 依然在数值上绝对优于低位所有贡献的总和。
假设当前处理到第 位,我们期望寻找的分支是 的取反值。如果该分支存在,异或结果在这一位将产生确定的贡献:
此时,查询指针移动至对立分支,并继续向下递归。若该分支不存在,我们则被迫走向与 当前位相同的分支,此时该位的异或贡献为 。通过这种从高到低的逐位锁定,我们最终能在 的时间内精准定位到那个能产出最大火花的配对数。
这种模型在处理 “最大异或子数组” 问题时展现了卓越的转化能力。根据异或运算的自反性( ),任意区间 的异或和可以转化为两个前缀异或和的异或:
通过这一性质,求 “子数组最大异或和” 的问题便等价于 “在所有的前缀异或和中找到一对异或值最大的点” 。我们将所有 依次插入 01-Trie,每插入一个新前缀,就在树中查询与其配对的最优历史前缀。这种方法将原本 的暴力枚举优化至 ,实现了时间复杂度与数值范围的完美平衡。
最大异或乘积
Problem Statement
给你三个整数 a 、b 和 n ,要求你从所有满足 的整数 中选择一个,使得表达式
的值最大。其中按位异或运算 是对两个整数的二进制位逐位运算,当对应位不同时结果为 ,相同为 。
由于结果可能非常大,请你将最终的最大值对 取余后返回。
Constraints
- 所有输入值均为整数
Input
输入包含一行:
Output
输出一个整数,表示满足条件的最大值 对 取余后的结果。
Sample Input 1
12 5 4Sample Output 1
98Sample Input 2
6 7 5Sample Output 2
930Sample Input 3
1 6 3Sample Output 3
12题目要点解析
本题要求在 的范围内选择一个整数 ,使得 取得最大值。由于 仅影响最低 位,因此问题天然适合采用逐位分析的方法。对于位运算题目而言,常见的处理方式是拆位分析 ,因为按位运算本质上是逐位独立定义的。当我们决定采用拆位思路时,首先应当思考是否可以通过贪心提前确定某些位的操作,从而将问题规模逐步缩减。这种 “先确认显然最优的局部决策” 的思考方式,往往能够显著简化整体结构。
我们引入两个变量:
问题等价于最大化:
为了理解如何选择 ,我们从每一位出发分析 与 的关系:
-
当某一位满足 时,如果令 ,则该位在 与 中都会变为 ;若令 ,则保持为 。显然,同时减少两个 会降低乘积,因此这一位的最优策略是保持不变,即令 。
-
当某一位满足 时,如果令 ,则该位在 与 中都会变为 ;若令 ,则保持为 。显然,同时增加两个 会提升乘积,因此这一位的最优策略是保持不变,即令 。
经过上述两类情况的分析后,可以发现这些位的贡献已经完全确定,它们构成了一个不可改变的 “固定部分” 。真正影响优化结构的,是那些满足 的位。在这些不同位上,若某位原本是 ,则当 时保持原状,当 时二者交换。因此,在这些位上, 的作用本质上是决定该位权值分配给 还是 。需要注意的是,无论如何分配,该位的权值 总是存在于两者之一,因此这些位上的总权值是固定的,只是可以在两数之间重新分配。
设固定部分分别为 与 ,所有不同位的权值总和为 。则可以写成:
其中 表示分配给 的那部分权值。于是乘积为:
这是一个关于 的二次函数。将其展开可以得到负的二次项系数,因此函数开口向下,存在唯一最大值。根据二次函数的基本性质可知,在总资源 固定的情况下,当两数尽可能接近时,乘积达到最大。因此,最优策略是在可交换位上尽量平衡两数的大小。由于二进制数中高位权重大于低位,在具体实现时应当从高位到低位进行贪心决策,每当遇到一个可以分配的位,就优先将其分配给当前数值较小的一方,从而保持两数尽可能接近。
# include <bits/stdc++.h>using namespace std;
int main(){
}数组末尾最小值
Problem Statement
给你两个整数 n 和 x 。你需要构造一个长度为 的正整数数组 nums ,满足:
- 对所有 ,都有
nums[i + 1] > nums[i]; - 数组
nums中所有元素的按位 AND 运算结果等于 ;
请返回数组 nums 的最后一个元素 nums[n - 1] 的 最小可能值 。
Constraints
- 输出的最后一个元素必为正整数
- 所有输入均为整数
Input
输入包含一行:
Output
输出一个整数,表示满足条件的数组 nums 的最小可能的最后一个元素 nums[n - 1] 。
Sample Input 1
3 4Sample Output 1
6Sample Input 2
2 7Sample Output 2
15题目要点解析
本题依旧是一道典型的位运算构造问题,因此自然应当从拆位的角度进行分析。对于位运算题目,我们通常优先考虑逐位拆解,并结合贪心思想,尽可能先确定那些可以直接确定的 “固定贡献” ,从而将问题转化为剩余部分的结构优化。
题目要求构造一个严格递增的正整数数组 nums ,并满足所有元素的按位与结果等于 。设数组为:
并要求:
按位与运算的性质决定了:某一位最终为 ,当且仅当 所有数在该位上都为 。因此,如果 的某一位是 ,那么数组中每一个元素在这一位上都必须是 。这些位是完全被强制确定的,不存在任何选择空间。接下来考虑 中为 的位。对于这些位,只需保证在所有元素中至少有一个数在该位上为 ,即可保证最终按位与结果为 。但由于我们希望数组严格递增且最后一个数尽量小,因此最优策略是尽量以最小幅度递增构造数组。
关键观察在于,既然所有数都必须包含 的 位,那么数组中的每个元素都可以写成:
其中 只在 为 的位上可以自由取值。于是问题转化为:构造一个严格递增的序列 ,并使得最终的 最小。
为了让 尽可能小,就应当让 本身尽可能小且按最小步长递增。显然,最优构造方式是令:
但需要注意的是,这里的 不是直接填入原数,而是将 的二进制表示嵌入到 为 的那些位中。也就是说,我们将自然数序列 的二进制表示依次填入 为 的位置上,从低位到高位对应填充。
这样构造得到的序列天然严格递增,同时保证所有元素都包含 的固定 位,因此按位与结果恰好为 。由于我们使用的是从 开始的最小连续整数序列,因此最终得到的 也是所有合法构造中的最小值。
# include <bits/stdc++.h>using namespace std;
int main(){
}平方和最大操作
Problem Statement
给你一个 0-索引的整数数组 nums 和一个正整数 k。你可以对数组执行如下操作任意次:
-
从数组中任选两个不同的下标
i和j,同时将nums[i]更新为(nums[i] AND nums[j])nums[j]更新为(nums[i] OR nums[j])
其中 AND 和 OR 分别表示 按位与 和 按位或 操作。
你可以在执行若干次(或不执行)操作之后,从最终的数组中选出 k 个元素 并计算它们的平方和。请你返回最大可能的平方和,由于结果可能很大,返回值对 取余。
Constraints
- 所有输入均为整数
Input
输入包含两行:
- 第一行包含两个整数 和 ,其中 表示数组长度, 表示选取个数。
- 第二行包含 个整数,表示数组的每个元素。
Output
输出一个整数,表示在执行任意合法操作后,选出 个元素的最大平方和,并对 取余。
Sample Input 1
4 22 6 5 8Sample Output 1
261Sample Input 2
4 34 5 4 7Sample Output 2
90题目要点解析
这道题依然是一类典型的位运算重排问题,因此自然应当从拆位分析入手。对于涉及 AND、OR 之类按位操作的题目,我们通常优先逐位研究其变化规律,寻找是否存在守恒量或固定贡献。一旦发现某种 “不可改变的结构” ,问题往往就会从复杂的操作模拟,转化为更本质的资源分配问题。
题目给定的操作为:任选两个数 ,执行:
为了理解其本质,我们只需考察单个位上的变化情况。设某一位上两数分别为 。如果是 或 ,显然操作后不发生变化;如果是 ,按位与得到 ,按位或得到 ,结果仍为 ;只有在 时,会被调整为 。
从这四种情况可以看出一个核心事实:每一位上的 的总数量保持不变。操作只是在不同元素之间 “转移” 该位上的 ,而不会创造或消灭 。因此,设第 位上最初共有 个 ,那么无论如何操作,最终这一位上仍然有且仅有 个 。这一步实际上已经揭示了问题的本质:我们可以在数组元素之间自由重新分配每一位上的 ,但各个位的 的总数是守恒的。于是原问题等价于——在每一位上拥有 个权值为 的资源,可以任意分配到各个元素中。
接下来考虑目标函数。我们最终要从数组中选出 个元素,使得它们的平方和最大:
平方函数是严格凸函数,这意味着在总和一定的情况下,将数值集中在少数元素上,比平均分配能获得更大的平方和,也就是:
因此,既然每一位上的贡献可以自由分配,那么最优策略必然是尽可能把各个位上的 叠加到同一批元素上,从而构造出尽可能大的数。具体实现时,可以按如下贪心思路进行:统计所有 ,然后重复 轮构造。每一轮构造一个新的数,对于所有满足 的位,将对应的 加入当前数,并令:
这样得到的第一个数最大,第二个数次大,以此类推。由于每一轮都尽可能收集所有剩余的高位贡献,数值会呈现逐层递减的结构,而平方和则因高度集中而被最大化。
# include <bits/stdc++.h>using namespace std;
int main(){
}最大异或和操作
Problem Statement
给你一个 0-索引 的整数数组 nums 。在一次操作中,你可以选择任意一个 非负整数 x 和一个下标 i ,然后将 nums[i] 更新为:
其中 AND 是按位与运算,XOR 是按位异或运算。
请返回在执行任意次数(包括 0 次)操作后,数组中所有元素按位异或(XOR)之后能获得的 最大可能值 。
Constraints
- 所有输入均为整数
Input
输入包含两行:
- 第一行包含一个整数 表示数组长度。
- 第二行包含 个整数表示数组的元素。
Output
输出一个整数,表示在经过任意次数操作后,数组所有元素按位 XOR 的最大可能值。
Sample Input 1
3 2 4 6Sample Output 1
7Sample Input 2
1 2 3 9 2Sample Output 2
11题目要点解析
这道题的关键在于先理解给定操作到底能做什么。题目允许我们进行如下操作:
注意到最外层是一个按位与运算,左边的 nums[i] 本身就相当于一个 “掩码” 。这意味着,无论我们怎样选择 ,运算结果都不可能在原本为 的位上变成 。换句话说,这个操作只能把某些 变成 ,而不能凭空增加新的 。再看中间的异或部分。由于 可以是任意非负整数,因此 nums[i] ^ x 可以在每一位上自由控制翻转与否。结合外层的按位与,可以发现我们实际上可以 “选择性地关闭” nums[i] 中的任意若干个 。也就是说,经过若干次操作之后,nums[i] 可以变成它的任意一个子集,但绝对不能增加新的 。
因此,本题的本质就变成了:我们可以对每个元素,删除它的一些 位,但不能新增 位。最终目标是让整个数组的异或值最大。
接下来从异或的角度分析。一个整数的异或结果在某一位上为 ,当且仅当该位上所有元素中 的个数为奇数。既然我们可以删除某些 ,那么在每一位上,我们唯一能做的事情就是减少 的数量。为了让最终异或结果最大,我们希望在尽可能高的位上得到 。而某一位能否最终为 ,取决于这一位在所有元素中是否 “至少出现过一次” 。如果某一位在原数组中从未出现过 ,那么由于我们不能增加 ,这一位最终必然为 。
而如果某一位原本出现过至少一次 ,那么我们就可以通过删除多余的 ,使这一位的 的总数变成奇数,从而保证最终异或结果在这一位为 。因为只要这一位原本有至少一个 ,我们就可以保留一个、删除其余全部,使其数量为 (奇数)。因此,对于每一位,只要原数组中存在至少一个元素在该位为 ,我们就一定可以让最终异或结果在这一位为 ;如果原本没有,则永远无法得到 。
这说明最终答案恰好等于所有元素的按位或结果:
因为按位或正好表示 “每一位是否至少出现过一次 1” 。
综上所述,这道题虽然操作形式复杂,但本质上只是一个位运算性质的转化问题。操作只能删除 ,不能增加 ;而异或最大化只关心每一位是否能保留奇数个 。最终答案就是原数组的按位或值,时间复杂度为 。
# include <bits/stdc++.h>using namespace std;
int main(){
}最短or路径问题
Problem Statement
给定一个由 到 编号的无向连通图,共有 个顶点和 条边。图中不包含自环,但可能包含重边。第 条边连接顶点 和顶点 ,并带有一个非负整数权值 。
对于任意一条从顶点 到顶点 的 简单路径(不重复经过同一顶点),定义该路径的代价为路径上所有边权的按位 OR 运算结果,即:
其中每个 是路径所包含的边的权值。
请找出所有从顶点 到顶点 的简单路径中,使上述 OR 结果最小的那个值,并输出该最小值。
Constraints
Input
输入格式如下:
Output
输出一个整数,表示从顶点 到顶点 的所有简单路径中,按位 OR 运算结果的最小可能值。
Sample Input 1
4 51 2 11 3 42 3 22 4 43 4 3Sample Output 1
3Sample Input 2
3 51 2 11 2 21 2 31 2 42 3 4Sample Output 2
4Sample Input 3
8 124 5 166913445 7 1296424412 7 7892754473 8 3353076513 5 5301633335 6 8112937733 8 3337127011 2 29099412 3 1602654785 7 4654142721 3 9033730046 7 408299562Sample Output 3
468549631题目要点解析
或位运算
异或数列博弈论
题目要点解析
异或位运算
最大异或对构造
题目要点解析
异或位运算
位运算和式变换
在一些涉及 位运算的求和问题 中,直接按照题意枚举所有元素往往是不可行的,因为数据范围通常非常大。这时,一个非常常见的技巧是对表达式进行 和式变换(sum transformation):将原本关于整数的运算,转化为 按位统计贡献 的形式。
其核心思想来源于位运算的 按位独立性 。例如在计算大量异或、与、或运算的和时,可以将问题拆解为:每一位对最终答案的贡献 。对于某一位 ,只需要统计在所有数中这一位为 或 的数量,然后计算能够使该位为 的组合数量,再乘上对应的权值 。这样就把一个复杂的双重求和问题,转化为对每一位进行一次简单计数。
例如在求解下面这个累加和:
我们可以对每一位 单独计算贡献。如果 中该位为 的数量为 ,为 的数量为 ,而 中该位为 的数量为 ,为 的数量为 ,那么只有 0 与 1 配对 才会使该位异或结果为 ,因此该位的贡献数量为:
通过这种方式,原本需要枚举 个配对的问题,可以被转化为 按位统计 + 数学计数 的过程,复杂度通常只需要 或 。这种思路在许多位运算求和问题中都非常常见,是逐位拆解方法的重要应用场景。从更深层次来看,这种方法之所以成立,尤其在异或问题中表现突出,是因为 异或运算本质上等价于二进制下的模 2 加法(无进位加法)。在每一位上,其运算规则为:
因此,可以把整数看作由 组成的二进制向量,而异或运算就是对这些向量进行 逐位的模 2 加法 。由于不存在进位,不同二进制位之间完全独立,这也使得 “按位统计贡献” 的和式变换方法在异或问题中具有天然优势。
在其他类似的问题中,我们还可以将 “寻找满足某种异或关系的数对” 转化为寻找对应的 目标值 。这种结构与经典的两数之和思想非常相似:在遍历数组时,可以用哈希表记录已经出现过的元素,并查询当前元素对应的目标值 是否存在。如果存在,我们就找到了一组满足条件的数对。
数组异或序列和
题目要点解析
子段和的异或和
题目要点解析
BIT性质分析问题
除了常见的逐位拆解思路之外,还有一类位运算问题并不适合直接按位分析,而是需要通过挖掘运算本身的 特殊性质 来解决。这类题目的关键往往不在于逐位枚举,而在于观察某些操作在整体结构上的变化规律。一旦发现这些规律,问题往往可以被大幅简化,甚至转化为一个完全不同类型的模型。
因此,在处理位运算问题时,除了考虑逐位分析之外,也可以尝试从 运算性质 的角度进行思考。例如某些运算具有单调性、可逆性或抵消性质,这些结构特征往往会对状态变化产生强约束。通过利用这些性质,可以快速排除大量不可能的情况,从而显著降低问题的复杂度。
AOR相关性质
按位与和按位或运算都具有非常明显的 单调结构 。按位与运算会使数值 单调不增:某一位一旦在某次运算中变为 ,之后再进行与运算时就不可能恢复为 。因此,在只包含按位与操作的过程中,整体数值只会不断减小,很多较大的状态从一开始就不可能被达到。这一性质在区间问题、动态维护以及状态压缩中经常被利用,例如区间按位与的不同结果数量通常是非常有限的。
与之相对,按位或运算则具有 单调不减 的特征:某一位一旦变为 ,之后通常无法再恢复为 。因此,在只包含按位或操作的过程中,数值会不断增大,状态变化也会逐渐趋于稳定。利用这一性质,可以快速判断某些状态是否可达,并且在许多问题中可以证明不同结果的数量是受限的,从而避免枚举所有可能的状态。
此外,按位与和按位或还具有 幂等性 ,即:
这意味着重复执行同样的操作不会产生新的结果。在某些操作序列问题中,这种性质会导致过程在有限步之后 收敛到稳定状态 ,从而使得原本需要大量模拟的过程可以被显著简化。
多数目子数组
Problem Statement
给你一个由非负整数组成的数组 nums 。我们定义子数组 nums[l..r] 的 得分 为:
其中 表示按位 与 操作。
现在你需要将数组分割成若干个 连续子数组 ,满足:
- 数组中的每个元素恰好属于一个子数组;
- 所有子数组得分之和 最小 。
在满足上述条件的前提下,返回能构造出的子数组 最多个数 。
这里的子数组必须是连续的数组片段。
Constraints
- 数组
nums中的所有元素均为整数
Input
输入包含两行:
- 第一行包含一个整数 ,表示数组的长度。
- 第二行包含 个整数,表示数组的元素。
Output
输出一个整数,表示在满足最小得分和的前提下,能够划分出的最大子数组数量。
Sample Input 1
1 0 2 0 1 2Sample Output 1
3Sample Input 2
5 7 1 3Sample Output 2
1题目要点解析
这道题的核心在于理解按位与运算的单调性质。对于一段区间而言,随着参与按位与的元素个数增加,结果只会保持不变或变小,而不可能变大。换句话说,区间越长,其按位与的结果越不利于增大总和。因此,在考虑如何划分子数组时,应当先分析整个数组的整体按位与结果。
设整个数组的按位与为:
显然,任意子数组的按位与结果都不会小于 。因为整体按位与已经包含了所有元素的约束,是能够达到的最小值。也就是说,所有合法划分的子数组得分之和,至少为若干个不小于 的数之和。
如果 ,那么问题立刻变得简单。因为无论如何划分,每个子数组的按位与结果都不小于 ,一旦拆分成多个子数组,得分之和就至少为:
显然会严格大于单个整体区间的得分 。既然题目要求总得分最小,那么此时最优策略只能是不拆分数组,即整个数组作为一个子数组,答案为 。
接下来考虑 的情况。既然整体按位与已经为 ,说明存在某些位在不同元素之间互相 “抵消” ,最终使得整段区间的按位与结果为零。由于按位与运算是单调递减的,我们希望尽可能多地构造出按位与为 的子数组。因为一旦某个子数组的得分为 ,它对总和的贡献就是最小的。
因此,在 的情况下,我们的目标转化为:将数组划分为尽可能多的连续区间,使得每个区间的按位与结果为 。这样总得分仍然为 ,同时子数组数量最多。具体实现时,可以从左到右维护一个当前区间的按位与值,初始设为一个全 的掩码或直接从第一个元素开始累积。每次将当前元素与进来,当结果变为 时,就立即在此处切断,计数加一,并重新开始下一段区间的累积。由于按位与只会变小,一旦为 就不可能再恢复,因此此时切分是最优且贪心正确的。
综上,本题的关键在于先计算整体按位与,分情况讨论。当整体按位与大于 时答案为 ;当整体按位与为 时,通过贪心切分每一个前缀按位与首次变为 的位置,可以得到最多的子数组数量。这种思路的本质是利用按位与的单调性,将问题转化为 “尽可能多地制造零贡献区间” 的结构化贪心问题。
# include <bits/stdc++.h>using namespace std;
int main(){
}按位或最大数组
Problem Statement
给你一个长度为 的 0-索引 数组 nums ,数组由非负整数组成。对于每个下标 ( ),你需要找出从位置 开始的 最短非空子数组 ,使得这个子数组的 按位 OR 运算值 是从位置 到末尾所有可能子数组中所能获得的 最大 OR 值 。
换句话说,记 为子数组 nums[i..j] 的按位 OR 值,你要找出最小的 满足:
按位 OR 运算定义为:对二进制数的每一位进行 OR,如果任一输入在该位是 ,则输出为 。
请你返回一个整数数组 answer ,长度为 ,其中 answer[i] 是从位置 开始,取得最大按位 OR 值所需的最短子数组长度。子数组必须是连续的非空片段。
Constraints
- 所有输入均为整数
Input
输入包含两行:
- 第一行包含一个整数 ,表示数组的长度。
- 第二行包含 个整数,表示数组的元素。
Output
输出一行整数数组 answer ,其中 answer[i] 表示从下标 开始的最短子数组长度,该子数组的按位 OR 值等于从 出发能达到的最大按位 OR 值。
Sample Input 1
1 0 2 1 3Sample Output 1
3 3 2 2 1Sample Input 2
1 2Sample Output 2
2 1题目要点解析
这道题如果换成 “子数组最大累加和” ,我们往往只需要维护某种前缀或后缀的最优结构即可,因为加法具有可逆性和线性结构。但按位或运算并不具备这种性质,它是不可逆的,因此必须从按位或本身的结构特征入手分析。
首先注意按位或的单调性,设:
当我们固定 ,不断增大 时, 只可能变大或保持不变,不可能变小。因为某一位一旦在或运算中被置为 ,之后就不可能再变回 。因此对于每个起点 ,所谓 “最大按位或值” ,实际上就是扩展到数组末尾得到的值:
问题就转化为:对于每个 ,找到最小的 ,使得:
如果从每个 暴力向右扩展,时间复杂度会达到 。但按位或还有一个更关键的性质:由于整数最多只有 位,而每一位最多只会从 变成 一次,因此对于固定起点 ,随着 的增加, 的不同取值不超过 种。
换一个角度来看,我们从右向左动态维护 “所有后缀按位或结果” 。设当前处理到位置 ,我们知道从 开始的所有不同的后缀或值。将 与这些值分别进行一次或运算,再加上单独的 ,就得到了从 开始的所有后缀或值。
这里有一个非常重要的细节:后缀按位或值在扩展过程中是单调不减的,因此生成的新或值序列也是单调的。由于单调性,相同的或值一定是连续出现的。换句话说,若在更新过程中产生了重复值,那么这些重复值必然是相邻的,可以在构造时直接去重。
因此在实现时,我们只需维护一个有序数组(或列表)表示不同的后缀或值。每次更新时按顺序计算新的或值序列,并在生成过程中去掉与前一个相同的值即可。由于总状态数不超过 个,因此每一步的复杂度是常数级的。
在得到从 开始的所有不同后缀或值后,最大值就是该列表中的最后一个元素。由于我们在维护时可以同时记录每个或值最远延伸到的下标,因此可以直接得到最短的 ,从而计算答案长度。
# include <bits/stdc++.h>using namespace std;
int main(){
}题目相关拓展
如果把本题中的 “最大 OR 值” 改成与 “最大 XOR 值” ,那么问题结构会发生明显变化。设我们定义后缀异或和:
则任意区间 的异或值可以表示为:
因此,如果我们希望在固定起点 的情况下,使某个后缀子区间的异或值最大,本质上就是在所有可能的 中,找到一个值,使得 最大。这就转化成了一个经典问题:给定一个数,在一组数中找到与它异或结果最大的数 。这正是 “最大异或对” 问题。
解决这类问题的标准做法是使用二进制字典树(Trie)。具体思路如下:
我们从右向左遍历数组,动态计算后缀异或和 。在处理到位置 时,Trie 中已经存储了所有 (其中 )。此时我们在 Trie 中查询一个数,使得与当前 异或的结果最大。Trie 的构造方式是按二进制位从高位到低位插入。查询时,为了让异或结果最大,我们在每一位上尽量选择与当前位相反的分支(若存在)。这样贪心地从高位优先保证结果尽可能大,最终得到全局最优解。
由于整数最多 位,每次插入和查询的复杂度都是 ,整体时间复杂度为 ,可以视为线性。
最长优雅子数组
Problem Statement
给你一个由正整数组成的数组 nums 。当一个子数组中任意两个 不同位置 的元素按位与(AND)运算结果为 0 时,我们称这个子数组为 优雅子数组(nice subarray)。按位与运算定义为:对于两个整数,对各个二进制位进行 AND 操作,当对应位都为 1 时该位结果为 1 ,否则为 0 。
请你返回数组 nums 中 最长的优雅子数组的长度 。
子数组指数组中的一个连续部分。注意,长度为 1 的子数组总是优雅的 。
Constraints
- 所有输入均为整数
Input
输入包含两行:
- 第一行包含一个整数 表示数组长度。
- 第二行包含 个整数表示数组的元素。
Output
输出一个整数,表示数组中最长的优雅子数组的长度。
Sample Input 1
1 3 8 48 10Sample Output 1
3Sample Input 2
3 1 5 11 13Sample Output 2
1题目要点解析
题目要求子数组中任意两个不同位置的元素按位与结果为 。等价地说,任意两个数在二进制表示中不能在同一位上同时为 。也就是说,这个子数组中的所有数字,在二进制层面上 的位置必须两两互不重叠。
从这个条件可以立刻得到一个重要结论:一个优雅子数组的长度不会超过二进制位数上限。因为一个整数最多只有 位(在本题数据范围内甚至不到 位),而每一位最多只能被一个元素占用。如果两个元素在同一位上都有 ,那么它们的按位与一定不为 ,子数组就不合法。因此,一个优雅子数组的最大长度最多是 ,也就是 “每个数只贡献一个不同的 位” 的极端情况。
这个上界非常关键。数组长度可以达到 ,但真正需要考虑的窗口长度最多只有 。也就是说,对于每个起点,向右最多扩展 步就可以确定该起点能形成的最长优雅子数组。最直接的做法是枚举左端点 ,然后从 开始向右扩展窗口,维护当前窗口内所有元素的按位或值 mask 。当我们尝试加入一个新元素 nums[j] 时,只需要检查:
如果成立,说明没有任何位冲突,可以加入窗口,并更新:
一旦不成立,就停止扩展当前起点。由于窗口长度最多 ,每个起点最多检查 次,总时间复杂度为 ,可以视为线性复杂度。
当然,也可以用更标准的滑动窗口写法。用双指针维护一个区间 [l, r) ,同时维护当前窗口的按位或值 mask 。当加入 nums[r] 时,如果产生冲突( mask & nums[r] != 0 ),就不断移动左指针,并把 nums[l] 从 mask 中删除。由于窗口中每一位只会出现一次,删除操作可以通过下面这个公式来完成:
这样每个元素最多进窗口一次、出窗口一次,总复杂度同样是线性。
# include <bits/stdc++.h>using namespace std;
int main(){
}XOR相关性质
与按位与、按位或不同,按位异或运算并不具备明显的单调性,因此很多问题往往无法通过简单的 “不断变大” 或 “不断变小” 的规律来进行分析。许多复杂的异或问题,如果只从数值变化的角度出发,往往难以找到清晰的思路。
不过,异或运算本身具有一些非常特殊的 代数结构性质 。如果一组数的整体异或结果为 ,其中某一部分的异或结果为 ,那么剩余部分的异或结果一定为 。这一性质来源于异或的抵消性与可逆性:
也就是说,在异或运算中,整体信息可以被自由拆分,任意一部分都可以通过整体结果与另一部分的结果反推出,这使得很多问题可以从 “整体与部分” 的关系入手进行分析。
这一性质在实际问题中有着非常经典的应用。例如,在一个序列中,如果除了某个元素出现奇数次之外,其余元素都出现偶数次,那么对整个序列进行一次异或运算后,所有出现偶数次的元素都会两两抵消,最终只会保留下那个出现奇数次的元素。因此可以在 的时间内直接求出答案。这类问题本质上就是利用了 “整体异或 = 各部分异或之和” 的可分解结构,将复杂的统计过程转化为一次简单的整体运算。
具体讲解可以看一下左神的这个视频
不仅如此,异或在某些特定结构下还会表现出明显的 周期性 。例如定义前缀异或:
可以得到如下规律:
也就是说,前缀异或的结果仅与 有关,呈现出周期为 的循环结构。对于任意区间 ,都有:
从而可以在 的时间内完成区间异或的计算。
这种周期性的本质来源于异或是 按位的模 2 加法:每一位只关心出现次数的奇偶性,而在连续整数中,各二进制位的变化具有固定的循环规律,叠加之后就形成了整体的周期性表现。正因为如此,许多看似需要枚举的大规模异或问题,都可以通过发现周期规律或转化为前缀异或来快速求解。