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

C++でUnion-Findを使って遺伝子の突然変異グループ総数を求める方法

問題概要

同じ長さの文字列リスト genes があるとします。各要素は ACGT のいずれかの文字のみで構成されており、次のルールが成り立ちます。

  • 2つの文字列 s1 と s2 がちょうど1文字だけ異なる場合、s1 と s2 は同じ突然変異グループに属します。
  • s1 と s2 が同じグループに属し、s2 と s3 が同じグループに属するならば、s1 と s3 も同じグループに属します(推移性)。

このとき、生成できる突然変異グループの総数を求めるのが目的です。

具体例

たとえば、入力が genes = ["ACGT", "ACGC", "ACTT", "TTTT", "TGTT"] の場合、出力は 2 になります。これは、次の2つの突然変異グループが存在するためです。

  • ["ACGT", "ACGC", "ACTT"]
  • ["TTTT", "TGTT"]

解法のアプローチ:Union-Find(素集合データ構造)

この問題は、Union-Find(Disjoint Set Union)を用いることで効率的に解けます。ポイントは、すべてのペアを比較する代わりに、「各文字列から1文字だけ変えた候補」を列挙し、それがリスト内に存在するかを高速に判定する点です。

  1. parent マップの定義: 各文字列の親ノードを記録するマップ parent を用意します。
  2. getPar() 関数: 経路圧縮を行いながら、ある要素の根(グループの代表元)を再帰的に求めます。
  3. unite() 関数: 2つの要素が属する集合を統合します。統合が行われた場合は true、すでに同じ集合だった場合は false を返します。
  4. ok() 関数: 2つの文字列を比較し、異なる文字がちょうど1個であるかどうかを判定します。
  5. メイン処理(solve):
    • まず配列 v をソートし、set を作成して O(log N) での存在判定を可能にします。
    • ret の初期値は v のサイズ(初期状態ではすべての文字列が独立したグループ)です。
    • 各文字列の各位置 j に対して、その文字を A / C / G / T の残り3文字に置き換えた文字列 temp を生成し、temp が set 内に存在すれば unite() で統合します。統合に成功するたびに ret を1減らします。

最終的な ret が、求める突然変異グループの総数になります。

C++での実装例

#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
    map<string, string> parent;

    string getPar(string& a){
        if(parent[a] == a)
            return a;
        return parent[a] = getPar(parent[a]);
    }

    bool unite(string& a, string& b){
        string parA = getPar(a);
        string parB = getPar(b);
        if(parA != parB){
            parent[parA] = parB;
            return true;
        }
        return false;
    }

    bool ok(string &a, string& b){
        int cnt = 0;
        for(int i = 0; i < a.size(); i++){
            cnt += a[i] != b[i];
        }
        return cnt == 1;
    }

    int solve(vector<string> v) {
        sort(v.begin(), v.end());
        set<string> s(v.begin(), v.end());

        int ret = v.size();
        for(auto& it : v){
            parent[it] = it;
        }
        for(auto& it : v){
            for(int j = 0; j < it.size(); j++){
                string temp = it;
                for(char x : {'A', 'C', 'G', 'T'}){
                    if(x != it[j]){
                        temp[j] = x;
                        if(s.count(temp)){
                            if(unite(temp, it)) ret--;
                        }
                    }
                }
            }
        }
        return ret;
    }
};

int main(){
    vector<string> v = {"ACGT", "ACGC", "ACTT", "TTTT", "TGTT"};
    Solution ob;
    cout << ob.solve(v);
}

入力

{"ACGT", "ACGC", "ACTT", "TTTT", "TGTT"}

出力

2

計算量の目安

N を文字列の個数、L を文字列の長さとすると、各文字列につき L × 3 個の候補を生成し、それぞれについて set への存在判定(O(log N))と Union-Find 操作(ほぼ定数時間)を行います。したがって全体の時間計算量は O(N × L × log N)、空間計算量は O(N × L) となります。全ペアを直接比較する O(N² × L) の手法に比べ、N が大きい場合に大きく高速化できる点がこの手法の利点です。

  1. C++で三角形の重心を求めるプログラムの作成方法

    この記事では、三角形の3つの頂点の座標を格納した2次元配列が与えられたときに、その三角形の重心を求めるC++プログラムの作成方法を解説します。 三角形の重心とは、三角形の3本の中線がすべて交わる点のことです。 また、三角形の中線とは、ある頂点と、その対辺(向かい合う辺)の中点を結ぶ線分のことを指します。 それでは、具体的な例を使って問題を確認してみましょう。 入力 (-3, 1), (1.5, 0), (-3, -4) 出力 (-1.5, -1) 説明 重心 (x, y) = ((-3 + 1.5 - 3) / 3, (1 + 0 - 4) / 3) = (-1.5, -1) 解法のアプロ

  2. C++で平行四辺形の面積を求めるプログラムの作成方法

    この記事では、平行四辺形の底辺と高さを表す2つの値が与えられたとき、C++を使ってその面積を求めるプログラムを作成する方法を解説します。 平行四辺形とは? 平行四辺形とは、4つの辺からなる閉じた図形であり、向かい合う2組の辺がそれぞれ長さが等しく、互いに平行になっている四角形のことです。 問題を理解するための具体例 入力 B = 20, H = 15 出力 300 説明 平行四辺形の面積 = 底辺 × 高さ = 20 × 15 = 300 解決アプローチ この問題を解くには、平行四辺形の面積を求める幾何学の公式を使用します。 面積 = 底辺 × 高さ つまり、与えられた底辺と高さを掛け合わせ