ホーム
/
作ったもの
/
考えたこと
/
きろく

001 - Three Color Reduction 解説

実行時間制限: 2sec / メモリ制限: 1024 MB

解説

 本問題は、文字を対称群 $S_3$ の互換(長さ2の巡回置換)に紐付けることで、状態数の非常に少ない動的計画法(DP)に帰着させることができます。

1. 群論への帰着($S_3$との対応)

 'A', 'B', 'C' の3種類の文字を、それぞれ3要素の置換(互換)に対応させます。

  • 'A' $\leftrightarrow (1\ 2)$
  • 'B' $\leftrightarrow (2\ 3)$
  • 'C' $\leftrightarrow (1\ 3)$

この対応付のもとでは、操作は以下のように考えられます:

  • 連続する同じ2文字を消す:
    'A' + 'A' $\to$ 空文字列。置換で考えると $(1\ 2)(1\ 2) = id$(単位元)となり、文字列の積が不変に保たれます。
  • 「XYX」を「Z」に置き換える:
    'A' + 'B' + 'A' $\to$ 'C'。置換で考えると $(1\ 2)(2\ 3)(1\ 2) = (1\ 3)$ となり、これも積が一致します。
  • 「XYZ」を「Y」に置き換える:
    'A' + 'B' + 'C' $\to$ 'B'。置換で考えると $(1\ 2)(2\ 3)(1\ 3) = (2\ 3)$ となり、やはり積が一致します。

この考察により、各文字を $S_3$ の元とみなして、全体の積が $id$ になるような置き換え方のうち、コストが最小のものを求めればよいということがわかります。

S_3 との同一視に関する厳密な証明

【命題の定式化】
 文字 '$\text{A}$', '$\text{B}$', '$\text{C}$' をそれぞれ対称群 $S_3$ の互換 $a=(1\ 2), b=(2\ 3), c=(1\ 3)$ と対応させます。 文字列 $T$ に対し、その各文字に対応する $S_3$ の元を順番に掛け合わせた積を $\pi(T)$ と定義します(空文字列の場合は単位元 $e$ とします)。 このとき、「文字列 $T$ が問題の操作を繰り返して空文字列にできる」ための必要十分条件は、「$\pi(T) = e$」であること、という命題が成り立ちます。

【必要性の証明】(空文字列にできる $\implies \pi(T) = e$)
 問題で許可されている3つの操作が、文字列全体が表す $S_3$ の元の積 $\pi(T)$ を不変に保つことを示します。

  • 操作1($\text{X}\text{X} \to$ 空):
    各互換は2乗すると単位元になるため、$x^2 = e$ が成り立ちます。したがって、隣り合う同じ文字を消去しても全体の積は変わりません。
  • 操作2($\text{X}\text{Y}\text{X} \to \text{Z}$):
    異なる2つの互換の積に、さらに左の互換を掛けると残りの互換になります。例えば $(1\ 2)(2\ 3)(1\ 2) = (1\ 3)$ すなわち $a b a = c$ です。したがって置換の前後で積は不変です。
  • 操作3($\text{X}\text{Y}\text{Z} \to \text{Y}$):
    異なる3つの互換の積は、中央の互換と等しくなります。例えば $(1\ 2)(2\ 3)(1\ 3) = (2\ 3)$ すなわち $a b c = b$ です。これも積の値を不変に保ちます。

 初期文字列にどのような操作を行っても $\pi(T)$ の値は変わらず、最終的に空文字列($\pi(\text{空}) = e$)に到達できるのであれば、初期状態の文字列 $T$ についても $\pi(T) = e$ でなければなりません。

【十分性の証明】($\pi(T) = e \implies$ 空文字列にできる)
 $\pi(T) = e$ である文字列 $T$ は、適切な操作を行うことで必ず長さを真に短くできることを、文字列の長さ $|T|$ に関する数学的帰納法で示します。

 $|T| = 0$ のときは既に空文字列であるため自明に成り立ちます。任意の操作で文字列の長さは2だけ減るので、偶奇性を考えれば、$\pi(T) = e$ のとき $|T|$ は必ず偶数になります。 $|T| \ge 2$ とし、文字列が空でないと仮定します。

  • 隣り合う2文字が同じ場合:
    操作1を適用することで、文字列の長さを $2$ 減らすことができます。
  • 隣り合う2文字がすべて異なる場合:
    $|T| = 2$ の場合、$\text{X} \neq \text{Y}$ ならば $\pi(T) = x y \neq e$ となり、$\pi(T) = e$ という仮定に矛盾します。よって $|T| \ge 4$(偶数長)です。 隣り合う文字がすべて異なる長さ $3$ 以上の部分文字列は、必ず「$\text{X}\text{Y}\text{X}$」または「$\text{X}\text{Y}\text{Z}$」のいずれかのパターンを含みます。 この部分にそれぞれ操作2または操作3を適用することで、積の値を $e$ に保ったまま文字列の長さを $2$ 減らすことができます。

 したがって、$\pi(T) = e$ であり $|T| \ge 2$ である限り、必ずいずれかの操作を適用して長さを $2$ 減らした文字列 $T'$($\pi(T') = e$)を得ることができます。これを繰り返すことで、有限回の操作で必ず空文字列に到達できることが示されました。

2. 動的計画法 (DP) によるコスト最小化

 左から順番に文字を決定していくため、以下のDPを考えます。

dp[i][g] : 先頭から $i$ 文字目までを決定したとき、そこまでの $S_3$ における積が $g$ になるための最小コスト

 対称群 $S_3$ の要素数は $3! = 6$ 個しかないため、状態数は $N \times 6$ であり、 遷移は次に選ぶ文字('A', 'B', 'C')に対応する置換を右から掛けるだけです。'?' の場合は、それぞれの文字に対応するコストを加算したうえで、3通りすべての遷移を行い min を取ります。

 最終的な答えは dp[N][id] となります(到達不可能な場合や $N$ が奇数の場合は -1 となります)。以上より、この問題を $O(N)$ の計算量で解くことができました。

コード例

 以下はC++による想定解法のコードです。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

const long long INF = 1e18;

int main() {
    int n;
    if (!(cin >> n)) return 0;
    string s;
    cin >> s;

    // '?' の個数をカウント
    int k = 0;
    for (char c : s) {
        if (c == '?') k++;
    }

    // コストの読み込み
    vector<vector<long long>> cost(k, vector<long long>(3));
    for (int i = 0; i < k; i++) {
        cin >> cost[i][0] >> cost[i][1] >> cost[i][2];
    }

    // 対称群 S3 の全要素を配列の置換として定義
    // 0: id, 1: A(1 2), 2: B(2 3), 3: C(1 3), 4: (1 2 3), 5: (1 3 2)
    vector<vector<int>> S3 = {
        {0, 1, 2}, 
        {1, 0, 2}, 
        {0, 2, 1},
        {2, 1, 0}, 
        {1, 2, 0}, 
        {2, 0, 1}
    };

    // 置換 p が S3 の何番目の要素かを取得するラムダ式
    auto get_idx = [&](const vector<int>& p) {
        for (int i = 0; i < 6; i++) {
            if (S3[i] == p) return i;
        }
        return -1;
    };

    // 積の乗積表を事前計算
    vector<vector<int>> mult(6, vector<int>(6));
    for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 6; j++) {
            vector<int> p(3);
            for (int m = 0; m < 3; m++) p[m] = S3[i][S3[j][m]];
            mult[i][j] = get_idx(p);
        }
    }

    // dp[要素のインデックス] = 最小コスト
    vector<long long> dp(6, INF);
    dp[0] = 0; // 初期状態は単位元 id、コスト0

    int q_idx = 0;
    for (char c : s) {
        vector<long long> next_dp(6, INF);
        for (int i = 0; i < 6; i++) {
            if (dp[i] == INF) continue;

            // 'A' または '?' の遷移
            if (c == 'A' || c == '?') {
                int nxt = mult[i][1];
                long long add_cost = (c == '?') ? cost[q_idx][0] : 0;
                next_dp[nxt] = min(next_dp[nxt], dp[i] + add_cost);
            }
            // 'B' または '?' の遷移
            if (c == 'B' || c == '?') {
                int nxt = mult[i][2];
                long long add_cost = (c == '?') ? cost[q_idx][1] : 0;
                next_dp[nxt] = min(next_dp[nxt], dp[i] + add_cost);
            }
            // 'C' または '?' の遷移
            if (c == 'C' || c == '?') {
                int nxt = mult[i][3];
                long long add_cost = (c == '?') ? cost[q_idx][2] : 0;
                next_dp[nxt] = min(next_dp[nxt], dp[i] + add_cost);
            }
        }
        // '?' を処理したらインデックスを進める
        if (c == '?') q_idx++;
        
        dp = next_dp;
    }

    // 長さが奇数、または到達不可能な場合は -1
    if (n % 2 != 0 || dp[0] == INF) {
        cout << -1 << endl;
    } else {
        cout << dp[0] << endl;
    }

    return 0;
}