C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で解く「因数から構成できる二分木」の総数を求めるアルゴリズム

問題概要

1より大きい正整数のリストが与えられます。これらの整数を使って二分木を構築することを考えます。ただし、同じ数は何度でも使用でき、葉以外のすべてのノードは、その2つの子ノードの値の積になっていなければなりません。この条件を満たす二分木が全部で何通り作れるかを求め、答えは 109 + 7 で割った余りとして返します。

たとえば入力が [2, 4, 5, 10] の場合、答えは 7 になります。具体的には次の7通りの木が作れます。

  • [2]
  • [4]
  • [5]
  • [10]
  • [4, 2, 2](根が4、子が2と2)
  • [10, 2, 5](根が10、子が2と5)
  • [10, 5, 2](根が10、子が5と2)

解法のアプローチ:動的計画法(DP)

この問題は動的計画法で効率よく解けます。あらかじめ配列を昇順にソートしておけば、ある値を処理する時点で、その約数となる小さい値のDP値はすでに確定しています。そのため、積の組み合わせを素早く数え上げることができます。

手順は以下の通りです。

  • キーを配列の値、値を「その値を根とする二分木の本数」とするマップ dp を用意する
  • 配列 A をソートし、n := A のサイズ、ret := 0 と初期化する
  • i を 0 から n − 1 まで繰り返す
    • dp[A[i]] を 1 増やす(単独の葉として使えるため)
    • j を 0 から i − 1 まで繰り返す
      • A[i] % A[j] == 0 のとき(A[j] が A[i] の約数のとき)
        • dp[A[i]] := dp[A[i]] + dp[A[j]] × dp[A[i] / A[j]]
    • ret := ret + dp[A[i]]
  • ret を返す

ここで dp[A[j]] × dp[A[i] / A[j]] は、「左の子が A[j]、右の子が A[i] / A[j] となる組み合わせ」の総数を表しています。2つの因子が異なる値なら左右の入れ替えが別カウントされ(例:10 = 2 × 5 と 10 = 5 × 2)、同じ値なら自然に1通りだけ数えられる(例:4 = 2 × 2)ため、重複や漏れなく木の総数を求められます。

計算量

ソートに O(n log n)、約数のペア確認のための二重ループに O(n2) かかるため、全体の時間計算量は O(n2) です。空間計算量は、マップに各値ごとのDP値を保持するため O(n) となります。

C++による実装例

#include <bits/stdc++.h>
using namespace std;
typedef long long int lli;
const int MOD = 1e9 + 7;
int add(lli a, lli b){
    return ((a % MOD) + (b % MOD)) % MOD;
}
int mul(lli a, lli b){
    return ((a % MOD) * (b % MOD)) % MOD;
}
class Solution {
    public:
    int numFactoredBinaryTrees(vector<int>& A) {
        unordered_map <int, int> dp;
        sort(A.begin(), A.end());
        int n = A.size();
        int ret = 0;
        for(int i = 0; i < n; i++){
            dp[A[i]] += 1;
            for(int j = 0; j < i; j++){
                if(A[i] % A[j] == 0){
                    dp[A[i]] = add(dp[A[i]], mul(dp[A[j]], dp[A[i] / A[j]]));
                }
            }
            ret = add(ret, dp[A[i]]);
        }
        return ret;
    }
};
main(){
    vector<int> v1 = {2,4,5,10};
    Solution ob;
    cout << (ob.numFactoredBinaryTrees(v1));
}

add 関数と mul 関数は、大きな数同士の演算でオーバーフローが起きないよう、毎回 MOD(109 + 7)で剰余を取るヘルパー関数です。dp マップには「その値を根とする二分木の本数」が格納されており、約数の関係にあるペアを見つけるたびに、子の組み合わせの数を掛け合わせて加算していきます。

入力

[2,4,5,10]

出力

7
  1. C++で解く「ユニークな二分探索木 II」― 再帰で全パターンのBSTを生成する方法

    整数 n が与えられたとき、1 から n までの値を格納する、構造的にユニークな二分探索木(BST)をすべて生成することを考えます。例えば、入力が 3 の場合、生成される木は以下のようになります。解法のアプローチこの問題は再帰(バックトラッキング)を用いることで効率的に解けます。二分探索木の性質上、ある値 i を根にしたとき、左部分木には i より小さい値が、右部分木には i より大きい値が属します。この性質を利用して、各値を根とした場合の左右の部分木を再帰的に生成していきます。アルゴリズムの手順low と high を引数に取る再帰関数 generate() を定義します。結果を格納するため

  2. C++で2つの二分木をマージする方法

    2つの二分木があるとします。一方の木をもう一方の木に重ねてみると、一部のノードは互いに重なり合い、残りのノードは重ならない状態になります。ここで、この2つの木を1つの新しい二分木へマージすることを考えます。マージのルールは次のとおりです。2つのノードが重なっている場合は、それらの値を合計したものをマージ後のノードの新しい値とします。どちらか一方しかノードが存在しない場合は、空でない方のノードをそのまま新しい木のノードとして使用します。たとえば、次のような2つの木が与えられたとします。このときの出力結果は以下のようになります。解法のアプローチこの問題を解くために、以下の手順に従います。メソッド名