【问题标题】:Fill dynamic size vector recursively递归填充动态大小向量
【发布时间】:2011-09-15 07:45:00
【问题描述】:

也许让我先用伪 C++ 代码说明我的情况:

std:vector<double> sample(someFunctor f, double lower, double upper) {
    double t = (lower + upper)/2;
    double newval = f(t);

    if (f(upper) - newval > epsilon)
        subsample1 = sample(f, t, upper);
    if (newval - f(lower) > epsilon)
        subsample2 = sample(f, lower, t);

    return concat(subsample2, newval, subsample1);
}

concat 只是连接返回的向量。基本上,我以一种方式对函数进行采样,使得两个保存的函数值之间只有很小的差异。

我对上述方式不满意,因为在每个递归步骤中似乎都有相当多的内存分配(分配两个子向量,然后连接它们和另一个元素)。那段代码必须在我的算法的一部分中运行,这对性能至关重要。一旦upper - lower 相当小,评估f 将不会花费大量时间。

所以我的问题:

  • 您是否发现了一种在所有递归调用中使用相同数据结构并仅填充该向量的当前部分的巧妙方法? (请记住,所需的函数评估数量是未知的)。对此的想法:

    • 使用列表而不是向量。但我觉得内存大修不足以存储双打。
    • 在向量中保留孔并维护另一个向量,说明哪些条目已被填充。递归调用的结束移动条目,以便subsamples 和newval 之间没有空洞。但现在我通过转移第二个向量的额外工作来切换复制 - 可能是个坏主意。
  • 您是否找到完全摆脱递归的方法?但是,为了正确起见,我使用上述分而治之的模式很重要。 f 函数大量使用了上下界,从而获得了相当大的性能。

感谢您的意见。


根据 Space_C0wb0y 的要求,让我尝试重新表述我的问题。可能第一个解释不是很清楚。

我有一些函数(在数学意义上)我想在给定的时间间隔内采样(例如,在某些点进行评估)。

假设区间为 [0,100]。我知道函数值为 0 和 100。也许是 f(0)=0f(100) = 40。 现在我在间隔中点(即 50)处评估函数。比如说,我的函数返回 f(50)=10。 作为f(0)-f(50) &lt;= 10,我不需要[0,50]区间内的更多样本。但是,我需要进一步计算区间 [50,100]。因此,在下一个(递归)步骤中,我评估f(75)。现在递归地重复上面的逻辑。

最后我想(两个)向量给我带有相应参数的函数值,如下所示:

parameter  = vector(0, 50, 56.25, 62.5, 75, 100)
value      = vector(0, 10, 17.21, 25    34,  40)

我正在寻找递归构建这些向量的最佳(性能最高)方法。

希望这可以澄清事情。

【问题讨论】:

  • 我不太明白你的问题。你能举一个输入和预期输出的例子吗?
  • 这里的“性能关键”是指时间关键还是空间关键?大多数时候它们是矛盾的。
  • @Space_C0wb0y:为问题添加了另一种方法。希望这会有所帮助。
  • @Eric:就算法期间所需的空间而言,空间是一个次要问题。然而,结果应该尽可能紧凑。时间最优性是我的主要关注点。

标签: c++ recursion vector divide-and-conquer


【解决方案1】:

由于空间不是您主要关心的问题,所以我将继续使用递归。

1.使用按引用复制而不是按(返回)值复制。

2。不需要传入函子,因为它是常数。

3.如果lowhigh 是整数,它可能会更快。不过,这取决于要求。

    // Thanks to Space_C0wb0y, here we avoid using a global vector
    // by passing the vector as reference. It's efficient as there
    // is no copy overhead as well.        
    void sample(vector<double>& samples, double low, double high)
    {
       // You can use shift operator if they're integers.
       double mid = (low + high)/2;

       // Since they're double, you need prevent them from being too close.
       // Otherwise, you'll probably see stack overflow.
       // Consider this case:
       // f(x): x=1, 0<x<8;  x*x, x<=0 or x>=8
       // low = 1, high = 10, epsilon = 10
       if (high - low < 0.5)
       {
          samples.push_back(f(mid));
          return;
       }   

       // The order you write the recursive calls guarantees you
       // the sampling order is from left to right.
       if (f(mid) - f(low) > epsilon)
       {
          sample(samples, low, mid);
       }

       samples.push_back(f(mid));

       if (f(high) - f(mid) > epsilon)
       {
          sample(samples, mid, high);
       }   
    }

【讨论】:

  • 谢谢,埃里克!我缺少的链接之一是订单以这种方式保存的简单事实。这基本上就是我一直在寻找的技术。只是出于兴趣:您将如何删除递归?
  • 基本思想是检测f(x)的最大梯度。这可以使用带有while循环的分而治之来完成。基于此,您可以计算可能的采样“步骤”。之后,您可以“逐步”迭代所有样本。
  • @Eric:我喜欢你如何保持订单的想法,但我不喜欢使用全局向量的想法。 Globals should usually be avoided.
  • @Eric:好的,在我描述的情况下,这将是一个选项。然而, f 背后的数学迫使我使用分治法来进行函数评估。逐步使用我会失去 f 的单调性,这会杀死整个算法:)
  • @Space_C0wb0y:是的,通常是这样,尤其是在可写全局变量将被复制到页面文件的窗口上。但是,如果您的软件不是大规模且设计良好的(例如线程安全的),那还不错。与其说是性能问题,不如说是设计问题。
【解决方案2】:

我会推荐以下方法:

  1. 不要使用两个向量,而是使用一个成对的向量或自定义struct 来表示参数和值:

    struct eval_point {
        double parameter;
        double value;
    };
    
    std::vector<eval_point> evaluated_points;
    
  2. 更改您的算法以将评估结果写入输出迭代器:

    template<class F, class output_iterator_type>
    void sample(F someFunctor, double lower, double upper,
                output_iterator_type out) {
        double t = (lower + upper)/2;
        eval_point point = { t, f(t) };
    
        if (f(upper) - point.value > epsilon) {
            *out = point;
            ++out;
            sample(f, t, upper, out);
        }
        if (point.value - f(lower) > epsilon) {
            *out = point;
            ++out;
            subsample2 = sample(f, lower, t, out);
        }
    }
    

    以上是对伪代码的修改,显示了使用输出迭代器时的样子。它没有经过测试,所以我不确定它是否正确。原则上,您可以这样称呼它:

    std::vector<eval_point> results;
    sample(someFunction, 0, 100, std::back_inserter<eval_point>(results));
    

    这样您就不必为每个递归调用创建新向量。如果您能猜出样本数量的合理下限,您可能能够预先分配,这样就不需要重新分配。在这种情况下,您可以这样称呼它:

    std::vector<eval_point> results(lower_bound_for_samples);
    sample(someFunction, 0, 100, results.begin());
    

然后您必须添加一个额外的计数器来跟踪生成了多少样本。

【讨论】:

  • 感谢您的意见。当然是一个有趣的想法。这意味着放弃我为结果向量假设的顺序。当然,这可以通过简单地事后排序来解决。
  • 好的,顺序用Eric的思路没问题,这里也可以应用。最终它可能会介于这和 Eric 的解决方案之间。
  • @Thilo:Eric 保持排序的方法很好,但请不要按照他的建议使用全局向量来存储结果(请参阅我对他的回答的评论)。
  • @Space_C0wb0y:我不打算使用全局变量。目前我正在考虑制作一个封装所有逻辑以及更多内容的“采样”类。这将非常适合我的设计,并允许我使用该向量的成员变量以及仿函数所需的信息。有异议吗?
  • @Thilo:听起来不错。只是确保:)
【解决方案3】:

我不明白您为什么拒绝列表解决方案。 最坏的情况是您的列表的大小是原始数据的 3 倍。 我认为这远远少于在每个函数调用上创建新向量时所拥有的。 您应该尝试一下,因为它不需要太多更改,因为两者的界面几乎相同。

【讨论】:

  • 我并不完全拒绝这份名单。然而,我的这两个想法都需要在算法过程中分配相当多的内存——这意味着需要做很多工作。 Space_C0wb0y 的解决方案不需要额外的 malloc。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-18
  • 2012-05-14
  • 1970-01-01
  • 1970-01-01
  • 2017-06-11
  • 1970-01-01
  • 2011-07-17
相关资源
最近更新 更多