Atcorder DPまとめコンテストhttps://atcoder.jp/contests/dp/tasks/dp_f

問題文

文字列 s およびt が与えられます。s の部分列かつ t の部分列であるような文字列のうち、最長のものをひとつ求めてください。

注釈

文字列x の部分列とは、x から 0 個以上の文字を取り除いた後、残りの文字を元の順序で連結して得られる文字列のことです。

制約

  • s およびt は英小文字からなる文字列である。

入力

入力は以下の形式で標準入力から与えられる。

s
t

出力

s の部分列かつt の部分列であるような文字列のうち、最長のものをひとつ出力せよ。 答えが複数ある場合、どれを出力してもよい。 


入力例 1

 
axyb
abyxb

出力例 1

 
axb

答えは axb または ayb です。 どちらを出力しても正解となります。

入力例 4

 
abracadabra
avadakedavra

出力例 4

 
aaadara

(説明) 最長共通部分列問題というものがあります。今回の問題では、2つの文字列のそれぞれについて、共通する最大の長さの部分文字列を求める問題です。LCS(最長共通部分列)の動的計画法を理解するには、実際に i と j をインデックスとした二次元の表(DPテーブル)を書いてみるのが一番の近道です
 例として、s = "abcbdab", t = "bdcaba" という短い文字列で表を作成してみます。

  (空) a b c b d a b
(空) 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1
d 0 0 1 1 1 2 2 2
c 0 0 1 2 2 2 2 2
a 0 1 1 2 2 2 3 3
b 0 1 2 2 3 3 3 4
a 0 1 2 2 3 3 4 4

① 横軸を​文字列s、​縦軸を​文字列tで​対応表を​作成します。
一致として取り出した文字数を対応表に記載していきます。
  (空) a b c b d a b
(空) 0              
b 0              
d 0              
c 0              
a 0              
b 0              
a 0              
② 次に​文字列sの​”a”を​取り出す際に​文字列tから​最大何個取り出せるか​記載します。
  (空) a b c b d a b
(空) 0 0            
b 0 0            
d 0 0            
c 0 0            
a 0 1            
b 0 1            
a 0 1            
③ 文字列sの​"ab"を​取り出す際に​文字列tから​最大何個取り出せるか​記載します。
  (空) a b c b d a b
(空) 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1
d 0 0 1 1 1 2 2 2
c 0 0 1 2 2 2 2 2
a 0 1 1 2 2 2 3 3
b 0 1 2 2 3 3 3 4
a 0 1 2 2 3 3 4 4
④ 文字列1の​"abc"を​取り出す際に​文字列tから​最大何個取り出せるか​記載します。
  (空) a b c b d a b
(空) 0 0 0 0        
b 0 0 1 1        
d 0 0 1 1        
c 0 0 1 2        
a 0 1 1 2        
b 0 1 2 2        
a 0 1 2 2        
⑤ 一文​字ずつ​取り出す文字を​増やしていき、​文字列2から​最大何個取り出せるか​記載します。
  (空) a b c b d a b
(空) 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1
d 0 0 1 1 1 2 2 2
c 0 0 1 2 2 2 2 2
a 0 1 1 2 2 2 3 3
b 0 1 2 2 3 3 3 4
a 0 1 2 2 3 3 4 4
⑥ 表の​右下から​左上に​向けて​数字が​高い​ものを​通るように​辿っていきます。
setumei

ただし、このたどり方にはルールがあります。下の図のように隣接する数字がすべて同じ場合は、斜め上に移動します。

b

次に、縦方向や横方向に移動する場合は数字が高いものを通るように移動します。なお、縦方向は文字列sに存在しない文字、横方向は文字列tに存在しない文字の場合。

この問題では、漸化式を使用して考えます。

1. 漸化式の定義

2つの文字列を s(長さ n)、t(長さ m)とします。

dp[i][j] を「s の i 文字目までと t の j 文字目までにおけるLCSの長さ」と定義します。

ご指摘の通り、漸化式は以下のようになります。

  1. s[i] == t[j] のとき(文字が一致する場合)

    その文字をLCSに含めることができるため、1つ前の状態に +1 します。

    dp[i][j] = dp[i-1][j-1] + 1
  2. s[i] \neq t[j] のとき(文字が一致しない場合)

    s を1文字削った場合と、tを1文字削った場合の、値が大きい方を引き継ぎます。

    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

2. 文字列の復元方法

DPテーブルが完成すると、dp[n][m] にLCSの最大長が格納されます。ここから具体的な文字列を特定するには、テーブルをゴール(右下)からスタート(左上)へ逆順に辿ります。

  • s[i] == t[j] ならば:

    その文字はLCSに含まれる文字です。結果の先頭に追加し、斜め上 (i-1, j-1) へ移動します。

  • dp[i][j] == dp[i-1][j] ならば:

    s の i 文字目を使わなくても現在の長さが維持できているため、上 (i-1) へ移動します。

  • それ以外 (dp[i][j] == dp[i][j-1]) ならば:

    左 (j-1) へ移動します。

(参考)この漸化式を踏まえたPythonコードは次のようです。一つしか出力できませんが、コードは読みやすいと思います。

# LCSのひとつを表示する

def solve_lcs(s, t):
    n = len(s)
    m = len(t)
   
    # DPテーブルの初期化 (n+1) x (m+1)
    # dp[i][j] は sのi文字目までと tのj文字目までのLCSの長さ
    dp = [[0] * (m + 1) for _ in range(n + 1)]
   
    # 1. DPテーブルを埋める
    for i in rangeNo:
        for j in range(m):
            if s[i] == t[j]:
                dp[i+1][j+1] = dp[i][j] + 1
            else:
                dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
   
    # 2. 文字列の復元 (後ろから辿る)
    res = []
    i, j = n, m
    while i > 0 and j > 0:
        if s[i-1] == t[j-1]:
            # 文字が一致する場合、その文字を採用して斜め上へ
            res.append(s[i-1])
            i -= 1
            j -= 1
        elif dp[i][j] == dp[i-1][j]:
            # 上のセルと同じ値なら、上へ移動
            i -= 1
        else:
            # 左のセルと同じ値なら、左へ移動
            j -= 1
           
    # 逆順で格納されているので反転させる
    return "".join(reversed(res))

# テスト実行
s = "abcbdab"
t = "bdcaba"
result = solve_lcs(s, t)

print(f"文字列1: {s}")
print(f"文字列2: {t}")
print(f"LCS: {result}")
print(f"長さ: {len(result)}")
Last modified: Friday, 24 April 2026, 1:28 PM