公式

E - 略称割り当て / Abbreviation Assignment 解説 by admin

Gemini 3.0 Flash (Thinking)

概要

\(N\) 個の文字列 \(S_i\) が与えられたとき、各 \(S_i\) の接頭辞(prefix)から略称 \(T_i\) を選び、どの \(T_i\) も他の \(T_j\) の接頭辞にならない(接頭辞フリー)ようにしつつ、略称の長さの合計 \(\sum |T_i|\) を最小化する問題です。

考察

接頭辞フリー条件の深掘り

接頭辞フリー条件を整理すると、以下のことがわかります。

  1. 包含関係の禁止: ある \(S_i\) が別の \(S_j\) の接頭辞である場合、条件を満たす割り当ては不可能です。
    • なぜなら、\(S_i\) のどのような接頭辞 \(T_i\) を選んでも、それは \(S_j\) の接頭辞でもあります。\(T_j\) も \(S_j\) の接頭辞であるため、\(T_i\) と \(T_j\) の一方が他方の接頭辞になってしまいます。
  2. 最短の略称: \(S_i\) が他のどの \(S_j\) の接頭辞でもない場合、\(T_i\) として「\(S_i\) の接頭辞であり、かつ他のどの \(S_j\) の接頭辞でもない最短の文字列」を選ぶのが最適です。
    • このような \(T_i\) の長さは、「\(S_i\) と他の文字列 \(S_j\) との最長共通接頭辞(LCP)の最大長」に \(1\) を足したものになります。

効率的な比較方法

すべてのペア \((S_i, S_j)\) について LCP を計算すると \(O(N^2)\) かかり、間に合いません。 文字列を辞書順にソートすると、ある文字列 \(S_i\) と最も長い共通接頭辞を持つ可能性があるのは、ソート順でその直前または直後にある文字列だけになります。

したがって、ソート後に隣接する要素間でのみ LCP を計算すれば、各 \(S_i\) について「他のすべての文字列との LCP の最大値」を効率的に求めることができます。

アルゴリズム

  1. \(N\) 個の文字列 \(S_1, \ldots, S_N\) を辞書順にソートします。
  2. 隣接する文字列 \(S_i\) と \(S_{i+1}\) の最長共通接頭辞の長さ \(LCP(S_i, S_{i+1})\) をすべて計算します。
  3. 各 \(S_i\) について、必要な略称の最小の長さ \(L_i\) を以下のように求めます。
    • \(L_i = \max(LCP(S_{i-1}, S_i), LCP(S_i, S_{i+1})) + 1\)
    • (端の要素については、存在する隣接要素のみを考慮します)
  4. もし、ある \(i\) について \(L_i > |S_i|\) となった場合、その \(S_i\) は他の文字列の接頭辞になっていることを意味するため、-1 を出力して終了します。
  5. すべての \(L_i\) の合計を出力します。

計算量

  • 時間計算量: \(O(\sum |S_i| \log N)\)
    • 文字列のソートに \(O(\sum |S_i| \log N)\)、隣接要素の LCP 計算に \(O(\sum |S_i|)\) かかります。
  • 空間計算量: \(O(\sum |S_i|)\)
    • 文字列を保持するためのメモリが必要です。

実装のポイント

  • ソートの効率: Python の sort() は文字列に対しても高速に動作します。

  • LCP の計算: 2つの文字列の先頭から 1 文字ずつ比較し、不一致が出るか、どちらかの文字列が終わるまでカウントします。

  • 不可能判定: ソート済みリストにおいて、\(S_i\) が \(S_{i+1}\) の接頭辞であるかどうかは \(LCP(S_i, S_{i+1}) == |S_i|\) で判定できます。このとき必要な長さ \(|S_i|+1\) は元の文字列長を超えるため、正しく -1 を出力できます。

    ソースコード

import sys

# 競技プログラミングにおける「略称割り当て」問題を解くためのプログラムです。
# 異なる社員の略称が互いに接頭辞にならない(接頭辞フリー条件)という制約のもと、
# 各社員のユーザー名の接頭辞を略称として選び、その長さの合計を最小化します。
# 
# 解法のポイント:
# 1. 各社員 i の略称 Ti は Si の接頭辞である必要があります。
# 2. 接頭辞フリー条件を満たすためには、各 Ti は他のどの Sj (j != i) の接頭辞であってもいけません。
#    なぜなら、もし Ti が Sj の接頭辞である場合、Tj も Sj の接頭辞であるため、
#    Ti と Tj は同じ文字列 Sj の接頭辞となり、一方が他方の接頭辞になってしまうからです。
# 3. したがって、各 Ti は「Si の接頭辞であり、かつ他のどの Sj の接頭辞でもないもの」の中で
#    最短のものを選ぶのが最適です。
# 4. このような最短の接頭辞の長さは、(Si が他の Sj と共有する最長共通接頭辞の長さ) + 1 となります。
# 5. すべての文字列を辞書順にソートすると、ある文字列 Si と他の文字列との最長共通接頭辞(LCP)は、
#    ソート順で隣接する文字列との LCP のいずれかになります。
# 6. もし計算された必要な長さが元の文字列 Si の長さを超える場合、Si が他の文字列の接頭辞であることを
#    意味し、条件を満たす割り当ては不可能です。

def solve():
    # 標準入力からすべてのデータを読み込みます。大量の入力に対応するため read().split() を使用します。
    input_data = sys.stdin.read().split()
    if not input_data:
        return
    
    # 社員数 N
    N = int(input_data[0])
    # 全社員のユーザー名リスト S
    S = input_data[1:N+1]
    
    # ユーザー名を辞書順にソートします。
    # Python の sort() は Timsort を採用しており、文字列のソートも効率的です。
    S.sort()
    
    # 隣接する文字列間の最長共通接頭辞 (LCP) の長さを格納するリスト
    lcps = [0] * (N - 1)
    for i in range(N - 1):
        s1 = S[i]
        s2 = S[i+1]
        n1 = len(s1)
        n2 = len(s2)
        min_len = n1 if n1 < n2 else n2
        l = 0
        # 文字を順番に比較して共通接頭辞の長さを求めます。
        # 計算量の合計は全文字列の長さの和 O(Σ|Si|) に収まります。
        while l < min_len and s1[l] == s2[l]:
            l += 1
        lcps[i] = l
        
    total_min_length = 0
    for i in range(N):
        # 文字列 S[i] が他のいずれかの文字列と共有する最長の接頭辞の長さを求めます。
        # 辞書順で隣接する S[i-1] と S[i+1] との比較だけで十分です。
        lcp_max = 0
        if i > 0:
            lcp_max = lcps[i-1]
        if i < N - 1:
            if lcps[i] > lcp_max:
                lcp_max = lcps[i]
        
        # S[i] の略称として必要な最小の長さ
        required_len = lcp_max + 1
        
        # 必要な長さが元の文字列の長さを超える場合、S[i] は他の文字列の接頭辞になっています。
        # このとき、接頭辞フリーな略称を割り当てることは不可能です。
        if required_len > len(S[i]):
            print("-1")
            return
        
        total_min_length += required_len
        
    # 略称の長さの合計の最小値を出力します。
    print(total_min_length)

if __name__ == '__main__':
    solve()

この解説は gemini-3-flash-thinking によって生成されました。

投稿日時:
最終更新: