在 XOR 序列上最大化 AND

问题描述

问题

我们得到了两个长度为 foselastica 的数组 ab。我们通过重新排列 n 中的值来构建第三个数组 c。目标是找到最大化

的最优b
c

其中 result = (a[0] ^ c[0]) & (a[1] ^ c[1]) & ... & (a[n - 1] ^ c[n - 1]) 是 XOR 而 ^ 是 AND。

是否可以有效地执行此操作?迭代 & 的所有可能排列很简单,但这对于大 b 来说是不可行的。

更多详情

  • n 中值的顺序是固定的。
  • a 中的值的顺序可以重新排列以形成 b。即以c开头,可能是将值重新排列为b = [1,2,3]时得到的结果最大。
  • 如果需要,
  • c = [2,1,3] 可以就地重新排列(我在示例代码中这样做了)。
  • 由于最优 b 不一定唯一,因此可能返回任何最优 c
  • 假设所有值都是 32 位无符号整数。
  • c

测试用例

1 <= n <= 10,000
Input:
a = [3,4,5]
b = [6,7,8]
Output:
c = [8,6] (result = 3)
Input:
a = [1,11,10,11]
b = [6,20,8,9,7]
Output:
c = [8,6,20] (result = 9)

示例代码

这是我用于一个天真的解决方案的 C++ 代码,该代码详尽地尝试了 Input: a = [0,16] b = [512,256,128,64,32,16] Output: c = [16,512] (result = 0) 的所有排列(使用 Visual Studio 2019 在 Windows 10 上测试):

b

解决方法

狄龙的回答已经有了最重要的想法。使用这些想法,我们可以在线性时间和空间中解决问题。

关键目标是使结果 1 的高有效位,无论我们是否牺牲较低的有效位。如果我们专注于单个位 k,那么我们可以执行以下操作:

  • a 中第 k 位设置为 1 (a1) 和 0 (a0) 的数字进行分区,分别
  • b 中第 k 位设置为 1 (b1) 和 0 (b0) 的数字进行分区,分别
  • 如果 a1 中的条目数等于 b0 中的条目数,我们可以将结果中的第 k 位设置为 1。在这种情况下,我们认为分区成功。

分区的含义如下:在我们最终的c中,我们需要将a1的条目与b0的条目以及a0的条目匹配到b1。如果我们这样做,XOR 运算将对所有条目产生 1,AND 运算将产生整体 1

不,我们如何在算法中使用这种洞察力? 我选择用索引来表示 a 的分区(即,分区是一组索引的集合)和 b 用实际数字的分区。最初,我们从每个只有一个集合的分区开始(即,a 的分区具有所有索引的集合,而 b 的分区具有 b 作为元素)。 我们从最重要的位开始,并尝试进行分区。

如果分区成功,我们最终会为 ab 生成两个分区(其中一个可能是空的)。然后我们已经知道 b 中的哪些数字可以放在哪些索引上。如果我们违反了这个结果,我们会得到一个较小的最终结果。

如果我们的分区不成功,我们只需忽略这一步。

现在让我们继续下一点。我们可能已经有一个分区,它不仅有初始集,还有更细粒度的东西。我们不想混淆分区。因此,我们可以使用与以前相同的方法对分区进行分区。如果我们对所有分区都成功,我们从现在开始使用子分区。如果没有,我们使用原始分区。

如果我们对所有位都这样做,我们最终会得到 b 中数字的映射以及它们可以放入的索引以获得最大的最终结果。它可能不是唯一的映射。如果一个分区包含多个元素,任何映射都会产生最大值。所以我们只需要选择一个就可以得到结果。

这是您问题中的一个示例:

a = {    1,11,7,4,10,11  }
  = { 0001b,1011b,0111b,0100b,1010b,1011b }
b = {    6,20,8,9,7  }
  = { 0110b,10100b,1000b,1001b,0111b }

这里是最重要的算法步骤:

               index partitioning    |   b partitioning
 -----------+------------------------+-----------------------
initial     | { 0,1,2,3,5 }   |  {6,7 }
------------+------------------------+-----------------------
after bit 3 | { 1,5 }            |  { 6,7 }
            | { 0,3 }            |  { 8,10 }
------------+------------------------+-----------------------
after bit 0 | { 1,5 }               |  { 6,20 }
(final)     | { 4 }                  |  { 7 }
            | { 0,2 }               |  { 8,10 }
            | { 3 }                  |  { 9 }

所以我们有一个非唯一案例。数字 620 可以同时出现在索引 15 处。但是编号 7 肯定会出现在索引 4 处。一种解决方案是:

c = { 8,6,20 }

检查:

a = { 0001b,1011b }
XOR
c = { 1000b,0110b,10100b }
-------------------------------------------------
    { 1001b,1101b,11111b }

AND = 1001b = 9

这是一些示例 C++ 代码。请注意,代码的重点是可理解性。有一些事情可以更有效地实施。

#include <iostream>
#include <vector>
#include <cstdint>

struct Partition
{
    std::vector<size_t> indices;
    std::vector<uint32_t> bs;
};

struct Partitioning
{
    bool success;
    Partition p1;
    Partition p2;
};

Partitioning partition(const std::vector<uint32_t>& a,const std::vector<size_t>& indices,const std::vector<uint32_t>& b,size_t bit)
{
    uint32_t mask = 1 << bit;

    Partitioning result;

    // partition the indices of a
    for (size_t i : indices)
    {
        uint32_t n = a[i];
        if (n & mask)
            result.p1.indices.push_back(i);
        else
            result.p2.indices.push_back(i);
    }
    
    // partition b
    for (uint32_t n : b)
        if (n & mask)
            result.p2.bs.push_back(n);
        else
            result.p1.bs.push_back(n);

    // check if we are successful
    bool canMakeBit1 = result.p1.indices.size() == result.p1.bs.size();
    result.success = canMakeBit1;

    return result;
}

void findMax(const std::vector<uint32_t>& a,const std::vector<uint32_t>& b)
{
    std::vector<uint32_t> aIndices(a.size());
    for (size_t i = 0; i < a.size(); ++i)
        aIndices[i] = i;

    // current partitioning
    std::vector<std::vector<uint32_t>> partsIndices;
    partsIndices.push_back(aIndices);
    std::vector<std::vector<uint32_t>> partsBs;
    partsBs.push_back(b);

    // temporary partitionings
    std::vector<Partitioning> partitionings;

    // assume 32 bits
    size_t bit = 32;
    do
    {
        --bit;

        bool success = true;
        partitionings.clear();
        
        // try to partition all current partitions
        for (size_t i = 0; i < partsIndices.size(); ++i)
        {
            partitionings.push_back(partition(a,partsIndices[i],partsBs[i],bit));
            if (!partitionings.back().success)
            {
                success = false;
                break;
            }
        }

        // if all partitionings are successful
        if (success)
        {
            // replace the current partitioning with the new one
            partsIndices.clear();
            partsBs.clear();
            for (auto& p : partitionings)
            {
                if (p.p1.indices.size() > 0)
                {
                    partsIndices.push_back(p.p1.indices);
                    partsBs.push_back(p.p1.bs);
                }

                if (p.p2.indices.size() > 0)
                {
                    partsIndices.push_back(p.p2.indices);
                    partsBs.push_back(p.p2.bs);
                }
            }
        }
    } while (bit > 0);

    // Generate c
    std::vector<uint32_t> c(a.size());    
    for (size_t i = 0; i < partsIndices.size(); ++i)
    {
        const auto& indices = partsIndices[i];
        const auto& bs = partsBs[i];
        for (size_t j = 0; j < indices.size(); ++j)
        {
            c[indices[j]] = bs[j];
        }
    }

    // Print the result
    uint32_t result = 0xffffffff;
    for (size_t i = 0; i < a.size(); ++i)
    {
        std::cout << c[i] << " ";
        result = result & (a[i] ^ c[i]);
    }
    std::cout << std::endl << result << std::endl;
}

int main()
{
    std::vector<uint32_t> a = { 1,11 };
    std::vector<uint32_t> b = { 6,7 };
    
    findMax(a,b);

    return 0;
}
,

TL;DR

我认为可以将其重新表述为 assignment problem,其最优解可以在 O(n^3) 时间内找到。但我没有在我的回答中尝试这样做。

免责声明

我将描述的方法仍然涉及检查排列。它通常似乎比天真的方法需要更少的迭代,但我的解决方案的额外开销实际上可能会降低整体速度。我没有将我的方法的运行时间与天真的方法进行比较,也没有彻底检查它是否没有错误(对于提供的 3 个测试用例,它似乎工作正常)。

也就是说,在此过程中我确实有一些见解,所以也许我在这方面的尝试将帮助其他人提出更有效的解决方案。希望我的解释是清楚的,而且我不是在胡说八道。

总体思路

假设我们创建了一个无向的 bipartite graph,其中两个独立的节点集分别对应于 ab,每个节点都是一个数组元素。

让我们一次考虑一位,例如最低有效位 (LSB)。 我们将 LSB 标记为位 0。 我们还考虑第一个测试用例(为了简单起见,我们只考虑最低的 4 位而不是全部 32 位):

Input:
a = [3,5]  // binary: [0011,0100,0101]
b = [6,8]  // binary: [0110,0111,1000]

我们的图在 a 集合中有 3 个节点,分别标记为 3、4 和 5;并且它在 b 集合中有 3 个节点,分别标记为 6、7 和 8。如果 a 节点的所选位(位 0)不同于b 节点。例如,我们将在节点 3 和 6 之间绘制一条边,因为 3 (0011) 的 LSB 与 6 (0110) 的 LSB 不同。我们不会在节点 3 和 7 之间绘制边,因为 3 (0011) 的 LSB 与 7 (0111) 的 LSB 相同。如果我们一路解决这个问题,我们最终会得到以下位 0 的邻接表:

3: 6,8
4: 7
5: 6,8

我们有两组边可以从 a 中的每个节点组成:

  • 如果所选位为 1,则为 b 中所选位为 0 的所有节点绘制一条边。
  • 如果所选位为 0,则为 b 中所选位为 1 的所有节点绘制一条边。

我们可以观察到,当且仅当该位为 1 的 a 节点的数量与 {{1}位为 0 的节点。否则,该位在最终结果中不能为 1,因为我们必须将至少一个 b 节点与一个相同位值的 a 节点配对,在 XOR 之后为该位生成 0,因此在 AND 之后为该位生成 0。

现在,如果我们为 4 位中的每一个创建这样的图,并确定哪些位是最终结果中的 1 的候选者,这将为我们提供最佳结果的上限。这也为我们提供了一个明显的结果下限,因为我们知道我们至少可以找到 b 的排序,导致设置最重要的候选位。

下面给出了我们测试用例中每个位的邻接表。

b

寻找Bit 0 (LSB) 3: 6,8 (candidate bit) Bit 1 3: 8 4: 6,7 5: 6,7 (candidate bit) Bit 2 3: 6,7 4: 8 5: 8 (NOT candidate) Bit 3 (MSB) 3: 8 4: 8 5: 8 (NOT candidate) Upper bound: 3 (both candidate bits 0 and 1 are set) Lower bound: 2 (only candidate bit 1 is set)

为了得到最优的 c,我们需要从最重要的候选位开始。我们循环遍历所有有效的 cc 的排列,我们遵守该位的邻接列表),与 {{ 的可能排列总数相比,其中希望相对较少1}}。

对于这些排列中的每一个,我们查看它们中的任何一个对于下一个最重要的候选位是否有效。如果它们都不是,那么我们检查之后的位,依此类推。如果在遵守 MSB 上的任何排列的邻接列表时不能包含其他位,那么最终结果中只有 MSB 可以为 1,这就是解决方案。否则,我们希望优先处理对更重要的位有效的排列,并且我们需要递归地检查在该点之前对所有位有效的排列。

排列“有效”是什么意思?本质上,邻接表充当从 bb 的映射的约束。如果一个候选位的约束与另一个候选位的约束不冲突,我们知道这些位在最终结果中都可以是 1。例如,查看位 0 和 1,我们看到有一种方法可以同时满足这两个映射(即 a)。因此,该排列 (c) 对位 0 和 1 均有效。(事实上,该排列被证明是最佳排列。)

对于冲突约束的示例,考虑位 0 和 2。位 2 要求节点 4 连接到节点 8。也就是说,在满足 3: 8; 4: 7; 5: 6 的索引 c = [8,6] 处,我们需要设置i 以便在最终结果中设置位 2。但是,位 0 要求节点 4 连接到节点 7。这是一个冲突,因此在最终结果中不能同时设置位 0 和 2。 (但无论如何,位 2 都不是候选位,因为 a[i] == 4 节点中的两个(4 和 5)对该位具有 1 但只有一个 c[i] = 8 节点(8)具有 0为了那一点。)

与已解决的图论问题的可能关系

我对图论问题不是很熟悉,但在我看来,这个问题的二部图公式与 maximum weighted bipartite matching 也称为 assignment problem 相关。不过,我们的边缘目前没有加权。也许一条边可以根据它存在多少位来加权?也许更重要的位会被赋予更大的权重?我认为我们仍然只需要考虑候选位的图。

本质上,从每个候选位的二部图构建一个 axb 邻接矩阵。为边分配统一权重 n;所以边缘权重是位 0 为 1,位 1 为 2,位 2 为 4,依此类推。这确保了更重要的候选位优先于较小位的任何组合。然后组合所有邻接矩阵的权重以构建最终的代表性邻接矩阵。这将是分配问题的输入。

如果此比较有效,则存在 Hungarian algorithm 以在 O(n^3) 时间内最优解决分配问题。这明显优于朴素排列方法所需的 O(n!) 时间。

乱码

为了完整起见,我将我为我提议的方法编写的代码包括在内,尽管我认为将这个问题重新表述为赋值问题的可能性更值得研究。要使用此代码,请从问题中提供的示例代码开始,然后将此代码复制到同一文件中。在 n 中,调用 pow(2,bit) 而不是 main

solvePermuteLess