在优于 O(K*lg N) 的运行时间内反转保序最小完美散列函数

Posted

技术标签:

【中文标题】在优于 O(K*lg N) 的运行时间内反转保序最小完美散列函数【英文标题】:Inverting an Order-Preserving Minimal Perfect Hash Function in Better than O(K*lg N) Running Time 【发布时间】:2021-12-07 06:55:09 【问题描述】:

我正在尝试找到一个比我已经找到的解决方案更有效的组合问题解决方案。

假设我有一组 N 个对象(索引 0..N-1)并希望考虑每个子集的大小 K0)。有 S=C(N,K)(即“N 选择 K”)这样的子集。我希望将每个这样的子集映射(或“编码”)到 0..S-1 范围内的唯一整数。

使用 N=7(即,索引为 0..6)和 K=4S=35) 为例,以下映射是目标:0 1 2 3 --> 0 0 1 2 4 --> 1 ... 2 4 5 6 --> 33 3 4 5 6 --> 34

NK 被选为较小的用于说明目的。但是,在我的实际应用程序中,C(N,K) 太大而无法从查找表中获取这些映射。它们必须即时计算。

在下面的代码中,combinations_table 是一个预先计算好的二维数组,用于快速查找 C(N,K) 值。

给出的所有代码都符合 C++14 标准。

如果子集中的对象按其索引的递增顺序排序,则以下代码将计算该子集的编码:

template<typename T, typename T::value_type N1, typename T::value_type K1>
typename T::value_type combination_encoder_t<T, N1, K1>::encode(const T &indexes)

   auto offsetcombinations_table[N1][K1] - combinations_table[N1 - indexes[0]][K1];

   for (typename T::value_type index1; index < K1; ++index)
   
      auto offset_due_to_current_index
           combinations_table[N1 - (indexes[index-1] + 1)][K1 - index] -
           combinations_table[N1 - indexes[index]][K1 - index]
                                      ;

      offset += offset_due_to_current_index;
   

   return offset;

这里,模板参数 T 将是 std::array&lt;&gt;std::vector&lt;&gt; ,其中包含我们希望为其查找编码的索引集合。

这本质上是一个“保序最小完美哈希函数”,可以在此处阅读:https://en.wikipedia.org/wiki/Perfect_hash_function

在我的应用程序中,子集中的对象在编码时已经自然排序,因此我不会增加排序操作的运行时间。因此,我的编码总运行时间是上面介绍的算法的运行时间,它具有 O(K) 运行时间(即,在 K 中是线性的,并且不依赖于 N)。

上面的代码运行良好。有趣的部分是试图反转这个函数(即将编码值“解码”回产生它的对象索引)。

对于解码,我想不出线性运行时间的解决方案。

我没有直接计算对应于编码值的索引(这将是 O(K)),而是最终实现了索引空间的二进制搜索来找到它们。这导致运行时间(不比,但我们称之为)O(K*lg N)。执行此操作的代码如下:

template<typename T, typename T::value_type N1, typename T::value_type K1>
void combination_encoder_t<T, N1, K1>::decode(const typename T::value_type encoded_value, T &indexes)

   typename T::value_type offset0;
   typename T::value_type previous_index_selection0;

   for (typename T::value_type index0; index < K1; ++index)
   
      auto lowest_possibleindex > 0 ? previous_index_selection + 1 : 0;
      auto highest_possibleN1 - K1 + index;

      // Find the *highest* ith index value whose offset increase gives a
      // total offset less than or equal to the value we're decoding.
      while (true)
      
         auto candidate(highest_possible + lowest_possible) / 2;

         auto offset_increase_due_to_candidate
                   index > 0 ?
                      combinations_table[N1 - (indexes[index-1] + 1)][K1 - index] -
                      combinations_table[N1 - candidate][K1 - index]
                             :
                      combinations_table[N1][K1] -
                      combinations_table[N1 - candidate][K1]
                                              ;

         if ((offset + offset_increase_due_to_candidate) > encoded_value)
         
            // candidate is *not* the solution
            highest_possible = candidate - 1;
            continue;
         

         // candidate *could* be the solution. Check if it is by checking if candidate + 1
         // could be the solution. That would rule out candidate being the solution.
         auto next_candidatecandidate + 1;

         auto offset_increase_due_to_next_candidate
                   index > 0 ?
                      combinations_table[N1 - (indexes[index-1] + 1)][K1 - index] -
                      combinations_table[N1 - next_candidate][K1 - index]
                             :
                      combinations_table[N1][K1] -
                      combinations_table[N1 - next_candidate][K1]
                                                   ;

         if ((offset + offset_increase_due_to_next_candidate) <= encoded_value)
         
            // candidate is *not* the solution
            lowest_possible = next_candidate;
            continue;
         

         // candidate *is* the solution
         offset += offset_increase_due_to_candidate;
         indexes[index] = candidate;
         previous_index_selection = candidate;
         break;
      
   

这可以改进吗?我正在寻找两类改进:

    O(K*lg N) 产生更好的算法改进 给定代码的运行时间;理想情况下,直接计算是可能的,给出与编码过程相同的 O(K) 运行时间 执行代码的改进 给定算法更快(即,降低隐藏的任何常数因子 O(K*lg N) 运行时间内)

【问题讨论】:

我不明白,你是怎么得到O(log N)运行时间的?你的外循环是O(K),所以它至少应该是 O( K * ? ) 或 O( K + ? )。你有证据证明两个循环都会产生 O(log(N)) 的运行时间吗?我怀疑它实际上类似于 O(K + N) 并且不可能做得更好。这肯定不是 O(log(N)),因为您正在填充结果,即 O(K)。 您可能想在 stackexchange 网站的计算机科学、数学或数学下发布此内容 Aivean,关于 O(lg N) 运行时间,您是正确的。我已经更正了我关于运行时间的陈述,并且我也试图做出其他澄清。 这个问题正在meta讨论 【参考方案1】:

看看recursive formula for combinations:


假设您有一个组合空间C(n,k)。您可以将该空间划分为两个子空间:

C(n-1,k-1) 所有组合,其中存在原始集合的第一个元素(长度为 nC(n-1, k) 第一个元素未预设

如果你有一个索引 X 对应于来自C(n,k) 的组合,你可以确定你的原始集合的第一个元素是否属于子集(对应于X),如果你检查是否X属于任一子空间:

X &lt; C(n-1, k-1):属于 X &gt;= C(n-1, k-1): 不属于

然后您可以递归地对C(n-1, ...) 应用相同的方法,依此类推,直到找到原始集合中所有n 元素的答案。


说明这种方法的 Python 代码:

import itertools, math

n=7
k=4
stuff = list(range(n))

# function that maps x into the corresponding combination
def rec(x, n, k, index):
  if n==0 and k == 0:
    return index

  # C(n,k) = C(n-1,k-1) + C(n-1, k)
  # C(n,0) = C(n,n) = 1
  c = math.comb(n-1, k-1) if k > 0 else 0
  if x < c:
    index.add(stuff[len(stuff)-n])
    return rec(x, n-1, k-1, index)
  else:
    return rec(x - c, n-1, k, index)

# Test:
for i,eta in enumerate(itertools.combinations(stuff, k)):
  comb = rec(i, n, k, set())
  print(f'i eta comb')

产生的输出:

0 (0, 1, 2, 3) 0, 1, 2, 3
1 (0, 1, 2, 4) 0, 1, 2, 4
2 (0, 1, 2, 5) 0, 1, 2, 5
3 (0, 1, 2, 6) 0, 1, 2, 6
4 (0, 1, 3, 4) 0, 1, 3, 4
5 (0, 1, 3, 5) 0, 1, 3, 5
...
33 (2, 4, 5, 6) 2, 4, 5, 6
34 (3, 4, 5, 6) 3, 4, 5, 6

这种方法是 O(n) (而您的方法似乎是 O( k * log(n) ) (?) ),如果迭代地重写它应该有相当小的常数。我不确定它是否会产生改进(需要测试)。

我还想知道您的典型 kn 值有多大?我认为它们应该足够小,以便 C(n,k) 仍然适合 64 位?

当然,您可以使用预计算表代替math.comb,将递归替换为迭代(它是尾递归,因此您不需要堆栈),并使用数组代替结果集。

【讨论】:

Aivean,我关心的大多数案例都有适合 64 位的 C(N,K),但有些不适合!所以,我希望我有 128 位整数,但我没有,所以我现在将忽略这些情况。 Aivean,你说得对,我的方法有 O(K*lg N) 时间,我已经更新了我的帖子以更正我的陈述。谢谢! @Dave,您考虑过我提出的方法吗?虽然它是 O(n)(技术上是 O(n+k)),但如果仔细重写,它在实践中更简单并且可能更快。不幸的是,我没有时间对这两种解决方案进行基准测试。 Aivean,抱歉,昨晚来晚了。哦,是的,我肯定考虑过你的解决方案!我只需要考虑清楚,而且我还必须安装 Python 3.8 才能运行它。我将在 C++ 中实现它,并看看它在实践中是否运行得更快。我有一种感觉,我(修订后的)声明的解码索引的“直接计算”目标(我最初称之为“封闭形式的解决方案”)是不可能的,并且某种搜索是不可避免的。因此,我可能会遵循@tarik 的建议,将其发布在数学论坛上。出色的工作,谢谢! Aivean,对不起,我没有完全回答你关于 N 和 K 的问题。迟到了!在我的应用程序中,N 固定为 52。5 【参考方案2】:

为了将来参考,我想添加 @aivean 给出的算法改进的 C++ 实现(这被证明非常有效),用于将编码值解码回产生它的索引。

与原帖一样,combinations_table 是一个预先计算的二维数组,用于快速查找 C(N,K) 值。

template<typename T, typename T::value_type N1, typename T::value_type K1>
void combination_encoder_t<T, N1, K1>::decode(const typename T::value_type encoded_value, T &indexes)

   auto nN1;
   auto kK1;
   auto x(encoded_value);
   T1 index0;

   while (k != 0)
   
      auto ccombinations_table[n-1][k-1];

      if (x < c)
      
         indexes[index++] = N1 - n;
         --k;
      
      else
         x -= c;

      --n;
   

【讨论】:

以上是关于在优于 O(K*lg N) 的运行时间内反转保序最小完美散列函数的主要内容,如果未能解决你的问题,请参考以下文章

在 O(n) 的复杂度内反转 SHA-256 sigma0 函数?

04-1_线性表的操作

如何在 O(n) 时间内找到与 n 个不同数字的中位数最近的 k 个邻居?

这个计算 a^n 的算法是如何被重写以在 O(log n) 时间内运行的?

在 O(nlogk) 时间内找到长度为 n 的列表中总和小于 t 的 k 个元素

锤子线性的反转性预测