Numbers that are bitwise AND of at least one non-empty sub-array

Given an array ‘arr’, the task is to find all possible integers each of which is the bitwise AND of at least one non-empty sub-array of ‘arr’.

Examples:

Input: arr = {11, 15, 7, 19}
Output: [3, 19, 7, 11, 15]
3 = arr[2] AND arr[3]
19 = arr[3]
7 = arr[2]
11 = arr[0]
15 = arr[1]

Input: arr = {5, 2, 8, 4, 1}
Output: [0, 1, 2, 4, 5, 8]
0 = arr[3] AND arr[4]
1 = arr[4]
2 = arr[1]
4 = arr[3]
5 = arr[0]
8 = arr[2]

Naive Approach:

  • An array of size ‘N’ contains N*(N+1)/2 sub-arrays. So, for small ‘N’, iterate over all possible sub-arrays and add each of their AND result to a set.
  • Since, sets do not contain duplicates, it’ll store each value only once.
  • Finally, print the contents of the set.

Below is the implementation of the above approach:

Java

filter_none

edit
close

play_arrow

link
brightness_4
code

// Java implementation of the approach
import java.util.HashSet;
class CP {
    public static void main(String[] args)
    {
        int[] A = { 11, 15, 7, 19 };
        int N = A.length;
  
        // Set to store all possible AND values.
        HashSet<Integer> set = new HashSet<>();
        int i, j, res;
  
        // Starting index of the sub-array.
        for (i = 0; i < N; ++i)
  
            // Ending index of the sub-array.
            for (j = i, res = Integer.MAX_VALUE; j < N; ++j) {
                res &= A[j];
  
                // AND value is added to the set.
                set.add(res);
            }
  
        // The set contains all possible AND values.
        System.out.println(set);
    }
}

chevron_right


C#

filter_none

edit
close

play_arrow

link
brightness_4
code

// C# implementation of the approach 
using System;
using System.Collections.Generic;
  
class CP { 
  
    public static void Main(String[] args) 
    
        int[] A = {11, 15, 7, 19}; 
        int N = A.Length; 
  
        // Set to store all possible AND values. 
        HashSet<int> set1 = new HashSet<int>(); 
        int i, j, res;
  
        // Starting index of the sub-array. 
        for (i = 0; i < N; ++i) 
        {
            // Ending index of the sub-array. 
            for (j = i, res = int.MaxValue; j < N; ++j)
            
                res &= A[j]; 
                  
                // AND value is added to the set. 
                set1.Add(res); 
            }
              
        }
          
        // displaying the values
        foreach(int m in set1)
        {
            Console.Write(m + " ");
        }
          
    

chevron_right


Output:

[3, 19, 7, 11, 15]

Time Complexity: O(N^2)

Efficient Approach: This problem can be solved efficiently by divide-and-conquer approach.

  • Consider each element of the array as a single segment. (‘Divide’ step)
  • Add the AND value of all the subarrays to the set.
  • Now, for the ‘conquer’ step, keep merging consecutive subarrays and keep adding the additional AND values obtained while merging.
  • Continue step 4, until a single segment containing the entire array is obtained.

Below is the implementation of the above approach:

filter_none

edit
close

play_arrow

link
brightness_4
code

// Java implementation of the approach
import java.util.*;
  
public class CP {
    static int ar[];
    static int n;
  
    // Holds all possible AND results
    static HashSet<Integer> allPossibleAND;
  
    // driver code
    public static void main(String[] args)
    {
        ar = new int[] { 11, 15, 7, 19 };
        n = ar.length;
  
        allPossibleAND = new HashSet<>(); // initialization
        divideThenConquer(0, n - 1);
  
        System.out.println(allPossibleAND);
    }
  
    // recursive function which adds all
    // possible AND results to 'allPossibleAND'
    static Segment divideThenConquer(int l, int r)
    {
  
        // can't divide into 
        //further segments
        if (l == r)
        {
            allPossibleAND.add(ar[l]);
  
            // Therefore, return a segment
            // containing this single element.
            Segment ret = new Segment();
            ret.leftToRight.add(ar[l]);
            ret.rightToLeft.add(ar[l]);
            return ret;
        }
  
        // can be further divided into segments
        else {
            Segment left 
                = divideThenConquer(l, (l + r) / 2);
            Segment right 
                = divideThenConquer((l + r) / 2 + 1, r);
  
            // Now, add all possible AND results,
            // contained in these two segments
  
            /* ********************************
            This step may seem to be inefficient 
            and time consuming, but it is not.
            Read the 'Analysis' block below for 
            further clarification.
            *********************************** */
            for (int itr1 : left.rightToLeft)
                for (int itr2 : right.leftToRight)
                    allPossibleAND.add(itr1 & itr2);
  
            // 'conquer' step
            return mergeSegments(left, right);
        }
    }
  
    // returns the resulting segment after
    // merging segments 'a' and 'b'
    // 'conquer' step
    static Segment mergeSegments(Segment a, Segment b)
    {
        Segment res = new Segment();
  
        // The resulting segment will have
        // same prefix sequence as segment 'a'
        res.copyLR(a.leftToRight);
  
        // The resulting segment will have
        // same suffix sequence as segment 'b'
        res.copyRL(b.rightToLeft);
  
        Iterator<Integer> itr;
  
        itr = b.leftToRight.iterator();
        while (itr.hasNext())
            res.addToLR(itr.next());
  
        itr = a.rightToLeft.iterator();
        while (itr.hasNext())
            res.addToRL(itr.next());
  
        return res;
    }
}
  
class Segment {
  
    // These 'vectors' will always 
    // contain atmost 30 values.
    ArrayList<Integer> leftToRight 
        = new ArrayList<>();
    ArrayList<Integer> rightToLeft 
        = new ArrayList<>();
  
    void addToLR(int value)
    {
        int lastElement 
            = leftToRight.get(leftToRight.size() - 1);
  
        // value decreased after AND-ing with 'value'
        if ((lastElement & value) < lastElement)
            leftToRight.add(lastElement & value);
    }
  
    void addToRL(int value)
    {
        int lastElement 
            = rightToLeft.get(rightToLeft.size() - 1);
  
        // value decreased after AND-ing with 'value'
        if ((lastElement & value) < lastElement)
            rightToLeft.add(lastElement & value);
    }
  
    // copies 'lr' to 'leftToRight'
    void copyLR(ArrayList<Integer> lr)
    {
        Iterator<Integer> itr = lr.iterator();
        while (itr.hasNext())
            leftToRight.add(itr.next());
    }
  
    // copies 'rl' to 'rightToLeft'
    void copyRL(ArrayList<Integer> rl)
    {
        Iterator<Integer> itr = rl.iterator();
        while (itr.hasNext())
            rightToLeft.add(itr.next());
    }
}

chevron_right


Output:

[19, 3, 7, 11, 15]

Analysis:

The major optimization in this algorithm is realizing that any array element can yield a maximum of 30 distinct integers (as 30 bits are needed to hold 1e9). Confused?? Let’s proceed step by step.

Let us start a sub-array from the ith element which is A[i]. As the succeeding elements are AND-ed with Ai, the result can either decrease or remain same (because a bit will never get changed from ‘0’ to ‘1’ after an AND operation).
In the worst case, A[i] can be 2^31 – 1 (all 30 bits will be ‘1’). As the elements are AND-ed, atmost 30 distinct values can be obtained because only a single bit might get changed from ‘1’ to ‘0’ in the worst case,
i.e. 111111111111111111111111111111 => 111111111111111111111111101111

So, for each ‘merge’ operation, these distinct values merge to form another set having atmost 30 integers.
Therefore, Worst Case Time Complexity for each merge can be O(30 * 30) = O(900).

Time Complexity: O(900*N*logN).

PS: The time complexity can seem too high, but in practice, the actual complexity lies around O(K*N*logN), where, K is much less than 900. This is because, the lengths of the ‘prefix’ and ‘suffix’ arrays are much less when ‘l’ and ‘r’ are quite close.



My Personal Notes arrow_drop_up