46

std::accumulateなぜ(別名reduce)3番目のパラメーターが必要なのか知りたいです。よくわからない人accumulateのために、次のように使います。

vector<int> V{1,2,3};  
int sum = accumulate(V.begin(), V.end(), 0);
// sum == 6

への呼び出しaccumulateは次と同等です。

sum = 0;  // 0 - value of 3rd param
for (auto x : V)  sum += x;

オプションの 4 番目のパラメーターもあり、加算を他の操作に置き換えることができます。

私が聞いた理論的根拠は、ベクトルの要素を合計するのではなく乗算する必要がある場合、他の (ゼロ以外の) 初期値が必要であるということです。

vector<int> V{1,2,3};
int product = accumulate(V.begin(), V.end(), 1, multiplies<int>());

しかし、なぜPythonのようにしないのですか? の初期値を設定しV.begin()、 から始まる範囲を使用してV.begin()+1ください。このようなもの:

int sum = accumulate(V.begin()+1, V.end(), V.begin());

これはどの操作でも機能します。なぜ3番目のパラメータが必要なのですか?

4

5 に答える 5

40

あなたは間違った仮定をしています:その型Tは と同じ型InputIteratorです。

しかしstd::accumulate、一般的であり、さまざまな種類の創造的な蓄積と削減を可能にします。

例 #1: 従業員全体の給与を累積する

簡単な例を次に示しEmployeeます。多くのデータ フィールドを持つクラスです。

class Employee {
/** All kinds of data: name, ID number, phone, email address... */
public:
 int monthlyPay() const;
};

一連の従業員を有意義に「蓄積」することはできません。それは意味がありません。それは未定義です。ただし、従業員に関する累積を定義できます。すべての従業員の月給をすべて合計したいとしましょう。それを行うことができます:std::accumulate

/** Simple class defining how to add a single Employee's
 *  monthly pay to our existing tally */
auto accumulate_func = [](int accumulator, const Employee& emp) {
   return accumulator + emp.monthlyPay();
 };

// And here's how you call the actual calculation:
int TotalMonthlyPayrollCost(const vector<Employee>& V)
{
 return std::accumulate(V.begin(), V.end(), 0, accumulate_func);
}

したがって、この例では、オブジェクトintのコレクションに対して値を累積していEmployeeます。ここで、累積合計、実際に合計する変数の型とは異なります。

例 #2: 平均の累積

より複雑なタイプの累積にも使用できますaccumulate-ベクトルに値を追加したい場合があります。入力全体で追跡している難解な統計があるかもしれません。など。あなたが蓄積するものは単なる数字である必要はありません。より複雑なものになる可能性があります。

たとえば、accumulateint のベクトルの平均を計算するために を使用する簡単な例を次に示します。

// This time our accumulator isn't an int -- it's a structure that lets us
// accumulate an average.
struct average_accumulate_t
{
    int sum;
    size_t n;
    double GetAverage() const { return ((double)sum)/n; }
};

// Here's HOW we add a value to the average:
auto func_accumulate_average = 
    [](average_accumulate_t accAverage, int value) {
        return average_accumulate_t(
            {accAverage.sum+value, // value is added to the total sum
            accAverage.n+1});      // increment number of values seen
    };

double CalculateAverage(const vector<int>& V)
{
    average_accumulate_t res =
        std::accumulate(V.begin(), V.end(), average_accumulate_t({0,0}), func_accumulate_average)
    return res.GetAverage();
}

例 #3: 移動平均を累積する

初期値が必要なもう 1 つの理由は、その値が、作成している計算のデフォルト/ニュートラル値であるとは限らないためです。

これまで見てきた平均的な例に基づいて考えてみましょう。しかし今、移動平均を保持できるクラスが必要です。つまり新しい値をフィードし続け、複数の呼び出しにわたってこれまでの平均を確認できます。

class RunningAverage
{
    average_accumulate_t _avg;
public:
    RunningAverage():_avg({0,0}){} // initialize to empty average

    double AverageSoFar() const { return _avg.GetAverage(); }

    void AddValues(const vector<int>& v)
    {
        _avg = std::accumulate(v.begin(), v.end(), 
            _avg, // NOT the default initial {0,0}!
            func_accumulate_average);
    }

};

int main()
{
    RunningAverage r;
    r.AddValues(vector<int>({1,1,1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 1.0
    r.AddValues(vector<int>({-1,-1,-1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 0.0
}

std::accumulateこれは、その初期値を設定できることに絶対に依存しているケースです。さまざまな開始点から累積を初期化できる必要があります。


要約すると、入力範囲を反復処理し、その範囲全体で 1 つの結果を構築するstd::accumulate場合に適しています。しかし、結果は範囲と同じ型である必要はなく、どの初期値を使用するかについて仮定を立てることはできません。これが、累積結果として使用する初期インスタンスが必要な理由です。

于 2015-02-18T19:38:39.773 に答える
11

現状では、範囲が空でないことを確実に知っていて、範囲の最初の要素から累積を開始したいコードにとっては面倒です。累積に使用される操作によっては、使用する「ゼロ」値が何であるかが常に明らかであるとは限りません。

一方、空でない範囲を必要とするバージョンのみを提供すると、範囲が空でないことを確実に知らない発信者にとって迷惑になります。彼らには追加の負担がかかります。

1 つの観点は、もちろん、両方の機能を提供することが両方の長所であるということです。例として、Haskell は(空でないリストを必要とする)とfoldl1(ミラー) の両方を提供します。foldr1foldlfoldrstd::transform

もう1つの観点は、1つは簡単な変換でもう1つの観点から実装できるため(あなたが示したように:std::transform(std::next(b), e, *b, f)-- std::nextC ++ 11ですが、ポイントは依然として有効です)、インターフェイスを最小限にすることが望ましいということです表現力を実際に失うことはありません。

于 2012-09-28T05:24:19.050 に答える
3

標準ライブラリのアルゴリズムは、任意の範囲の (互換性のある) イテレータに対して機能するはずだからです。したがって、 の最初の引数accumulateは である必要はありません。とbegin()の間の任意のイテレータにすることができます。逆イテレータを使用している可能性もあります。begin()end()

全体的なアイデアは、アルゴリズムをデータから切り離すことです。あなたの提案は、私が正しく理解していれば、データに特定の構造が必要です。

于 2012-09-28T05:16:53.830 に答える
3

あなたが望むなら、あなたはaccumulate(V.begin()+1, V.end(), V.begin())それを書くことができます。しかし、v.begin() が v.end() かもしれない (つまり、v が空である) と思ったらどうなるでしょうか? が実装されていない場合はどうなりますかv.begin() + 1(v は一般化された加算ではなく ++ のみを実装するため)? アキュムレータの型が要素の型でない場合はどうなるでしょうか? 例えば。

std::accumulate(v.begin(), v.end(), 0, [](long count, char c){
   return isalpha(c) ? count + 1 : count
});
于 2012-09-28T05:24:37.607 に答える
0

それは確かに必要ありません。私たちのコードベースには、T{}値を使用する 2 つまたは 3 つの引数のオーバーロードがあります。

しかし、std::accumulateかなり古いです。元のSTLから来ています。std::enable_if私たちのコードベースには、「2 つの反復子と初期値」と「2 つの反復子とリダクション演算子」を区別する凝ったロジックがあります。これには C++11 が必要です。このコードでは、末尾の戻り値の型 ( auto accumulate(...) -> ...) を使用して戻り値の型を計算しています。これは、C++11 のもう 1 つの機能です。

于 2018-04-13T11:46:35.437 に答える