【问题标题】:Finding a number of maximally different binary vectors from a set从一组中找到多个最大不同的二进制向量
【发布时间】:2018-05-11 12:37:10
【问题描述】:

考虑所有长度为 n 的二进制向量的集合 S,其中每个向量恰好包含 m 个;所以每个向量中有 n-m 个零。
我的目标是从 S 构造一个数量为 k 的向量,以使这些向量彼此尽可能不同。

举个简单的例子,取n=4,m=2和k=2,那么一个可能的解决方案是:[1 ,1,0,0] 和 [0,0,1,1]。

这似乎是编码理论文献中的一个开放问题(?)。

有没有办法(即算法)找到次优但好的解决方案?
汉明距离是在这种情况下使用的正确性能度量吗?

一些想法:
this paper 中,作者提出了几种算法来查找向量子集,使得成对汉明距离>= 某个值,d
我已经实现了如下的随机方法:获取一个集合 SS,它由 S 中的任何向量初始化。然后,我考虑剩余的向量 在 S 中。对于这些向量中的每一个,我检查该向量是否与 SS 中的每个向量至少有一个距离 d。如果是,则将其添加到 SS
通过取最大可能d,如果SS的大小>=k,那么我认为SS为一个最佳解决方案,我从 SS 中选择 k 个向量的任何子集。 使用这种方法,我认为生成的 SS 将取决于 SS 中初始向量的标识;即有多种解决方案(?)。
但是如果SS的大小是k怎么办呢?
从论文中提出的算法中,我只了解了随机算法。我对二进制字典搜索(第 2.3 节)感兴趣,但我不知道如何实现它(?)。

【问题讨论】:

  • 标题具有误导性,当我阅读时我认为 m = n/2
  • @WalterTross 我同意。我已经编辑了标题。
  • 感谢@din。现在,这是一个真正的问题吗?如果是,n、m、k的数量级是多少?
  • 假设:n
  • 你能定义你测量两个向量之间的差异吗?例如,给定向量1001010101101010,与1100 相比,它们的“距离”是否相同,因为它们在两个位上都不同?

标签: algorithm data-structures combinatorics binary-data hamming-distance


【解决方案1】:

也许你觉得this paper 有用(我写的)。它包含有效创建位串排列的算法。

例如inc()算法:

long  inc(long h_in , long m0 , long m1) {
    long  h_out = h_in | (~m1); //pre -mask
    h_out ++;
    // increment
    h_out = (h_out & m1) | m0; //post -mask
    return  h_out;
}

它接受输入 h_in 并返回比 h_in 至少大 1 的下一个更高的值,并且“匹配”边界 m0m1。 “匹配”意味着:无论m0 有一个1,结果都有一个1,而无论m1 有一个0,结果都有一个0。并不是说h_in 必须是关于mom1 的有效值!另外,请注意m0 必须按位小于m1,这意味着m0 不能在m10 的位置有1

这可用于生成与给定输入字符串具有最小编辑距离的排列:

假设您有0110,您首先将其否定为1001(编辑距离= k)。 设置“m0=1001”和“m1=1001”。使用它只会导致“1001”本身。

现在要获得所有编辑距离k-1的值,你可以做以下,简单地翻转m0m1的位,然后inc()将返回一个具有kk-1 差异的所有位串的有序序列。

我知道,目前还不是很有趣,但是您最多可以修改 k 位,并且 inc() 将始终返回具有最大允许编辑差异的所有排列关于 m0m1

现在,要获得 所有 个排列,您必须使用 m0m1 的所有可能组合重新运行算法。

示例:要获得具有编辑距离20110 的所有可能排列,您必须使用m0=0110m1=0110 的以下排列运行inc()(要获得排列,位位置具有要扩展,这意味着m0 设置为0m1 设置为1

  • 位 0 和 1 扩展m0=0010m1=1110
  • 位 0 和 2扩展m0=0100m1=1110
  • 位 0 和 3 已扩展m0=0110m1=1111
  • 位 1 和 2 已扩展m0=0000m1=0110
  • 位 1 和 3已扩展m0=0010m1=0111
  • 第 2 位和第 3 位已扩展m0=0100m1=0111

作为h_0 的起始值,我建议简单地使用m0。一旦inc() 返回m1,就可以中止迭代。

总结 上述算法在 O(x) 中生成所有 x 二进制向量,它们与给定向量 v 至少有 y 位(可配置)不同。

使用您对n=number of bits in a vector v 的定义,设置y=n 正好生成1 个向量,它与输入向量v 正好相反。对于y=n-1,它将生成n+1 向量:n 向量除了一位之外的所有向量和 1 个所有位都不同的向量。等等y的不同值。

**编辑:在上面的文本中添加了摘要并将错误的“XOR”替换为“NEGATE”。

【讨论】:

  • 对不起,我不确定我是否理解了您回答中的所有想法。你能用简单的话描述一下你的算法能找到什么吗?我的意思是,它是否找到例如的子集? k 个向量之间的差异最大?
  • 我添加了一个摘要和一个固定的单词。要回答您的问题,不,它没有找到例如的子集k 个与 each other 最大不同的向量。相反,它会找到与 single 给定向量(但彼此之间不存在)具有(可配置的)最小汉明距离(== 不同)的所有向量。抱歉,如果我误解了您的要求...?
  • 实际上,正如问题中所述,我正在寻找一种方法来找到例如k 个彼此最大不同(即距离较远)的向量。您的算法是一种计算复杂度较低的方法,可以找到与给定向量至少有一定距离 d 的所有向量;这是一个有趣的方法,谢谢。除了您的算法之外,您还可以使用详尽的搜索来找到这些向量。然而,问题是如何找到彼此最大不同的 k 个向量。也许我在这里遗漏了一些东西..
【解决方案2】:

我不知道最大化汉明距离的总和是否是获得一组“最大不同”二进制向量的最佳标准,但我强烈怀疑它是。此外,我强烈怀疑我将要介绍的算法恰好产生一组 k 向量,该向量使具有 m 个 1 和 n - m 个零的 n 位向量的汉明距离之和最大化。不幸的是,我没有时间证明这一点(当然,我可能是错的——在这种情况下,根据您的要求,您将得到一个“次优但很好”的解决方案。)

警告:在下面我假设,作为进一步的条件,结果集可能不会包含两次相同的向量。

我提出的算法如下:

从只有一个向量的结果集开始,重复添加一个 那些具有最大汉明距离和的剩余向量 来自结果集中已经存在的所有向量。停止时 结果集包含 k 个向量或所有可用向量已 已添加。

请注意,结果集的汉明距离之和不取决于第一个或任何后续向量的选择。

鉴于您在评论中提到的限制,我发现“蛮力”方法是可行的:

n

“蛮力”包括预先计算数组中“字典”顺序的所有向量,并保持最新的相同大小的数组,对于具有相同索引的每个向量,总汉明该向量与结果集中的所有向量的距离。在每次迭代中,总汉明距离被更新,并且选择与当前结果集具有最大总汉明距离的所有向量中的第一个(按“字典”顺序)。选择的向量被添加到结果集中,数组被移动以填充它的位置,有效地减小了它们的大小。

这是我在 Java 中的解决方案。如果需要,它可以很容易地翻译成任何程序语言。计算 n 中的 m 项组合的部分可以由库调用(如果可用)替换。以下 Java 方法具有相应的 C/C++ 宏,该宏在现代 CPU 上使用快速专用处理器指令: Long.numberOfTrailingZeros__builtin_ctzl,Long.bitCount__builtin_popcountl.

package waltertross.bits;

public class BitsMain {

    private static final String USAGE =
        "USAGE: java -jar <thisJar> n m k (1<n<64, 0<m<n, 0<k)";

    public static void main (String[] args) {

        if (args.length != 3) {
            throw new IllegalArgumentException(USAGE);
        }
        int n = parseIntArg(args[0]); // number of bits
        int m = parseIntArg(args[1]); // number of ones
        int k = parseIntArg(args[2]); // max size of result set
        if (n < 2 || n > 63 || m < 1 || m >= n || k < 1) {
            throw new IllegalArgumentException(USAGE);
        }
        // calculate the total number of available bit vectors
        int c = combinations(n, m);
        // truncate k to the above number
        if (k > c) {
            k = c;
        }
        long[] result   = new long[k]; // the result set (actually an array)
        long[] vectors  = new long[c - 1]; // all remaining candidate vectors
        long[] hammingD = new long[c - 1]; // their total Hamming distance to the result set
        long firstVector = (1L << m) - 1; // m ones in the least significant bits
        long lastVector  = firstVector << (n - m); // m ones in the most significant bits
        result[0] = firstVector; // initialize the result set
        // generate the remaining candidate vectors in "lexicographical" order
        int size = 0;
        for (long v = firstVector; v != lastVector; ) {
            // See http://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation
            long t = v | (v - 1); // t gets v's least significant 0 bits set to 1
            // Next set to 1 the most significant bit to change,
            // set to 0 the least significant ones, and add the necessary 1 bits.
            v = (t + 1) | (((~t & -~t) - 1) >>> (Long.numberOfTrailingZeros(v) + 1));
            vectors[size++] = v;
        }
        assert(size == c - 1);
        // chosenVector is always the last vector added to the result set
        long chosenVector = firstVector;
        // do until the result set is filled with k vectors
        for (int r = 1; r < k; r++) {
            // find the index of the new chosen vector starting from the first
            int chosen = 0;
            // add the distance to the old chosenVector to the total distance of the first
            hammingD[0] += Long.bitCount(vectors[0] ^ chosenVector);
            // initialize the maximum total Hamming distance to that of the first
            long maxHammingD = hammingD[0];
            // for all the remaining vectors
            for (int i = 1; i < size; i++) {
                // add the distance to the old chosenVector to their total distance
                hammingD[i] += Long.bitCount(vectors[i] ^ chosenVector);
                // whenever the calculated distance is greater than the max,
                // update the max and the index of the new chosen vector
                if (maxHammingD < hammingD[i]) {
                    maxHammingD = hammingD[i];
                    chosen = i;
                }
            }
            // set the new chosenVector to the one with the maximum total distance
            chosenVector = vectors[chosen];
            // add the chosenVector to the result set
            result[r] = chosenVector;
            // fill in the hole left by the chosenVector by moving all vectors
            // that follow it down by 1 (keeping vectors and total distances in sync)
            System.arraycopy(vectors,  chosen + 1, vectors,  chosen, size - chosen - 1);
            System.arraycopy(hammingD, chosen + 1, hammingD, chosen, size - chosen - 1);
            size--;
        }
        // dump the result set
        for (int r = 0; r < k; r++) {
            dumpBits(result[r], n);
        }
    }

    private static int parseIntArg(String arg) {
        try {
            return Integer.parseInt(arg);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException(USAGE);
        }
    }

    private static int combinations(int n, int m) {
        // calculate n over m = n! / (m! (n - m)!)
        // without using arbitrary precision numbers
        if (n <= 0 || m <= 0 || m > n) {
            throw new IllegalArgumentException();
        }
        // possibly avoid unnecessary calculations by swapping m and n - m
        if (m * 2 < n) {
            m = n - m;
        }
        if (n == m) {
            return 1;
        }
        // primeFactors[p] contains the power of the prime number p
        // in the prime factorization of the result
        int[] primeFactors = new int[n + 1];
        // collect prime factors of each term of n! / m! with a power of 1
        for (int term = n; term > m; term--) {
            collectPrimeFactors(term, primeFactors, 1);
        }
        // collect prime factors of each term of (n - m)! with a power of -1
        for (int term = n - m; term > 1; term--) {
            collectPrimeFactors(term, primeFactors, -1);
        }
        // multiply the collected prime factors, checking for overflow
        int combinations = 1;
        for (int f = 2; f <= n; f += (f == 2) ? 1 : 2) {
            // multiply as many times as requested by the stored power
            for (int i = primeFactors[f]; i > 0; i--) {
                int before = combinations;
                combinations *= f;
                // check for overflow
                if (combinations / f != before) {
                    String msg = "combinations("+n+", "+m+") > "+Integer.MAX_VALUE;
                    throw new IllegalArgumentException(msg);
                }
            }
        }
        return combinations;
    }

    private static void collectPrimeFactors(int n, int[] primeFactors, int power) {
        // for each candidate prime that fits in the remaining n
        // (note that non-primes will have been preceded by their component primes)
        for (int i = 2; i <= n; i += (i == 2) ? 1 : 2) {
            while (n % i == 0) {
                primeFactors[i] += power;
                n /= i;
            }
        }
    }

    private static void dumpBits(Long bits, int nBits) {
        String binary = Long.toBinaryString(bits);
        System.out.println(String.format("%"+nBits+"s", binary).replace(' ', '0'));
    }
}

n=5, m=2, k=4 的算法数据:

result
00011   00101 00110 01001 01010 01100 10001 10010 10100 11000 vectors
          0→2   0→2   0→2   0→2   0→4   0→2   0→2   0→4   0→4 hammingD
                                    ^                         chosen
00011   00101 00110 01001 01010 10001 10010 10100 11000
01100     2→4   2→4   2→4   2→4   2→6   2→6   4→6   4→6
                                    ^
00011   00101 00110 01001 01010 10010 10100 11000
01100     4→6   4→8   4→6   4→8   6→8   6→8   6→8
10001             ^

00011   00101 01001 01010 10010 10100 11000
01100       6     6     8     8     8     8
10001
00110

样本输出(n=24,m=9,k=20):

[wtross ~/Dropbox/bits]$ time java -jar bits-1.0-SNAPSHOT.jar 24 9 20
000000000000000111111111
000000111111111000000000
111111000000000000000111
000000000000111111111000
000111111111000000000000
111000000000000000111111
000000000111111111000000
111111111000000000000000
000000000000001011111111
000000111111110100000000
111111000000000000001011
000000000000111111110100
001011111111000000000000
110100000000000000111111
000000001011111111000000
111111110100000000000000
000000000000001101111111
000000111111110010000000
111111000000000000001101
000000000000111111110010

real    0m0.269s
user    0m0.244s
sys     0m0.046s

在我的 Mac 上,您的限制条件(n=24、m=9、k=99)内最棘手的情况大约需要 550 毫秒。

通过一些优化可以使算法更快,例如,通过移动较短的数组块。值得注意的是,在 Java 中,我发现“向上”移动比“向下”移动要慢得多。

【讨论】:

  • 您能解释一下您是如何初始化“结果集”的吗?我想使用你的解决方案,结果集取决于你用(?)初始化集合的向量。由于我不熟悉 java,如果您可以为代码的每个部分添加描述,我将不胜感激;或者请将算法作为伪代码提供。
  • 结果集使用“字典顺序”中的第一个向量进行初始化。添加其他向量,其中第一个(按“字典顺序”)与已经在结果集中的那些具有最大总汉明距离的向量。我提供的示例输出有助于将其可视化。输出序列是固定的,并在第 k 个向量处截断。我编写的代码在主要部分与 C 代码非常相似(顺便说一句,我从我提供的 bithacks URL 中粘贴了代码行)。无论如何,我会尽快改进答案。
  • @din 我试图澄清算法,并且在代码中添加了许多 cmets。计算 C(n, m) = n 的部分! / (m! (n-m)!) 如果可以使用执行相同操作的库函数,则可能完全是多余的,因此我认为它本身很有趣,但对您的问题却没有。如果您需要任何进一步的说明,请告诉我。
  • 如果我理解正确,您将按字典顺序对向量进行排序,并选择第一个向量(按此顺序),使结果集中的向量的汉明距离之和最大化。这个排序重要吗?如果我使用随机排序并选择与结果集产生最大汉明距离的向量(如果有多个向量,则随机选择一个)怎么办?
  • 不,顺序根本不重要。您可以使用随机顺序并随机选择一个向量,只要它与结果集的汉明距离最大。字典序的优点是易于生成,调试时易于遵循。
【解决方案3】:

更新答案

查看 Walter Tross 代码的示例输出,我认为生成随机解可以简化为:

以任何向量开头,例如对于 n=8,m=3,k=5:

A:   01001100  

每一步之后,对向量求和,得到每个位置被使用的次数:

SUM: 01001100

然后,对于下一个向量,将它们放在最少使用的位置(在本例中为零次),例如:

B:   00110001

得到:

A:   01001100  
B:   00110001
SUM: 01111101  

那么,剩下2个最少使用的位置,所以对于下一个向量中的3个,使用这2个位置,然后将第三个放在任何位置:

C:   10010010

得到:

A:   01001100  
B:   00110001
C:   10010010
SUM: 11121111  (or reset to 00010000 at this point)  

然后对于下一个向量,您有 7 个最少使用的位置(总和中的位置),因此选择任意 3 个,例如:

D:   10100010

得到:

A:   01001100  
B:   00110001
C:   10010010
D:   10100010
SUM: 21221121  

对于最终向量,选择 4 个最少使用的位置中的任何一个,例如:

E:   01000101

要生成所有解决方案,只需在每个步骤中生成每个可能的向量:

A:   11100000, 11010000, 11001000, ... 00000111

然后,例如当 A 和 SUM 为 11100000 时:

B:   00011100, 00011010, 00011001, ... 00000111

然后,例如当 B 为 00011100 且 SUM 为 11111100 时:

C:   10000011, 01000011, 00100011, 00010011, 00001011, 00000111

然后,例如当 C 为 10000011 且 SUM 为 21111111 时:

D:   01110000, 01101000, 01100100, ... 00000111

最后,例如当 D 为 01110000 且 SUM 为 22221111 时:

E:   00001110, 00001101, 00001011, 00000111

这将导致 C(8,3) × C(5,3) × C(8,1) × C(7,3) × C(4,3) = 56 × 10 × 8 × 35 ×对于 n=8、m=3、k=5,4 = 627,200 个解。


其实你需要加一个方法,避免重复同一个向量,避免把自己画到角落里;所以我认为这不会比沃尔特的回答更简单。


初步答案 - 存在重大问题

(我假设m不大于n/2,即1的个数不大于0的个数。否则,使用对称方法。)

当k×m不大于n时,显然存在最优解,例如:

n=10, m=3, k=3:  
A: 1110000000  
B: 0001110000  
C: 0000001110  

汉明距离都是2×m:

|AB|=6, |AC|=6, |BC|=6, total=18

当 k×m 大于 n 时,连续向量之间的汉明距离差异最小的解决方案提供最大的总距离:

n=8, m=3, k=4:
A: 11100000
B: 00111000
C: 00001110
D: 10000011
|AB|=4, |AC|=6, |AD|=4, |BC|=4, |BD|=6, |CD|=4, total=28  
n=8, m=3, k=4:
A: 11100000
B: 00011100
C: 00001110
D: 00000111
|AB|=6, |AC|=6, |AD|=6, |BC|=2, |BD|=4, |CD|=2, total=26  

所以,实际上,你取 m×k 看看它比 n 大多少,我们称它为 x = m×k−n,这个 x 是重叠的数量,即一个向量多久会有一个一个与前一个向量在同一位置。然后,您尽可能均匀地将重叠分布在不同的向量上,以最大化总距离。

在上面的例子中,x = 3×4−8 = 4,我们有 4 个向量,所以我们可以均匀地展开重叠,每个向量在与前一个向量相同的位置有 1 个。


要生成所有独特的解决方案,您可以:

  • 计算 x = m×k−n 并将 x 的所有分区生成 k 个部分,并具有尽可能小的最大值:
n=8, m=3, k=5  ->  x=7  
22111, 21211, 21121, 21112, 12211, 12121, 12112, 11221, 11212, 11122  
(discard partitions with value 3)  
  • 生成要用作向量 A 的所有向量,例如:
A: 11100000, 11010000, 11001000, 11000100, ... 00000111
  • 对于其中的每一个,生成所有向量 B,它们在字典上小于向量 A,并且与向量 A 有正确的重叠数量(在示例中为 1 和 2),例如:
A: 10100100
overlap=1:  
B: 10011000, 10010010, 10010001, 10001010, 10001001, 10000011, 01110000, ... 00000111
overlap=2:  
B: 10100010, 10100001, 10010100, 10001100, 10000110, 10000101, 01100100, ... 00100101  
  • 对于其中的每一个,生成所有向量 C,依此类推,直到你有 k 个向量的集合。在生成最后一个向量时,您必须考虑与前一个向量以及下一个(即第一个)向量的重叠。

我认为最好将 x 到 k 的分区视为二叉树:

                   1                                      2
      11                      12                    21         22
111        112           121       122        211       212    221
1112   1121   1122   1211   1212   1221   2111   2112   2121   2211
11122  11212  11221  12112  12121  12211  21112  21121  21211  22111

并在创建解的同时遍历这棵树,这样每个向量只需要生成一次。


我认为这种方法只适用于 n、m 和 k 的某些值;我不确定它是否适用于一般情况。

【讨论】:

  • 我认为这给出了与 Walter 方法相同的结果,看起来更容易编码。但是看看他的示例输出,我认为您可以简单地将所有向量相加,然后将 1 放在最少使用的位置以生成解决方案中的下一个向量。
  • @WalterTross 据我所知,这不是问题,例如对于 n=4, m=2, k=3,[1100, 0110, 0011] 和 [1100, 0011, 1100] 的总汉明距离为 8。
  • @m69 我对你所说的最小距离很感兴趣。但是,在您的示例中,您考虑 1100 和 0011,然后取 1100 并说最小汉明距离为 0。但是,在这种情况下,您正在考虑相同的向量 (1100) 两次,这是不正确的。我的意思是一旦你在集合中有一个向量(例如 1100),你就不能再考虑它了。我没有成功找到您的陈述适用的示例,即对于两个向量的最小汉明距离不相同,其中与集合的总汉明距离最大且相同。
  • @din 啊,我忽略了结果不能包含相同向量的事实。无论如何,我很晚才看到你的问题,我仍在消化问题的所有细节。我可能会在未来几天进一步更新我的答案。您是否有兴趣一次生成所有解决方案或只生成一个随机解决方案,并且均匀分布很重要?
  • @m69 只要没有比它更好的解决方案,一个解决方案就足够了;也许这就是你所说的“均匀分布很重要”(?)的意思。
猜你喜欢
  • 2013-12-12
  • 2022-08-14
  • 1970-01-01
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-06
相关资源
最近更新 更多