{ }codelper
동적 계획법P0117

가장 긴 공통 부분 수열

영어 소문자로 된 문자열 A와 B가 주어진다. 어떤 문자열에서 글자 몇 개를 지우고(하나도 지우지 않아도 된다) 남은 글자를 원래 차례대로 이은 것을 그 문자열의 부분 수열이라 한다. 남는 글자는 이어져 있지 않아도 된다. 예를 들어 "ace"는 "abcde"의 부분 수열이지만 "aec"는 아니다. A의 부분 수열이면서 B의 부분 수열이기도 한 문자열 가운데 가장 긴 것의 길이를 출력하라. 공통인 글자가 하나도 없으면 0을 출력한다.

입력
첫 줄에 A, 둘째 줄에 B가 주어진다.
두 문자열은 영어 소문자(a~z)로만 되어 있고, 길이는 각각 1 이상 1,000 이하이다.
출력
가장 긴 공통 부분 수열의 길이 하나
예시 1 · stdin
abcbdab
bdcaba
stdout
4
예시 2 · stdin
abc
def
stdout
0
main.py
입력(stdin)

실행하면 이 줄들이 차례로 input()·표준 입력으로 들어가요(실행 중에 묻지 않아요).

$ 준비됐어요

코드를 입력해 풀려면 PC에서 열어 주세요. 모바일에서는 문제와 풀이를 볼 수 있어요.

다른 언어로는?언어별 풀이 보기 · 8개 언어 풀이

L[i][j] = A의 앞 i글자와 B의 앞 j글자의 답이라 두면, 두 글자가 같을 때 L[i-1][j-1] + 1, 다를 때 max(L[i-1][j], L[i][j-1])이에요(동적 계획법).
표는 1,001 × 1,001 = 약 100만 칸이라 C·C++·Java·C#·Python·JavaScript·TypeScript는 2차원으로 만들어도 되지만, 크레이콘 목록은 한 개에 10,000칸까지라서 바깥 목록 1,001개에 안쪽 목록 1,001칸씩으로 쪼개야 해요.
바로 윗줄만 쓰니 한 줄(B 길이 + 1칸)로 줄이는 게 깔끔해요.
이때 함정: 왼쪽 칸 row[j - 1]은 이미 이번 줄 값으로 바뀌어 있어서, 윗줄의 왼쪽 위 칸(대각선)은 덮어쓰기 전에 diag 변수에 따로 챙겨야 해요 — 챙기지 않으면 "aa"와 "aaaa"의 답이 4가 돼요.
또 부분 수열은 건너뛰어도 되니 이어진 공통 부분 문자열("abxcd"·"abycd"에서 2)과 달라요(답 4).
앞에서부터 같은 글자를 바로 짝짓는 욕심도 틀려요("zabcd"·"abcdz"에서 z를 먼저 잡으면 1, 답 4).
한 글자씩 비교: Python·JavaScript·TypeScript는 b[j - 1]이 길이 1인 문자열, Java는 b.charAt(j - 1)(문자열에 []를 못 써요), C#·C·C++은 b[j - 1]이 char, 크레이콘도 b[j - 1]이 한 글자 문자열이고 각각 (ch 에서 a)로 글자를 돌아요.
입력 끝의 \r이나 빈칸이 글자로 섞이지 않게 줄을 읽으면 strip()·trim()·공백제거()를 해요(C·C++의 scanf("%s")·cin >>는 저절로 걸러요).

Pythonmain.py · 16 줄
a = input().strip()b = input().strip()m = len(b) # row[j] = (a의 지금까지 읽은 앞부분)과 b[:j]의 가장 긴 공통 부분 수열 길이. 2차원 표 대신 한 줄만 둔다row = [0] * (m + 1)for ch in a:    diag = 0  # 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다    for j in range(1, m + 1):        up = row[j]        if ch == b[j - 1]:            row[j] = diag + 1        elif row[j - 1] > row[j]:            row[j] = row[j - 1]        diag = upprint(row[m])
JavaScriptmain.js · 14 줄
const [a, b] = require("fs").readFileSync(0, "utf8").split("\n").map((s) => s.trim());const m = b.length; // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다const row = new Array(m + 1).fill(0);for (const ch of a) {  let diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다  for (let j = 1; j <= m; j++) {    const up = row[j];    row[j] = ch === b[j - 1] ? diag + 1 : Math.max(row[j], row[j - 1]);    diag = up;  }}console.log(row[m]);
TypeScriptmain.ts · 16 줄
import { readFileSync } from "node:fs"; const [a, b]: string[] = readFileSync(0, "utf8").split("\n").map((s) => s.trim());const m: number = b.length; // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다const row: number[] = new Array<number>(m + 1).fill(0);for (const ch of a) {  let diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다  for (let j = 1; j <= m; j++) {    const up = row[j];    row[j] = ch === b[j - 1] ? diag + 1 : Math.max(row[j], row[j - 1]);    diag = up;  }}console.log(row[m]);
JavaMain.java · 25 줄
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        String a = in.readLine().trim();        String b = in.readLine().trim();        int m = b.length();         // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다        int[] row = new int[m + 1];        for (int i = 0; i < a.length(); i++) {            char ch = a.charAt(i);            int diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다            for (int j = 1; j <= m; j++) {                int up = row[j];                row[j] = ch == b.charAt(j - 1) ? diag + 1 : Math.max(row[j], row[j - 1]);                diag = up;            }        }        System.out.println(row[m]);    }}
C#Main.cs · 25 줄
using System; class Program{    static void Main()    {        string a = Console.ReadLine().Trim();        string b = Console.ReadLine().Trim();        int m = b.Length;         // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다        int[] row = new int[m + 1];        foreach (char ch in a)        {            int diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다            for (int j = 1; j <= m; j++)            {                int up = row[j];                row[j] = ch == b[j - 1] ? diag + 1 : Math.Max(row[j], row[j - 1]);                diag = up;            }        }        Console.WriteLine(row[m]);    }}
C++main.cpp · 22 줄
#include <algorithm>#include <iostream>#include <string>#include <vector> int main() {    std::string a, b;    std::cin >> a >> b;    int m = static_cast<int>(b.size());     // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다    std::vector<int> row(m + 1, 0);    for (char ch : a) {        int diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다        for (int j = 1; j <= m; j++) {            int up = row[j];            row[j] = ch == b[j - 1] ? diag + 1 : std::max(row[j], row[j - 1]);            diag = up;        }    }    std::cout << row[m] << "\n";}
Cmain.c · 22 줄
#include <stdio.h>#include <string.h> int main(void) {    static char a[1002], b[1002];    scanf("%1001s %1001s", a, b);    int n = (int)strlen(a), m = (int)strlen(b);     /* row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이. 한 줄만 둔다 */    static int row[1001];    for (int i = 0; i < n; i++) {        int diag = 0; /* 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다 */        for (int j = 1; j <= m; j++) {            int up = row[j];            if (a[i] == b[j - 1]) row[j] = diag + 1;            else if (row[j - 1] > row[j]) row[j] = row[j - 1];            diag = up;        }    }    printf("%d\n", row[m]);    return 0;}
Craykonmain.cray · 25 줄
a = 콘솔.줄읽기().공백제거();b = 콘솔.줄읽기().공백제거();m = b.길이; // row[j] = (a의 지금까지 읽은 앞부분)과 b의 앞 j글자의 가장 긴 공통 부분 수열 길이// 1,001 × 1,001 표 대신 한 줄(m + 1칸)만 둔다row = 새목록(m + 1, 0);각각 (ch 에서 a){    diag = 0; // 윗줄의 row[j - 1] — 덮어쓰기 전에 따로 챙긴다    반복 (j = 1; j <= m; j++)    {        up = row[j];        만약 (ch == b[j - 1])        {            row[j] = diag + 1;        }        아니면        {            row[j] = 수학.큰수(row[j], row[j - 1]);        }        diag = up;    }}콘솔.쓰기(row[m]);