本文参考自灵神的相关题解:灵茶山艾府 - 力扣(LeetCode) 以及 LogTrick 方法解决子数组最值或计数问题 | 半方池水半方田
Logtrick 是在时间复杂度为 O(n2) 计算子数组的问题中,利用|、&、gcd、lcm等操作的性质进行优化的一种算法,通常用于求子数组经过一些操作(|、&、gcd、lcm)后的最值或计数问题。
或运算
给你一个长度为 n 下标从 0 开始的数组 nums ,数组中所有数字均为非负整数。对于 0 到 n - 1 之间的每一个下标 i ,你需要找出 nums 中一个 最小 非空子数组,它的起始位置为 i (包含这个位置),同时有 最大 的 按位或运算值 。
换言之,令 B_ij 表示子数组 nums[i...j] 的按位或运算的结果,你需要找到一个起始位置为 i 的最小子数组,这个子数组的按位或运算的结果等于 max(B_ik) ,其中 i <= k <= n - 1 。
一个数组的按位或运算值是这个数组里所有数字按位或运算的结果。
请你返回一个大小为 n 的整数数组 answer,其中 answer[i]是开始位置为 i ,按位或运算结果最大,且 最短 子数组的长度。
子数组 是数组里一段连续非空元素组成的序列。
数据范围:
n == nums.length
1 <= n <= 10^5
0 <= nums[i] <= 10^9
暴力做法
有一个 O(n2) 暴力做法:从左到右正向遍历 nums,对于 x=nums[i],从 i−1 开始倒着遍历 nums[j],更新 nums[j]=nums[j] ∣ x,如果 nums[j] 变大,则更新 ans[j]=i−j+1。
class Solution {
public int[] smallestSubarrays(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, 1);
for(int i = 0; i < n; i++){
for(int j = i - 1; j >= 0; j--){
int tmp = nums[j];
nums[j] |= nums[i];
if(nums[j] > tmp){
ans[j] = i - j + 1;
}
}
}
return ans;
}
}
该方法的主要思想是把子数组按位或的结果保存在 nums 中。
Logtrick
解法一
对于或运算来说,两个数字按位或的结果只会变大或者不变,而不会变小。因为对于某个二进制比特位,该位上的数字只会由 0 变成 1 ,而不会由 1 变成 0。
那么,我们来思考一个问题:对于起始位置为 i 的子数组的按位或的结果,至多有多少种不同的结果?
根据或运算的性质,我们可以从 x=nums[i] 开始,不断往右进行按位或,由于每次按位或的结果要么使 x 不变,要么使 x 的某些比特位由 0 变成 1。最坏情况下,从 x=0 出发,每次改变一个比特位,最多有 33 种不同的结果。
除了上面的结论,我们还可以得到另一个重要的结论:相同的按位或对应的子数组的右端点会形成一个连续的区间。(因为对于以 i 为左端点的子数组,随着右端点的不断增大,按位或的结果是不可能减小的。)
class Solution {
public int[] smallestSubarrays(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
List<int[]> ors = new ArrayList<>();
for(int i = n - 1; i >= 0; i--){
ors.add(new int[]{0, i});
int k = 0;
for(int[] or : ors){
or[0] |= nums[i];
if(ors.get(k)[0] == or[0]){
ors.get(k)[1] = or[1];
}else{
ors.set(++k, or);
}
}
ors.subList(k + 1, ors.size()).clear();
ans[i] = ors.get(0)[1] - i + 1;
}
return ans;
}
}
时间复杂度:O(nlongU) ,其中 n 为 nums 的长度,U=max(nums) 。
解法二
我们可以把二进制数看成集合,二进制数第 i 位为 1 表示 i 在集合种。两个二进制数的或,就可以看成是两个集合的并集。
对于两个二进制数 a 和 b 。如果 a∣b=a ,从集合的角度上看,b 对应的集合是 a 对应的集合的子集。
因此我们按照如下算法进行:
从左往右正向遍历 nums ,对于 x=nums[i] ,从 i−1 开始倒着遍历 nums[j] :
- 如果 nums[j]∣x=nums[j] ,说明 nums[j] 可以变大(集合元素增多),更新 nums[j]=nums[j]∣x ;
- 如果 nums[j]∣x=nums[j] ,从集合的角度来看,此时 x 不仅是 nums[j] 的子集,同时也是 nums[k](k<j) 的子集(因为循环保证了每个集合都是其左侧相邻集合的子集),那么后续的循环都无法让元素变大,退出循环。
- 在循环中,如果 nums[j] 可以变大,则更新 ans[j]=i−j+1 。
class Solution {
public int[] smallestSubarrays(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, 1);
for(int i = 0; i < n; i++){
for(int j = i - 1; j >= 0; j--){
if((nums[j] | nums[i]) == nums[j]){
break;
}
nums[j] |= nums[i];
ans[j] = i - j + 1;
}
}
return ans;
}
}
时间复杂度:O(nlongU) ,其中 n 为 nums 的长度,U=max(nums) 。
给定一个整数数组 arr,返回所有 arr 的非空子数组的不同按位或的数量。
子数组的按位或是子数组中每个整数的按位或。含有一个整数的子数组的按位或就是该整数。
子数组 是数组内连续的非空元素序列。
数据范围:
1 <= nums.length <= 5 * 10^4
0 <= nums[i] <= 10^9
解法一
与上一题的解法一类似,我们可以利用或运算的性质,与上一题不同的是,这题只需统计或运算的结果的个数,而不需要记录长度,因此我们可以直接用 Set 来存储或运算的结果。
class Solution {
public int subarrayBitwiseORs(int[] arr) {
int n = arr.length;
Set<Integer> ans = new HashSet<>();
Set<Integer> ors = new HashSet<>();
for(int i = 0; i < n; i++){
Set<Integer> tmp = new HashSet<>();
tmp.add(arr[i]);
for(int or : ors){
tmp.add(or | arr[i]);
}
ans.addAll(tmp);
ors = tmp;
}
return ans.size();
}
}
解法二
从集合的角度思考。
class Solution {
public int subarrayBitwiseORs(int[] arr) {
int n = arr.length;
Set<Integer> ans = new HashSet<>();
for(int i = 0; i < n; i++){
ans.add(arr[i]);
for(int j = i - 1; j >= 0; j--){
if((arr[j] | arr[i]) == arr[j]){
break;
}
arr[j] |= arr[i];
ans.add(arr[j]);
}
}
return ans.size();
}
}
与运算
func(arr, l, r) {
if (r < l) {
return -1000000000
}
ans = arr[l]
for (i = l + 1; i <= r; i++) {
ans = ans & arr[i]
}
return ans
}
Winston 构造了一个如上所示的函数 func 。他有一个整数数组 arr 和一个整数 target ,他想找到让 |func(arr, l, r) - target| 最小的 l 和 r 。
请你返回 |func(arr, l, r) - target| 的最小值。
请注意, func 的输入参数 l 和 r 需要满足 0 <= l, r < arr.length 。
数据范围:
1 <= arr.length <= 10^5
1 <= arr[i] <= 10^6
0 <= target <= 10^7
我们可以把二进制数看成集合,二进制数第 i 位为 1 表示 i 在集合种。两个二进制数的或,就可以看成是两个集合的交集。
对于两个二进制数 a 和 b 。如果 a&b=a ,从集合的角度上看,a 对应的集合是 b 对应的集合的子集。
class Solution {
public int closestToTarget(int[] arr, int target) {
int ans = 0x3f3f3f3f;
int n = arr.length;
for(int i = 0; i < n; i++){
ans = Math.min(ans, Math.abs(arr[i] - target));
for(int j = i - 1; j >= 0 && (arr[i] & arr[j]) != arr[j]; j--){
arr[j] &= arr[i];
ans = Math.min(ans, Math.abs(arr[j] - target));
}
}
return ans;
}
}
给你一个整数数组 nums 和一个整数 k ,请你返回 nums 中有多少个子数组满足:子数组中所有元素按位 AND 的结果为 k 。
数据范围:
1 <= nums.length <= 10^5
0 <= nums[i], k <= 10^9
解法一
我们可以额外维护一个 List,存储子数组与运算的结果以及该结果对应的数量。
class Solution {
public long countSubarrays(int[] nums, int k) {
long ans = 0;
int n = nums.length;
List<int[]> ands = new ArrayList<>();
for(int i = 0; i < n; i++){
int x = nums[i];
ands.add(new int[]{x, 1});
for(int[] and : ands){
and[0] &= x;
if(and[0] == k){
ans += and[1];
}
}
int t = 0;
for(int j = 1; j < ands.size(); j++){
int[] and = ands.get(j);
int[] last = ands.get(t);
if(and[0] == last[0]){
last[1] += and[1];
}else{
ands.set(++t, and);
}
}
ands.subList(t + 1, ands.size()).clear();
}
return ans;
}
}
解法二
二分查找。
由于每个元素都是其右侧元素的子集,所以从 nums[0] 到 nums[i] 的元素值是非递减的。既然是有序数组,我们可以在 [0,i] 中二分查找 k 出现的次数。
class Solution {
public long countSubarrays(int[] nums, int k) {
long ans = 0;
int n = nums.length;
for (int i = 0; i < n; i++){
int x = nums[i];
for (int j = i - 1; j >= 0 && (nums[j] & x) != nums[j]; j--){
nums[j] &= x;
}
ans += find_right(nums, i + 1, k) - find_left(nums, i + 1, k);
}
return ans;
}
public int find_left(int[] nums, int r, int k){
int l = 0;
while(l < r){
int mid = (l + r) >> 1;
if(nums[mid] >= k){
r = mid;
}else{
l = mid + 1;
}
}
return l;
}
public int find_right(int[] nums, int r, int k){
int l = 0;
while(l < r){
int mid = (l + r) >> 1;
if(nums[mid] > k){
r = mid;
}else{
l = mid + 1;
}
}
return l;
}
}
GCD

与上一题的解法一类似,使用原地去重的方法。由于本题需要统计最大公因数等于 vi 的子数组的个数,因此需要额外维护一个 map 来统计每个最大公因数出现的次数。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
ll a[N];
int n,q;
vector<vector<ll>> ands;
map<int,ll> cnt;
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%lld", &a[i]);
}
for(int i = 0; i < n; i++){
ll x = a[i];
cnt[x]++;
ands.push_back({x, 1});
for(int j = 0; j < ands.size() - 1; j++){
vector<ll>& e = ands[j];
ll newGcd = __gcd(e[0], x);
cnt[newGcd] += e[1];
e[0] = newGcd;
}
int k = 0;
for(int j = 1; j < ands.size(); j++){
if(ands[j][0] == ands[k][0]){
ands[k][1] += ands[j][1];
}else{
ands[++k] = ands[j];
}
}
ands.erase(ands.begin() + k + 1, ands.end());
}
scanf("%d", &q);
while(q--){
int v;
scanf("%d", &v);
printf("%lld\n", cnt[v]);
}
return 0;
}
给你一个整数数组 nums 和一个整数 maxC。
如果一个 子数组 的所有元素的最大公因数(简称 HCF) 大于或等于 2,则称该子数组是稳定的。
一个数组的 稳定性因子 定义为其 最长 稳定子数组的长度。
你 最多 可以修改数组中的 maxC 个元素为任意整数。
在最多 maxC 次修改后,返回数组的 最小 可能稳定性因子。如果没有稳定的子数组,则返回 0。
注意:
子数组 是数组中连续的元素序列。
数组的 最大公因数(HCF)是能同时整除数组中所有元素的最大整数。
如果长度为 1 的 子数组 中唯一元素大于等于 2,那么它是稳定的,因为 HCF([x]) = x。
数据范围:
1 <= n == nums.length <= 10^5
1 <= nums[i] <= 10^9
0 <= maxC <= n
看到最小化最大值,可以想到二分答案法。
原问题可以转换成一个判定问题:给定上界 upper ,需要通过至少 cnt 次修改,才能让 nums 的稳定性因子不超过 upper ,如果 cnt<maxC ,说明答案 ⩽upper ,否则说明答案 >upper 。
遍历 nums ,计算以 i 为右端点的所有子数组的最大公因数。对于最大公因数 ⩾2 的以 i 为右端点的最长子数组,如果其长度 >2 ,那么必须修改元素的值。
此时贪心修改的元素一定是把当前 nums[1] 修改成 1 。如果修改 i 左边的元素,那么 nums[i] 可能与后续元素组成不符合要求的子数组。而如果修改 nums[i] ,那么包含 nums[i] 的所有子数组的最大公因数均为 1,这样后续就不用考虑 nums[i] 及其左边的所有元素了。
class Solution {
public int minStable(int[] nums, int maxC) {
int l = 0, r = nums.length;
while(l < r){
int mid = (l + r) >> 1;
if(check(nums, mid) <= maxC){
r = mid;
}else{
l = mid + 1;
}
}
return l;
}
public int check(int[] nums, int len){
List<int[]> gcds = new ArrayList<>();
int cnt = 0;
for(int i = 0; i < nums.length; i++){
int x = nums[i];
gcds.add(new int[]{x, i});
for(int[] gcd : gcds){
gcd[0] = Gcd(gcd[0], x);
}
int k = 0;
for(int j = 1; j < gcds.size(); j++){
if(gcds.get(j)[0] != gcds.get(k)[0]){
gcds.set(++k, gcds.get(j));
}
}
gcds.subList(k + 1, gcds.size()).clear();
if(gcds.get(0)[0] == 1){
gcds.remove(0);
}
if(!gcds.isEmpty() && i - gcds.get(0)[1] + 1 > len){
cnt++;
gcds.clear();
}
}
return cnt;
}
public int Gcd(int a, int b){
if(b == 0){
return a;
}else{
return Gcd(b, a % b);
}
}
}

与上一题的思路类似,当遍历到 a[i] 不满足条件时,则修改 a[i] 为 1 ,这样才不会影响后面的数。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int a[N];
int n;
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d", &a[i]);
}
vector<vector<int>> gcds;
int cnt = 0;
for(int i = 0; i < n; i++){
int x = a[i];
gcds.push_back({x, i});
for(auto& gcd : gcds){
gcd[0] = __gcd(x, gcd[0]);
}
int k = 0;
for(int j = 1; j < gcds.size(); j++){
if(gcds[j][0] != gcds[k][0]){
gcds[++k] = gcds[j];
}
}
gcds.erase(gcds.begin() + k + 1, gcds.end());
bool find = false;
for(int j = 0; j < gcds.size(); j++){
if(j == gcds.size() - 1){
int len = i - gcds[j][1] + 1;
if(gcds[j][0] <= len){
cnt++;
find = true;
break;
}
}else{
int min_len = i - gcds[j + 1][1] + 2;
int max_len = i - gcds[j][1] + 1;
if(gcds[j][0] >= min_len && gcds[j][0] <= max_len){
cnt++;
find = true;
break;
}
}
}
if(find){
gcds.clear();
gcds.push_back({2, i});
}
printf("%d ", cnt);
}
return 0;
}