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

时间:2018-08-26 06:56:32

标签: c++ algorithm permutation

例如,我有一系列砝码

[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个排列,而无需生成整个列表并对其进行排序。

4 个答案:

答案 0 :(得分:1)

在现代C ++中,按照您的要求去做的方法是使用std::bitset表示所有可能的 multiset s位,然后用{ comparer functor struct以呼叫std::sort()。我强调可能的multisets 位,permutations ,因为后者仅允许更改顺序。然后,您的代码将如下所示:

distance()

输出:

#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> weights{1, 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";
}

实时版本here

注意:这样一来,您最好将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 更改为在distance()而不是std::bitset上运行,因为这样可以节省所有不必要的转换。

  

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

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

答案 1 :(得分:1)

听起来像困难的问题。

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

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

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

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

P.S。有一种特殊情况可能对您有用。对权重进行排序,并针对所有1 weights[N] == weights[N-1] || weights[N] >= sum( weights[0 .. N-1],您只需要在已排序的权重上执行一个循环即可进行检查。如果所有权重均适用,并且所有权重均为非负数,则解决方案将非常简单,并且性能将非常快,只需将索引视为XOR位掩码即可。您唯一需要做的就是对索引中的位重新排序,以匹配权重数组的排序结果,这些权重数组的排序结果已更改。为了权重,请切换第一位和第二位,因为排序顺序为[0,1,3,5]。

顺便说一句,问题中的权重满足该条件,因为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 :(得分:0)

此问题无法有效解决。可以将其多项式化为子集和问题,而子集和问题本身就是一个NP完全问题。

如果您不介意详尽的解决方案,则可以仅迭代与基础字符串相同长度的所有可能字符串,然后使用distance计算其距离并跟踪最大i距离。

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

您可以尝试类似 [1]
 1.生成所有长度与基本字符串相同的字符串。 这很简单。只需从0循环到(2 | base_str | -1),然后使用sprintf(&strs[loop_counter]"%b", loop_counter)
 2.使用strsqsort进行排序,并使用distance作为编译器。类似于qsort(str, 1 << strlen(base_str)-1, sizeof(char*), comp)的事物,其中comp是一个接受两个字符串的函数,如果第一个到base_str的距离小于第二个字符串,则返回-1,如果两个具有相等的距离,则返回0,如果第一个更远则返回1来自base_str而不是第二个参数。

[1] 我是C而不是C ++程序员,所以我确定还有其他(也许更好)的方法可以完成我在C ++中建议的工作,但我的示例在C中。

答案 3 :(得分:0)

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

如果权重是反向排序的,例如[5,3,1,0],而您想进行第5次置换,那么您就需要将0, 1, 0, 1翻转为5 = 0101的二进制文件。

因此,您需要从权重到原始索引的很小映射。然后,按从大到小的顺序排序,基于N的二进制表示形式获取第N个置换,然后翻转原始字符串的映射位。