如何置换二进制字符串以最小化它们之间的距离?

Posted

技术标签:

【中文标题】如何置换二进制字符串以最小化它们之间的距离?【英文标题】:How to permute binary string so to minimize distance between them? 【发布时间】:2018-08-26 06:56:31 【问题描述】:

我有一个权重数组,例如

[1, 0, 3, 5]

两个字符串之间的距离定义为不同位的权重之和,如下所示:

size_t distance(const std::string& str1, const std::string& str2, const std::vector<size_t>& weights) 
  size_t result = 0;
  for (size_t i = 0; i < str1.size(); ++i) 
    if (str1[i] != str2.at(i))
      result += weights.at(i);
  
  return result;

以起始字符串为例

'1101'

我需要以与原始字符串距离最小的字符串先行的方式生成排列,如下所示:

'1001'  # changed bits: 2nd. Because it has lowest weight. Distance is 0
'0101'  # changed bits: 1st.                               Distance is 1
'0001'  # changed bits: 1st, 2nd.                          Distance is 1
'1011'  # changed bits: 2nd, 3rd.                          Distance is 3
'1111'  # changed bits: 3rd.                               Distance is 3
'0111'  # changed bits: 1st, 3rd.                          Distance is 4
'0011'  # changed bits: 1st, 2nd, 3rd.                     Distance is 4
'1100'  # changed bits: 4th.                               Distance is 5
'1000'  # changed bits: 2nd, 4th.                          Distance is 5
'0100'  # changed bits: 1st, 4th.                          Distance is 6
'0000'  # changed bits: 1st, 2nd, 4th.                     Distance is 6
'1110'  # changed bits: 3rd, 4th.                          Distance is 8
'1010'  # changed bits: 2nd, 3rd, 4th.                     Distance is 8
'0110'  # changed bits: 1st, 3nd, 4th.                     Distance is 9
'0010'  # changed bits: 1st, 2nd, 3rd, 4th.                Distance is 9

我不需要代码,我只需要一个算法,它获取长度为 N 的字符串、相同长度的权重数组和 i 作为输入并生成第 i 个排列,而无需生成整个列表并对其进行排序。

【问题讨论】:

你试过什么?什么不起作用?您要解决的问题不是很清楚 @AlanBirtles,我试着让它更清楚一点 生成相同长度的权重数组。怎么样? 那些并不是真正的排列(排列只会改变顺序)。要生成 '0''1' 的所有四字符字符串,请将 0 到 15 的数字转换为四位二进制数字。 这听起来好像您想根据它们与“1101”的距离对上述所有可能的字符串进行排序。如果是这样,你提到“排列”这个词有点牵强。 【参考方案1】:

听起来是个难题。

如果你使用 size_t 作为排列索引,你的字符串将被限制为 32 或 64 个字符,否则你需要更大的整数作为排列索引。因此,您可以从字符串切换到 size_t 位掩码。

这样,您的算法不再依赖于字符串,您会找到第 i 个位掩码,将其(C++ 中的^ 运算符)与输入字符串位掩码进行异或运算,然后您就会得到结果。困难的部分是找到第 i 个位掩码,但通过这种方式,即在算法的内部循环中不使用字符串,代码会快得多(一个数量级)。

现在最困难的部分是如何找到面具。对于一般情况,我能想到的唯一算法是广泛搜索,可能使用memoisation 来提高性能。这对于较小的排列索引会很快,但对于较大的排列索引会很慢。

如果您在编译时知道权重,则可以将索引预先计算到搜索树中,但最好在 C++ 之外完成,很难将模板元编程用于像这样的复杂算法。

附:有一种特殊情况可能对您有用。对权重进行排序,并检查以下是否为真weights[N] == weights[N-1] || weights[N] &gt;= sum( weights[0 .. N-1] 对于所有 1

顺便说一句,您在问题中的权重满足该条件,因为 1>=0、3>=0+1 和 5>=0+1+3,所以这个简单的算法适用于您的特定权重。

更新:这是一个完整的解决方案。它打印的结果与您的样本略有不同,例如在您的示例中,您有 '1011' 然后 '1111' ,我的代码将在 '1111' 之后立即打印 '1011' 但它们的距离是相同的,即我的算法仍然可以正常工作。

#include <string>
#include <vector>
#include <algorithm>
#include <stdio.h>

struct WeightWithBit

    size_t weight, bit;
;

// Sort the weights while preserving the original order in the separate field
std::vector<WeightWithBit> sortWeights( const std::vector<size_t>& weights )

    std::vector<WeightWithBit> sorted;
    sorted.resize( weights.size() );
    for( size_t i = 0; i < weights.size(); i++ )
    
        sorted[ i ].weight = weights[ i ];
        sorted[ i ].bit = ( (size_t)1 << i );
    
    std::sort( sorted.begin(), sorted.end(), []( const WeightWithBit& a, const WeightWithBit& b )  return a.weight < b.weight;  );
    return sorted;


// Check if the simple bit-based algorithm will work with these weights
bool willFastAlgorithmWork( const std::vector<WeightWithBit>& sorted )

    size_t prev = 0, sum = 0;
    for( const auto& wb : sorted )
    
        const size_t w = wb.weight;
        if( w == prev || w >= sum )
        
            prev = w;
            sum += w;
            continue;
        
        return false;
    
    return true;


size_t bitsFromString( const std::string& s )

    if( s.length() > sizeof( size_t ) * 8 )
        throw std::invalid_argument( "The string's too long, permutation index will overflow" );
    size_t result = 0;
    for( size_t i = 0; i < s.length(); i++ )
        if( s[ i ] != '0' )
            result |= ( (size_t)1 << i );
    return result;


std::string stringFromBits( size_t bits, size_t length )

    std::string result;
    result.reserve( length );
    for( size_t i = 0; i < length; i++, bits = bits >> 1 )
        result += ( bits & 1 ) ? '1' : '0';
    return result;


// Calculate the permitation. Index is 0-based, 0 will return the original string without any changes.
std::string permitation( const std::string& str, const std::vector<WeightWithBit>& weights, size_t index )

    // Reorder the bits to get the bitmask.
    // BTW, if this function is called many times for the same weights, it's a good idea to extract just the ".bit" fields and put it into a separate vector, memory locality will be slightly better.
    size_t reordered = 0;
    for( size_t i = 0; index; i++, index = index >> 1 )
        if( index & 1 )
            reordered |= weights[ i ].bit;

    // Convert string into bits
    const size_t input = bitsFromString( str );
    // Calculate the result by flipping the bits in the input according to the mask.
    const size_t result = input ^ reordered;
    // Convert result to string
    return stringFromBits( result, str.length() );


int main()

    const std::vector<size_t> weights =  1, 0, 3, 5 ;

    using namespace std::literals::string_literals;
    const std::string theString = "1101"s;

    if( weights.size() != theString.length() )
    
        printf( "Size mismatch" );
        return 1;
    

    if( weights.size() > sizeof( size_t ) * 8 )
    
        printf( "The string is too long" );
        return 1;
    

    // Sort weights and check are they suitable for the fast algorithm
    const std::vector<WeightWithBit> sorted = sortWeights( weights );
    if( !willFastAlgorithmWork( sorted ) )
    
        printf( "The weights aren't suitable for the fast algorithm" );
        return 1;
    

    // Print all permutations
    const size_t maxIndex = ( 1 << weights.size() ) - 1;
    for( size_t i = 0; true; i++ )
    
        const std::string p = permitation( theString, sorted, i );
        printf( "%zu: %s\n", i, p.c_str() );
        if( i == maxIndex )
            break;  // Avoid endless loop when the string is exactly 32 or 64 characters.
    
    return 0;

【讨论】:

太棒了,谢谢,但我不完全明白为什么它适用于这种特殊情况【参考方案2】:

在现代 C++ 中,执行您所要求的操作的方法是使用 std::bitset 表示所有可能的位 multiset,然后用 comparer functor struct 包装 distance()请致电std::sort()。我强调可能的位multisets不是permutations,因为后者只允许更改顺序。您的代码将如下所示:

#include <string>
#include <array>
#include <cmath>
#include <bitset>
#include <vector>
#include <algorithm>
#include <iostream>

constexpr size_t BITSET_SIZE = 4;

size_t distance(const std::string& str1, const std::string& str2, const std::array<size_t, BITSET_SIZE>& weights) 
    size_t result = 0;
    for (size_t i = 0; i < str1.size(); ++i) 
        if (str1[i] != str2.at(i))
            result += weights.at(i);
    
    return result;


struct of_lesser_distance

    const std::bitset<BITSET_SIZE>& originalBitSet;
    const std::array<size_t, BITSET_SIZE>& distanceVec;

    inline bool operator() (const std::bitset<BITSET_SIZE>& lhs, const std::bitset<BITSET_SIZE>& rhs)
    
        return distance(originalBitSet.to_string(), lhs.to_string(), distanceVec) < distance(originalBitSet.to_string(), rhs.to_string(), distanceVec);
    
;

int main()

    std::string s"1101";    
    std::array<size_t, 4> weights1, 0, 3, 5;

    int possibleBitSetsCount = std::pow(2, s.length());

    std::vector<std::bitset<BITSET_SIZE>> bitSets;

    // Generates all possible bitsets
    for (auto i = 0; i < possibleBitSetsCount; i++)
        bitSets.emplace_back(i);

    // Sort them according to distance
    std::sort(bitSets.begin(), bitSets.end(), of_lesser_distance std::bitset<BITSET_SIZE>(s), weights );

    // Print
    for (const auto& bitset : bitSets)
        std::cout << bitset.to_string().substr(BITSET_SIZE - s.length(), s.length()) << " Distance: " << distance(s, bitset.to_string(), weights) << "\n";

输出:

1001 Distance: 0
1101 Distance: 0
0001 Distance: 1
0101 Distance: 1
1011 Distance: 3
1111 Distance: 3
0011 Distance: 4
0111 Distance: 4
1000 Distance: 5
1100 Distance: 5
0000 Distance: 6
0100 Distance: 6
1010 Distance: 8
1110 Distance: 8
0010 Distance: 9
0110 Distance: 9

直播版here.

注意:通过这种方式,您最好将distance() 更改为std::bitsets 而不是std::strings,因为这样可以节省所有不必要的转换。

我不需要代码,我只需要一个算法

提供代码对我来说更容易,但如果您需要其他内容,请告诉我。

【讨论】:

不完全是,我希望算法给出第 i 个字符串而不生成整个列表并对其进行排序。 @devalone 我现在看到了。由于您使用 C++ 标记和distance() sn-p 是 C++ 标记了问题,因此您没有指定非生成、非排序要求并且代码答案特别是 C++,因此造成混淆。 .. 也许这是计算机科学堆栈交换的问题...... 是的,我应该指定它,感谢 C++ 表示位的方式 @devalone 很高兴提供任何帮助【参考方案3】:

这个问题无法有效解决。它可以多项式地简化为子集和问题,而子集和问题本身就是一个 NP-Complete 问题。

如果您不介意一个详尽的解决方案,那么只需迭代所有可能的与您的基本字符串长度相同的字符串并使用distance 计算它们的距离并跟踪最大i 距离。

由于对问题的误解而导致的原始错误答案:听起来像一个简单的问题。由于您已经必须生成所有这些字符串,因此您的解决方案相对于基本字符串将是指数的(在空间和时间上)。你基本上不受约束。

您可以尝试类似[1]: 1. 生成与基本字符串长度相同的所有可能字符串。 这很简单。只需从 0 循环到 (2|base_str|-1),然后使用 sprintf(&amp;strs[loop_counter]"%b", loop_counter) 2. 使用qsortstrs 进行排序,并使用distance 作为比较器。类似于qsort(str, 1 &lt;&lt; strlen(base_str)-1, sizeof(char*), comp) 的东西,其中comp 是一个接收两个字符串的函数,如果第一个字符串到base_str 的距离小于第二个字符串,则返回-1,如果两个字符串的距离相等,则返回0,如果第一个字符串距离base_str 比第二个字符串更远,则返回1。第二个论点。

[1]我是 C 而不是 C++ 程序员,所以我确信还有其他(也许更好)的方法可以按照我在 C++ 中的建议进行操作,但我的示例是在 C 中.

【讨论】:

你没有明白,我需要一个算法来从列表中获取第 n 个字符串,而不是整个列表。类似std::string getPermutation(const std::string&amp; original, const std::vector&lt;size_t&gt;&amp; weights, size_t n); 哦,这是一个更有趣的问题。我会尝试看看我是否找到任何见解。如果我不会,我会删除这个答案。 @Neowizard “这个问题无法有效解决” 有时可以。检查我的答案,了解适用于 OP 权重的非常快速的方法。 你提出的条件很受限制。我认为它不适用于该域的足够大部分。基本上,它要求权重列表的下限为几何级数(忽略等于其前身的元素)。【参考方案4】:

如果您只想要第 i 个排列,那么您只需要查看权重即可。

如果权重是反向排序的,比如[5,3,1,0],并且您想要第 5 个排列,那么您需要将0, 1, 0, 1 翻转为二进制的5 = 0101

因此,您需要一个从权重到原始索引的非常小的映射。然后,从大到小排序,根据N的二进制表示抓取第N个排列,并翻转原始字符串的映射位。

【讨论】:

“基于 N 的二进制表示获取第 N 个排列”权重为 [4, 5, 6],二进制表示将在 6 之前返回距离 4+5=9。这仅适用于某些权重 (例如适用于 [1, 0, 3, 5]),请参阅我的实施答案。 @soonts 你是对的,我没想到。仅适用于素数权重。

以上是关于如何置换二进制字符串以最小化它们之间的距离?的主要内容,如果未能解决你的问题,请参考以下文章

在Matlab中计算两个二进制数字串之间的汉明距离

算法 - 计算汉明距离

遗传算法的基本原理

461. 汉明距离

最小汉明距离

双字节距离显示一半的值