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

C++で解く「交差しない接続線」問題:動的計画法で最大本数を求める方法

問題概要

2つの整数配列 A と B が与えられ、それぞれ別々の水平な直線上に、与えられた順序どおりに並べて書かれているとします。ここで、次の条件を満たす「接続線」を引くことを考えます。

  • A[i] == B[j] を満たす2つの数 A[i] と B[j] を結ぶこと

  • 引いた線が、他のどの接続線(水平線は除く)とも一切交差しないこと

重要なのは、接続線どうしは端点同士でも接触してはいけないという点です。つまり、1つの数値が属せる接続線は高々1本だけです。この条件下で、引くことのできる接続線の最大本数を求めます。

たとえば、入力が [1,4,2] と [1,2,4] の場合、答えは 2 になります。

142
124

上の表のように、2本の交差しない線を引くことは可能です。しかし3本は引けません。A[1]=4 から B[2]=4 への線と、A[2]=2 から B[1]=2 への線が交差してしまうためです。

解法の考え方

実はこの問題は、「最長共通部分列(LCS)」とまったく同じ構造をしています。A と B の共通する要素を、順序を保ったまま選ぶことができれば、その選び方がそのまま交差しない接続線に対応するからです。そこで、再帰呼び出しとメモ化(動的計画法)を組み合わせて解きます。手順は以下のとおりです。

  • solve() というメソッドを定義します。引数は添字 i、j、配列 A、配列 B、およびメモ化用の行列 dp です。

  • i が配列 A の範囲外であれば 0 を返します。

  • j が配列 B の範囲外であれば 0 を返します。

  • nj := j と初期化します。

  • nj が B のサイズ未満であり、かつ B[nj] != A[i] である間、nj を1ずつ増やします(A[i] と一致する位置を探します)。

  • nj が B のサイズ未満なら temp := 1、そうでなければ temp := 0 とします。

  • ret := max(solve(i+1, j, A, B, dp), temp + solve(i+1, nj+1, A, B, dp)) とします。「A[i] を使わない場合」と「A[i] を B[nj] と結ぶ場合」のうち良い方を選んでいます。

  • dp[i][j] := ret として結果を保存し、ret を返します。

  • メインのメソッドからは次を行います。

    • n := A のサイズ、m := B のサイズ とする

    • n × m の行列 dp を作成し、すべて −1 で初期化する(未計算の印)

    • solve(0, 0, A, B, dp) を呼び出して結果を得る

C++での実装例

以下に実際のコードを示します。

#include <bits/stdc++.h>
using namespace std;
class Solution {
    public:
    int solve(int i, int j, vector<int>& A, vector<int>& B, vector<vector<int>>& dp){
        if(i >= A.size()) return 0;
        if(j >= B.size()) return 0;
        if(dp[i][j] != -1) return dp[i][j];
        int nj = j;
        while(nj < B.size() && B[nj] != A[i]) nj++;
        int ret = max(solve(i + 1, j, A, B, dp), (nj < B.size() ? 1 : 0) + solve(i + 1, nj + 1, A, B, dp));
        return dp[i][j] = ret;
    }
    int maxUncrossedLines(vector<int>& A, vector<int>& B) {
        int n = A.size();
        int m = B.size();
        vector<vector<int>> dp(n, vector<int>(m, -1));
        return solve(0, 0, A, B, dp);
    }
};
main(){
    vector<int> v1 = {1,4,2};
    vector<int> v2 = {1,2,4};
    Solution ob;
    cout << (ob.maxUncrossedLines(v1, v2));
}

入力

[1,4,2]
[1,2,4]

出力

2

計算量と補足

状態 (i, j) の組は高々 n × m 通りしか存在せず、各状態の結果は dp テーブルにキャッシュされるため、このアルゴリズムの時間計算量は O(n × m)、空間計算量も O(n × m) となります。なお、この問題は前述のとおり最長共通部分列(LCS)の長さを求める問題と等価なので、ボトムアップ型のDPで書き換えることもできます。その場合、dp[i][j] を「A の先頭 i 要素と B の先頭 j 要素を使ったときの答え」と定義し、A[i-1] == B[j-1] なら dp[i-1][j-1] + 1、そうでなければ max(dp[i-1][j], dp[i][j-1]) と漸化式を立てれば、再帰なしで同様の結果が得られます。

  1. C++で配列内のクロスライン(交差線)を数える方法

    問題の概要 ソートされていない、重複のない要素からなる整数配列が与えられます。この課題のゴールは、配列をソートしたときに発生するクロスライン(交差線)の総数を求めることです。 クロスラインとは、配列の各要素を縦線として表現したとき、ソートの過程で線と線が交差する回数のことです。言い換えると、これは配列内の転倒(インバージョン)、すなわち「手前の要素が後ろの要素より大きい」というペアの個数を数える問題と同じものです。 Arr[] = { 1,2,4,3,5 } の場合:下図のように3本のクロスラインが存在します。 Arr[] = { 1,2,3,4,5 } の場合:すでにソート済みのため、クロ

  2. C++で2本の直線の交点を求めるプログラムの書き方

    直線ABを定義する2点A・Bと、直線CDを定義する2点C・Dが与えられたとき、この2つの直線の交点を求めるのが課題です。 注意 − すべての点は、X座標とY座標を持つ2次元平面上にあるものとします。 図では、A(a1, a2)とB(b1, b2)を通る直線、C(c1, c2)とD(d1, d2)を通る直線という、互いに異なる2つの直線が描かれており、P(p1, p2)がその交点を表しています。 交点の求め方 まず、2点を通る直線を「ax + by = c」の形の方程式で表します。各点の座標を使って、次のように係数を計算します。 A1 = b2 - a2 B1 = a1 - b1 C1 =