web-dev-qa-db-ja.com

std :: accumulateを理解する

_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());
_

これはすべてのopで機能します。なぜ3番目のパラメーターが必要なのですか?

33

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

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

1つの観点は、両方の長所はもちろん両方の機能を提供することです。例として、Haskellはfoldlおよびfoldr(_foldl1_をミラー化する)と共に_foldr1_と_std::transform_(空でないリストが必要)の両方を提供します。

もう1つの観点は、一方を簡単な変換で他方の観点から実装できるためです(お見せしたように、std::transform(std::next(b), e, *b, f)-_std::next_はC++ 11ですが、ポイントはまだ立っています)、表現力を実際に失うことなくできる限り最小限のインターフェイスにすることが望ましい。

8
Luc Danton

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

ただし、std::accumulateは汎用的であり、あらゆる種類の創造的な蓄積と削減が可能です。

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

簡単な例:Employeeクラスで、多くのデータフィールドがあります。

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

従業員のセットを意味のある「累積」することはできません。それは意味がありません。未定義です。ただし、累積を定義することはできますに関して従業員。 allall従業員の月給を合計したいとしましょう。 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を使用して、より複雑なタイプの累積にも使用できます。ベクトルに値を追加することもできます。入力全体で追跡している不可解な統計情報があるかもしれません。など。蓄積するものはhaveだけではありません。もっと複雑なものにすることもできます。

たとえば、以下はaccumulateを使用してintのベクトルの平均を計算する簡単な例です。

// 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つの理由は、その値がalways計算のデフォルト/中立値ではないためです。

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

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の初期値を設定できることに完全に依存している場合です。つまり、異なる開始点から累積を初期化できるようにするためにneedです。


要約すると、std::accumulateは、入力範囲で繰り返し処理を行う場合に適しています。また、構築その範囲全体で1つの結果が得られます。ただし、結果は範囲と同じ型である必要はなく、使用する初期値について推測することはできません。これが、累積結果として使用する初期インスタンスが必要な理由です。

28
Ziv

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
});
3
rici

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

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

2
juanchopanza

本当に必要ありません。コードベースには、_T{}_値を使用する2および3引数のオーバーロードがあります。

ただし、_std::accumulate_はかなり古いです。それは元のSTLから来ています。コードベースには、「2つのイテレータと初期値」と「2つのイテレータとリダクション演算子」を区別するための空想的な_std::enable_if_ロジックがあります。それにはC++ 11が必要です。また、このコードでは、後続の戻り型(auto accumulate(...) -> ...)を使用して、別のC++ 11機能である戻り型を計算します。

0
MSalters