【问题标题】:Optimal solution for "Bitwise AND" problem in C#C#中“按位与”问题的最佳解决方案
【发布时间】:2021-09-26 20:44:13
【问题描述】:

问题陈述: 给定一个非负整数数组,计算无序数组元素对的数量,使得它们的按位与是 2 的幂。

例子: arr = [10, 7, 2, 8, 3]

答案: 6 (10&7, 10&2, 10&8, 10&3, 7&2, 2&3)

约束:

1 <= arr.Count <= 2*10^5
0 <= arr[i] <= 2^12

这是我想出的蛮力解决方案:

    private static Dictionary<int, bool> _dictionary = new Dictionary<int, bool>();

    public static long CountPairs(List<int> arr)
    {
        long result = 0;

        for (var i = 0; i < arr.Count - 1; ++i)
        {
            for (var j = i + 1; j < arr.Count; ++j)
            {
                if (IsPowerOfTwo(arr[i] & arr[j])) ++result;
            }
        }

        return result;
    }

    public static bool IsPowerOfTwo(int number)
    {
        if (_dictionary.TryGetValue(number, out bool value)) return value;
        var result = (number != 0) && ((number & (number - 1)) == 0);
        _dictionary[number] = result;
        return result;
    }

对于小输入,这工作正常,但对于大输入,这工作缓慢。 我的问题是:该问题的最佳(或至少更佳)解决方案是什么?请在 C# 中提供一个优雅的解决方案。 ????

【问题讨论】:

  • 在这里使用字典只会让事情变慢。
  • @Olivier 我虽然对于更大的输入,尤其是在大量重复的情况下,这会更快。但是对于大输入,带或不带字典的两个版本都运行缓慢

标签: c# .net algorithm optimization bitwise-operators


【解决方案1】:

加速您的方法的一种方法是在计数之前计算数据值的直方图。

这将减少长数组的计算次数,因为值 (4096) 的选项少于数组的长度 (200000)。

在计算 2 次方的 bin 时要小心,以确保在将数字与其自身进行比较时,不要通过包含 case 来高估对数。

【讨论】:

  • 我曾经编写了一个复杂度为 O(2^N * N^2 + n * N) 的解决方案,其中 N 是最大位数,(包含在此处的答案中),但想知道是否这种复杂性可以改进。
  • 哇 - 您的解决方案看起来比使用直方图更有效,非常酷!
【解决方案2】:

我们可以调整bit-subset dynamic programming idea 以获得具有O(2^N * N^2 + n * N) 复杂度的解决方案,其中N 是范围内的位数,n 是列表中的元素数。 (因此,如果整数限制为 [1, 4096] 或 2^12,n 为 100,000,我们将有大约 2^12 * 12^2 + 100000*12 = 1,789,824 次迭代。)

我们的想法是我们想要计算我们有重叠位子集的实例,并添加一个固定的集合位。给定Ai——为简单起见,取6 = b110——如果我们要找到所有与为零的合作伙伴,我们将取Ai的否定,

110 -> ~110 -> 001

现在我们可以构建一个动态程序,它采用递减掩码,从全数开始,向左递减掩码

001
^^^

001
^^

001
^

Ai 否定的每个设置位都表示一个零,可以与 1 或 0 进行与运算以达到相同的效果。 Ai 的否定上的每个未设置位代表Ai 中的一个设置位,我们只想与零配对,除了单个设置位

我们通过分别检查每种可能性来构建这个集合位。那么在哪里计算将与Ai 的对数为零,我们会做类似的事情

001 ->
  001
  000

我们现在要枚举

011 ->
  011
  010

101 ->
  101
  100

每次修复一个位。

我们可以通过向内部迭代添加维度来实现这一点。当掩码最后确实有一个设置位时,我们通过仅计算将设置该位的前一个 DP 单元的结果,而不是可能设置该位的子集的通常联合来“修复”相关位与否。

这里有一些 JavaScript 代码(对不起,我不懂 C#),用于在最后与蛮力解决方案进行比较来演示。

var debug = 0;

function bruteForce(a){
  let answer = 0;
  for (let i = 0; i < a.length; i++) {
    for (let j = i + 1; j < a.length; j++) {
      let and = a[i] & a[j];
      if ((and & (and - 1)) == 0 && and != 0){
        answer++;
        if (debug)
          console.log(a[i], a[j], a[i].toString(2), a[j].toString(2))
      }
    }
  }
  return answer;
}
  
function f(A, N){
  const n = A.length;
  const hash = {}; 
  const dp = new Array(1 << N);
  
  for (let i=0; i<1<<N; i++){
    dp[i] = new Array(N + 1);
    
    for (let j=0; j<N+1; j++)
      dp[i][j] = new Array(N + 1).fill(0);
  }
      
  for (let i=0; i<n; i++){
    if (hash.hasOwnProperty(A[i]))
      hash[A[i]] = hash[A[i]] + 1;
    else
      hash[A[i]] = 1;
  }
  
  for (let mask=0; mask<1<<N; mask++){
    // j is an index where we fix a 1
    for (let j=0; j<=N; j++){
      if (mask & 1){
        if (j == 0)
          dp[mask][j][0] = hash[mask] || 0;
        else
          dp[mask][j][0] = (hash[mask] || 0) + (hash[mask ^ 1] || 0);
        
      } else {
        dp[mask][j][0] = hash[mask] || 0;
      }
    
      for (let i=1; i<=N; i++){
        if (mask & (1 << i)){
          if (j == i)
            dp[mask][j][i] = dp[mask][j][i-1];
          else
            dp[mask][j][i] = dp[mask][j][i-1] + dp[mask ^ (1 << i)][j][i - 1];
          
        } else {
          dp[mask][j][i] = dp[mask][j][i-1];
        }
      }
    }
  } 
  
  let answer = 0; 
  
  for (let i=0; i<n; i++){
    for (let j=0; j<N; j++)
      if (A[i] & (1 << j))
        answer += dp[((1 << N) - 1) ^ A[i] | (1 << j)][j][N];
  }

  for (let i=0; i<N + 1; i++)
    if (hash[1 << i])
      answer = answer - hash[1 << i];

  return answer / 2;
} 
 
var As = [
  [10, 7, 2, 8, 3] // 6
];

for (let A of As){
  console.log(JSON.stringify(A));
  console.log(`DP, brute force: ${ f(A, 4) }, ${ bruteForce(A) }`);
  console.log('');
}

var numTests = 1000;

for (let i=0; i<numTests; i++){
  const N = 6;
  const A = [];
  const n = 10;
  for (let j=0; j<n; j++){
    const num = Math.floor(Math.random() * (1 << N));
    A.push(num);
  }

  const fA = f(A, N);
  const brute = bruteForce(A);
  
  if (fA != brute){
    console.log('Mismatch:');
    console.log(A);
    console.log(fA, brute);
    console.log('');
  }
}

console.log("Done testing.");

【讨论】:

    【解决方案3】:
    int[] numbers = new[] { 10, 7, 2, 8, 3 };
    
    static bool IsPowerOfTwo(int n) => (n != 0) && ((n & (n - 1)) == 0);
    
    long result = numbers.AsParallel()
        .Select((a, i) => numbers
            .Skip(i + 1)
            .Select(b => a & b)
            .Count(IsPowerOfTwo))
        .Sum();
    

    如果我正确理解了问题,这应该可以工作并且应该更快。

    • 首先,对于数组中的每个数字,我们会抓取数组中的所有元素,以获取要配对的数字集合。
    • 然后我们用按位与转换每个对数,然后计算满足我们的“IsPowerOfTwo;”的数谓词(实现here)。
    • 最后我们简单地得到所有计数的总和 - 我们的输出是 6。

    我认为这应该比您基于字典的解决方案更高效 - 它避免了每次您希望检查 2 的幂时都必须执行查找。

    我认为还考虑到您输入的数值限制,使用 int 数据类型很好。

    【讨论】:

    • 这基本上与我的方法具有相同的逻辑,除了它使用更多的内存,因为它生成更多的集合。正如我在上面的 cmets 部分中提到的,如果没有字典,我的代码运行缓慢。我认为有更好的算法方法来解决这个问题,但我不知道如何。
    • 是的,这只是一个示例。我必须使用 long 作为返回类型,才能对通过 Constraint 部分的所有可能输入正常工作。
    • 但是你的回答通过了 "provide a graceful solution in C#" 我的问题的一部分:D
    • 好吧 1/3 还不错 :) 出于兴趣,它实际上是否比您的字典方法运行得更快? AsParrallel 可能是此解决方案的快速性能提升。算法上......现在想到的一般情况下没有任何效果......
    猜你喜欢
    • 2020-04-12
    • 2019-09-20
    • 1970-01-01
    • 1970-01-01
    • 2015-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    相关资源
    最近更新 更多