【发布时间】:2011-05-06 04:59:47
【问题描述】:
partial_sum 算法在STL 中的实际用途是什么/在哪里?
还有哪些其他有趣/重要的示例或用例?
【问题讨论】:
标签: c++ algorithm stl sum partial
partial_sum 算法在STL 中的实际用途是什么/在哪里?
还有哪些其他有趣/重要的示例或用例?
【问题讨论】:
标签: c++ algorithm stl sum partial
我用它来减少我的玩具 lambda 演算解释器中一个简单的标记清除垃圾收集器的内存使用量。
GC 池是一个大小相同的对象数组。目标是消除未链接到其他对象的对象,并将剩余对象压缩到数组的开头。由于对象在内存中移动,因此每个链接都需要更新。这需要一个对象重映射表。
partial_sum 允许以压缩格式存储表(每个对象只有一位),直到扫描完成并释放内存。由于对象很小,这显着减少了内存使用。
remove_if 将标记的对象压缩到池的开头。partial_sum 以生成指向新池的指针/索引表。
将重映射表放在刚刚释放的内存中,对数据缓存特别友好。
【讨论】:
partial_sum。
partial_sum 究竟是如何帮助你的。因此,如果您可以发布一些代码以使事情易于理解,那就太好了。
关于部分和要注意的一点是,它是撤消相邻差异的操作,很像 - 撤消 +。或者更好的是,如果您还记得微积分是微分消除积分的方式。更好,因为相邻的差异本质上是微分,部分和是积分。
假设您模拟了一辆汽车,并且在每个时间步,您都需要知道位置、速度和加速度。您只需要存储其中一个值,就可以计算另外两个值。假设您在每个时间步存储位置,您可以将位置的相邻差值给出速度,并将速度的相邻差值给出加速度。或者,如果你存储加速度,你可以取部分和来给出速度,而速度的部分和给出位置。
部分求和是大多数人不会经常使用的功能之一,但当您找到合适的情况时会非常有用。很像微积分。
【讨论】:
我上次(本来)使用它是在将离散概率分布(p(X = k) 的数组)转换为累积分布(p(X
不过,该代码不在 C++ 中,所以我自己做了部分求和。
【讨论】:
您可以使用它来生成单调递增的数字序列。例如,下面会生成一个包含数字 1 到 42 的 vector:
std::vector<int> v(42, 1);
std::partial_sum(v.begin(), v.end(), v.begin());
这是一个日常用例吗?可能不会,尽管我发现它在很多场合都很有用。
您还可以使用std::partial_sum 生成阶乘列表。 (不过,这甚至更没用,因为可以用典型整数数据类型表示的阶乘数量非常有限。不过这很有趣 :-D)
std::vector<int> v(10, 1);
std::partial_sum(v.begin(), v.end(), v.begin());
std::partial_sum(v.begin(), v.end(), v.begin(), std::multiplies<int>());
【讨论】:
iota 在 C++0x 中死而复生。
partial_sum 的东西。这肯定没有像 Potatoswatter 的回答那样令人兴奋。
个人用例:轮盘赌-轮盘选择
我在轮盘赌选择算法中使用partial_sum (link text)。该算法从容器中随机选择元素,其概率与预先给定的某个值成线性关系。
因为我的所有元素都可以从带来不必要的标准化值中进行选择,所以我使用partial_sum 算法来构建类似“轮盘赌”的东西,因为我总结了所有元素。然后我在这个范围内选择了一个随机变量(最后一个partial_sum 是所有变量的总和)并使用stl::lower_bound 搜索我的随机搜索到达的“轮子”。 lower_bound 算法返回的元素是被选中的元素。
除了使用partial_sum 的代码清晰和富有表现力的优势之外,我还可以在尝试GCC 并行模式时获得一些速度,该模式为某些算法带来并行化版本,其中之一是 partial_sum (@ 987654323@)。
我知道的另一个用途:并行处理中最重要的算法原语之一(但可能离 STL 有点远)
如果您对使用 partial_sum 的重度优化算法感兴趣(在这种情况下,同义词“scan”或“prefix_sum”下的结果可能会更多),请访问并行算法社区。他们一直需要它。如果不使用它,您将找不到基于quicksort 或mergesort 的并行排序算法。此操作是使用的最重要的并行原语之一。我认为它最常用于计算动态算法中的偏移量。想想快速排序中的一个分区步骤,它被拆分并馈送到并行线程。在计算之前,您不知道分区的每个插槽中的元素数量。因此,您需要为所有线程设置一些偏移量以供以后访问。
也许您会在GPU 处理这个现在热门的话题中找到更多信息。一篇关于 Nvidia 的 CUDA 和 scan-primitive 的短文以及一些应用示例,您可以在 Chapter 39. Parallel Prefix Sum (Scan) with CUDA 中找到。
【讨论】:
个人用例:从 CLRS 计数排序的中间步骤:
COUNTING_SORT (A, B, k)
for i ← 1 to k do
c[i] ← 0
for j ← 1 to n do
c[A[j]] ← c[A[j]] + 1
//c[i] now contains the number of elements equal to i
// std::partial_sum here
for i ← 2 to k do
c[i] ← c[i] + c[i-1]
// c[i] now contains the number of elements ≤ i
for j ← n downto 1 do
B[c[A[i]]] ← A[j]
c[A[i]] ← c[A[j]] - 1
【讨论】:
您可以建立一个“移动总和”(移动平均线的前身):
template <class T>
void moving_sum (const vector<T>& in, int num, vector<T>& out)
{
// cummulative sum
partial_sum (in.begin(), in.end(), out.begin());
// shift and subtract
int j;
for (int i = out.size() - 1; i >= 0; i--) {
j = i - num;
if (j >= 0)
out[i] -= out[j];
}
}
然后调用它:
vector<double> v(10);
// fill in v
vector<double> v2 (v.size());
moving_sum (v, 3, v2);
【讨论】:
partial_sum 之前的移位和减法不太可能溢出。
moving_sum 将迭代器作为参数,而不是vector。
你知道,我实际上确实使用过一次 partial_sum()... 这是一个有趣的小问题,我在一次工作面试中被问到。我非常喜欢它,我回家编码了。
问题是:给定一个连续的整数序列,找到具有最高值的最短子序列。例如。给定:
Value: -1 2 3 -1 4 -2 -4 5
Index: 0 1 2 3 4 5 6 7
我们会找到子序列 [1,4]
现在显而易见的解决方案是运行 3 个 for 循环,遍历所有可能的开始和结束,并依次将每个可能的子序列的值相加。效率低下,但可以快速编写代码并且很难出错。 (特别是当第三个 for 循环只是 accumulate(start,end,0) 时。)
正确的解决方案涉及分而治之/自下而上的方法。例如。将问题空间分成两半,对每一半计算该部分中包含的最大子序列、包括起始编号的最大子序列、包括结束编号的最大子序列以及整个部分的子序列。有了这些数据,我们就可以将这两部分组合在一起,而无需对任何一个部分进行进一步评估。显然,每一半的数据可以通过进一步将每一半分成两半(四分之一),每四分之一分成两半(八分之一),依此类推,直到我们有琐碎的单例。这一切都非常有效。
但除此之外,我还想探索第三种(效率稍低)的选项。它类似于 3-for-loop 的情况,只是我们将相邻的数字相加以避免太多工作。这个想法是,当我们可以添加 t1=a+b、t2=t1+c 和 t3=t2+d 时,不需要添加 a+b、a+b+c 和 a+b+c+d。这是一个空间/计算权衡的事情。它通过转换序列来工作:
Index: 0 1 2 3 4
FROM: 1 2 3 4 5
TO: 1 3 6 10 15
从而为我们提供了从 index=0 开始到 index=0,1,2,3,4 结束的所有可能的子字符串。
然后我们迭代这个集合,减去连续的可能“开始”点......
FROM: 1 3 6 10 15
TO: - 2 5 9 14
TO: - - 3 7 12
TO: - - - 4 9
TO: - - - - 5
从而为我们提供所有可能子序列的值(总和)。
我们可以通过max_element()找到每次迭代的最大值。
通过 partial_sum() 最容易完成第一步。
剩下的步骤通过 for 循环和 transform(data+i,data+size,data+i,bind2nd(minus
显然 O(N^2)。但仍然有趣和有趣......
【讨论】:
部分和通常在并行算法中很有用。考虑代码
for (int i=0; N>i; ++i) {
sum += x[i];
do_something(sum);
}
如果你想并行化这段代码,你需要知道部分和。我正在使用 GNUs 并行版本的 partial_sum 来做一些非常相似的事情。
【讨论】:
我经常使用部分求和,不是求和,而是根据前面的顺序计算序列中的当前值。
例如,如果您集成一个功能。每个新步骤都是上一步,vt += dvdt 或 vt = integrate_step(dvdt, t_prev, t_prev+dt);。
【讨论】:
在非参数贝叶斯方法中,有一个 Metropolis-Hastings 步骤(每次观察),用于确定对新的或现有的集群进行采样。如果必须对现有集群进行采样,则需要使用不同的权重来完成。这些加权可能性在以下示例代码中进行了模拟。
#include <random>
#include <iostream>
#include <algorithm>
int main() {
std::default_random_engine generator(std::random_device{}());
std::uniform_real_distribution<double> distribution(0.0,1.0);
int K = 8;
std::vector<double> weighted_likelihood(K);
for (int i = 0; i < K; ++i) {
weighted_likelihood[i] = i*10;
}
std::cout << "Weighted likelihood: ";
for (auto i: weighted_likelihood) std::cout << i << ' ';
std::cout << std::endl;
std::vector<double> cumsum_likelihood(K);
std::partial_sum(weighted_likelihood.begin(), weighted_likelihood.end(), cumsum_likelihood.begin());
std::cout << "Cumulative sum of weighted likelihood: ";
for (auto i: cumsum_likelihood) std::cout << i << ' ';
std::cout << std::endl;
std::vector<int> frequency(K);
int N = 280000;
for (int i = 0; i < N; ++i) {
double pick = distribution(generator) * cumsum_likelihood.back();
auto lower = std::lower_bound(cumsum_likelihood.begin(), cumsum_likelihood.end(), pick);
int index = std::distance(cumsum_likelihood.begin(), lower);
frequency[index]++;
}
std::cout << "Frequencies: ";
for (auto i: frequency) std::cout << i << ' ';
std::cout << std::endl;
}
请注意,这与https://stackoverflow.com/users/13005/steve-jessop 的答案没有什么不同。添加它是为了提供有关特定情况的更多上下文(非参数贝叶斯方法,例如 Neal 使用 Dirichlet 过程作为先验的算法)和使用 partial_sum 结合 lower_bound 的实际代码。
【讨论】:
std::lower_bound!该文档提到它返回大于或等于边界的第一个元素。为了避免在序列中排在第一位时选择 0 概率,我建议改用 std::upper_bound。只会选择高于(不等于)随机选择的值(并且永远不会选择 0 概率)。您需要确保您的随机生成器是独占的(从不返回随机范围的上限),以便在所有情况下都能正常工作。 uniform_real_distribution 例如这样做。