【发布时间】:2021-12-07 06:55:09
【问题描述】:
我正在尝试找到一个比我已经找到的解决方案更有效的组合问题解决方案。
假设我有一组 N 个对象(索引 0..N-1)并希望考虑每个子集的大小 K (0)。有 S=C(N,K)(即“N 选择 K”)这样的子集。我希望将每个这样的子集映射(或“编码”)到 0..S-1 范围内的唯一整数。
使用 N=7(即,索引为 0..6)和 K=4(S=35) 为例,以下映射是目标:
0 1 2 3 --> 0
0 1 2 4 --> 1
...
2 4 5 6 --> 33
3 4 5 6 --> 34
N 和 K 被选为较小的用于说明目的。但是,在我的实际应用程序中,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 offset{combinations_table[N1][K1] - combinations_table[N1 - indexes[0]][K1]};
for (typename T::value_type index{1}; 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<> 或 std::vector<> ,其中包含我们希望为其查找编码的索引集合。
这本质上是一个“保序最小完美哈希函数”,可以在此处阅读:
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 offset{0};
typename T::value_type previous_index_selection{0};
for (typename T::value_type index{0}; index < K1; ++index)
{
auto lowest_possible{index > 0 ? previous_index_selection + 1 : 0};
auto highest_possible{N1 - 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_candidate{candidate + 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讨论
标签: algorithm performance combinations