# ----------------------------------------------------------------------------
# -- HNU CE 데이터베이스(2025년 2학기 02분반): FD 관련 프로그래밍 과제 Part A
# ----------------------------------------------------------------------------
# -- 이름: 이은섭
# -- 학번: 20210508
# ----------------------------------------------------------------------------
import io
from contextlib import redirect_stdout
# 위 두개 import하는 이유:
# (2) 구할때 (1)의 closure() 사용하려고 하는데
# 함수 안에 print문이 출력 안되게 하려고
# (1) 3.2.4절 Algorithm 3.7 관련 closure 함수
# closure : FD 집합 , 속성 집합 -> 속성 집합
# closure(S, {A1,A2...})는 {A1,A2,...}+를 계산
def closure( S, R) :
# 0. S 출력
print ( "S = {" , end= '' )
for FD in S:
print ( ' ' .join ( FD[ 0 ] ) , "->" , ' ' .join ( FD[ 1 ] ) , end= ", " if S.index ( FD) != S.index ( S[ -1 ] ) else "" )
print ( "}" )
# 1. split
temp = [ ]
for FD in S:
if len ( FD[ 1 ] ) >= 2 :
for j in range ( len ( FD[ 1 ] ) ) :
temp.append ( ( FD[ 0 ] , [ FD[ 1 ] [ j] ] ) )
S.remove ( FD)
S += temp
# print("========FD Spliting========")
# print(S)
# 2. 초기화
X = R.copy ( )
# print("========X Reset========")
# print(X)
# 3. 반복 확장
temp = [ ]
while ( temp != X) :
temp = X.copy ( )
for FD in S:
if set ( FD[ 0 ] ) .issubset ( set ( X) ) and not ( set ( FD[ 1 ] ) .issubset ( set ( X) ) ) :
X += FD[ 1 ]
X.sort ( )
# print("========Repeat Expansion========")
# print(X)
# 4. X 출력과 결과 반환
print ( "{" , end= '' )
for i in R:
print ( i, end= ',' if R.index ( i) != R.index ( R[ -1 ] ) else "" )
print ( "}+ = " , end= '' )
print ( "{" , end= '' )
for i in X:
print ( i, end= ',' if X.index ( i) != X.index ( X[ -1 ] ) else "" )
print ( "}" )
return X
# (2) 주어진 한 FD가 기존의 FD 집합으로부터 유도 가능한지 검사하는 함수 (closure 활용하면 간단)
# is_derived_from : FD , FD 집합 -> Bool
# is_derived_from(fd, S)는 fd가 S로부터 유도 가능하면 참, 그렇지 않으면 거짓
def is_derived_from( fd, S) :
# 0. S 출력
print ( "S = {" , end= '' )
for FD in S:
print ( ' ' .join ( FD[ 0 ] ) , "->" , ' ' .join ( FD[ 1 ] ) , end= ", " if S.index ( FD) != S.index ( S[ -1 ] ) else "" )
print ( "}" )
# 1. 주어진 fd에서 좌항 뽑아내 리스트(R형식으로 만들기)
R = fd[ 0 ]
# print("========fd to List========")
# print(R)
# 2. closure 함수 이용해 좌항에 있는 속성의 클로저 구하기
f = io.StringIO ( ) # closure()안에 있는 print안나오게 하려고 씀
with redirect_stdout( f) :
X = closure( S, R)
# print("========closure calcurate========")
# print(R)
# 3. X값 안에 fd의 우항이 있는지 확인
derived = set ( fd[ 1 ] ) .issubset ( set ( X) )
print ( ' ' .join ( fd[ 0 ] ) , "->" , ' ' .join ( fd[ 1 ] ) , " is derived from S :" , derived)
# 4. 결과 값 반환
return derived
# (3) 3.2.7절 관련 주어진 FD 집합이 기존 FD집합의 basis인지 검사
# is_basis_of :: FD 집합 , FD 집합 -> Bool
# is_basis_of(B, S)는 B가 S의 basis이면 (minimal이 아닌 경우도 포함) 참, 그렇지 않으면 거짓
def is_basis_of( B, S) :
# 0. B, S 출력
print ( "S = {" , end= '' )
for FD in S:
print ( ' ' .join ( FD[ 0 ] ) , "->" , ' ' .join ( FD[ 1 ] ) , end= ", " if S.index ( FD) != S.index ( S[ -1 ] ) else "" )
print ( "}" )
print ( "B = {" , end= '' )
for FD in B:
print ( ' ' .join ( FD[ 0 ] ) , "->" , ' ' .join ( FD[ 1 ] ) , end= ", " if B.index ( FD) != B.index ( B[ -1 ] ) else "" )
print ( "}" )
# 1. 두 FD들의 집합 B, S의 좌항에 있는 속성 뽑아 합집합으로 만듬
lefts = [ ]
for FD in B:
if ( FD[ 0 ] not in lefts) :
lefts.append ( FD[ 0 ] )
for FD in S:
if ( FD[ 0 ] not in lefts) :
lefts.append ( FD[ 0 ] )
# print("========Combine leftS and leftB========")
# print(lefts)
# 2. B와 S각각 lefts의 속성들 closure함수 사용하여 비교 둘이 같으면 basis O 틀리면 X
basis = True
for attr in lefts:
f = io.StringIO ( ) # closure()안에 있는 print안나오게 하려고 씀
with redirect_stdout( f) :
if closure( S, attr) != closure( B, attr) :
basis = False
break
# 3. 출력 및 반환
print ( "B is basis of S :" , basis)
return basis
# ===============================TEST===============================
# (1)-1. closure함수 예제 1 (Example 3.8)
S = [
( [ 'A' , 'B' ] , [ 'C' ] ) ,
( [ 'B' , 'C' ] , [ 'A' , 'D' ] ) ,
( [ 'C' , 'F' ] , [ 'B' ] ) ,
( [ 'D' ] , [ 'E' ] )
]
R = [ 'A' , 'B' ]
print ( "== Example for closure test 1=============================" )
X = closure( S, R)
print ( "" )
# (1)-2. closure함수 예제 2 (MyExample)
R = [ 'C' , 'F' ]
print ( "== Example for closure test 2=============================" )
X = closure( S, R)
print ( "" )
# (2)-1. is_drived_from함수 예제 1 (MyExample)
S = [
( [ 'A' , 'B' ] , [ 'C' ] ) ,
( [ 'B' , 'C' ] , [ 'A' , 'D' ] ) ,
( [ 'C' , 'F' ] , [ 'B' ] ) ,
( [ 'D' ] , [ 'E' ] )
]
fd = ( [ 'A' , 'B' ] , [ 'C' , 'D' ] )
print ( "== Example for is_derived_from test 1=====================" )
is_derived_from( fd, S)
print ( "" )
# (2)-2. is_drived_from함수 예제 2 (MyExample)
S = [
( [ 'A' , 'B' ] , [ 'C' ] ) ,
( [ 'B' , 'C' ] , [ 'A' , 'D' ] ) ,
( [ 'C' , 'F' ] , [ 'B' ] ) ,
( [ 'D' ] , [ 'E' ] )
]
fd = ( [ 'A' , 'B' ] , [ 'C' , 'D' , 'F' ] )
print ( "== Example for is_derived_from test 2=====================" )
is_derived_from( fd, S)
print ( "" )
# (3)-1. is_basis_of 함수 예제 1 (example 3.11)
S = [
( [ 'A' ] , [ 'B' ] ) ,
( [ 'B' ] , [ 'C' ] ) ,
( [ 'C' ] , [ 'A' ] )
]
B = [
( [ 'A' ] , [ 'B' ] ) ,
( [ 'B' ] , [ 'A' ] ) ,
( [ 'B' ] , [ 'C' ] ) ,
( [ 'C' ] , [ 'B' ] )
]
print ( "== Example for is_basis_of test 1=====================" )
is_basis_of( B, S)
print ( "" )
# (3)-2. is_basis_of 함수 예제 2(MyExample)
S = [
( [ 'A' ] , [ 'B' ] ) ,
( [ 'B' ] , [ 'C' ] ) ,
( [ 'C' ] , [ 'A' ] )
]
B = [
( [ 'A' ] , [ 'B' ] ) ,
( [ 'B' ] , [ 'A' ] ) ,
( [ 'D' ] , [ 'F' ] ) ,
( [ 'C' ] , [ 'B' ] )
]
print ( "== Example for is_basis_of test 2=====================" )
is_basis_of( B, S)
print ( "" )
IyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiMgLS0gSE5VIENFIOuNsOydtO2EsOuyoOydtOyKpCgyMDI164WEIDLtlZnquLAgMDLrtoTrsJgpOiBGRCDqtIDroKgg7ZSE66Gc6re4656Y67CNIOqzvOygnCBQYXJ0IEEKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiMgLS0g7J2066aEOiDsnbTsnYDshK0KIyAtLSDtlZnrsog6IDIwMjEwNTA4CiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKCmltcG9ydCBpbwpmcm9tIGNvbnRleHRsaWIgaW1wb3J0IHJlZGlyZWN0X3N0ZG91dCAKIyDsnIQg65GQ6rCcIGltcG9ydO2VmOuKlCDsnbTsnKA6IAojICgyKSDqtaztlaDrlYwgKDEp7J2YIGNsb3N1cmUoKSDsgqzsmqntlZjroKTqs6Ag7ZWY64qU642wIAojIO2VqOyImCDslYjsl5AgcHJpbnTrrLjsnbQg7Lac66ClIOyViOuQmOqyjCDtlZjroKTqs6AKCiMgKDEpIDMuMi407KCIIEFsZ29yaXRobSAzLjcg6rSA66CoIGNsb3N1cmUg7ZWo7IiYCgojIGNsb3N1cmUgOiBGRCDsp5HtlakgLCDsho3shLEg7KeR7ZWpIC0+IOyGjeyEsSDsp5HtlakKIyBjbG9zdXJlKFMsIHtBMSxBMi4uLn0p64qUIHtBMSxBMiwuLi59K+ulvCDqs4TsgrAKZGVmIGNsb3N1cmUoUywgUik6CiAgICAjIDAuIFMg7Lac66ClCiAgICBwcmludCgiUyA9IHsiLCBlbmQ9JycpCiAgICBmb3IgRkQgaW4gUzoKICAgICAgICBwcmludCgnICcuam9pbihGRFswXSksICItPiIsICAnICcuam9pbihGRFsxXSksIGVuZD0iLCAiIGlmIFMuaW5kZXgoRkQpICE9IFMuaW5kZXgoU1stMV0pIGVsc2UgIiIpCiAgICBwcmludCgifSIpCgogICAgIyAxLiBzcGxpdAogICAgdGVtcCA9IFtdCiAgICBmb3IgRkQgaW4gUzoKICAgICAgICBpZiBsZW4oRkRbMV0pID49IDI6CiAgICAgICAgICAgIGZvciBqIGluIHJhbmdlKGxlbihGRFsxXSkpOgogICAgICAgICAgICAgICAgdGVtcC5hcHBlbmQoKEZEWzBdLCBbRkRbMV1bal1dKSkKICAgICAgICAgICAgUy5yZW1vdmUoRkQpCiAgICBTICs9IHRlbXAKICAgICMgcHJpbnQoIj09PT09PT09RkQgU3BsaXRpbmc9PT09PT09PSIpCiAgICAjIHByaW50KFMpCgogICAgIyAyLiDstIjquLDtmZQKICAgIFggPSBSLmNvcHkoKQogICAgIyBwcmludCgiPT09PT09PT1YIFJlc2V0PT09PT09PT0iKQogICAgIyBwcmludChYKQogICAgCiAgICAjIDMuIOuwmOuztSDtmZXsnqUKICAgIHRlbXAgPSBbXQogICAgd2hpbGUodGVtcCAhPSBYKToKICAgICAgICB0ZW1wID0gWC5jb3B5KCkKICAgICAgICBmb3IgRkQgaW4gUzoKICAgICAgICAgICAgaWYgc2V0KEZEWzBdKS5pc3N1YnNldChzZXQoWCkpIGFuZCBub3Qoc2V0KEZEWzFdKS5pc3N1YnNldChzZXQoWCkpKToKICAgICAgICAgICAgICAgIFggKz0gRkRbMV0KICAgIFguc29ydCgpCiAgICAjIHByaW50KCI9PT09PT09PVJlcGVhdCBFeHBhbnNpb249PT09PT09PSIpCiAgICAjIHByaW50KFgpCgogICAgIyA0LiBYIOy2nOugpeqzvCDqsrDqs7wg67CY7ZmYCiAgICBwcmludCgieyIsIGVuZD0nJykKICAgIGZvciBpIGluIFI6CiAgICAgICAgcHJpbnQoaSwgZW5kPScsJyBpZiBSLmluZGV4KGkpICE9IFIuaW5kZXgoUlstMV0pIGVsc2UgIiIpCiAgICBwcmludCgifSsgPSAiLCBlbmQ9JycpCgogICAgcHJpbnQoInsiLCBlbmQ9JycpCiAgICBmb3IgaSBpbiBYOgogICAgICAgIHByaW50KGksIGVuZD0nLCcgaWYgWC5pbmRleChpKSAhPSBYLmluZGV4KFhbLTFdKSBlbHNlICIiKQogICAgcHJpbnQoIn0iKQogICAgCiAgICByZXR1cm4gWAoKCgoKIyAoMikg7KO87Ja07KeEIO2VnCBGROqwgCDquLDsobTsnZggRkQg7KeR7ZWp7Jy866Gc67aA7YSwIOycoOuPhCDqsIDriqXtlZzsp4Ag6rKA7IKs7ZWY64qUIO2VqOyImCAoY2xvc3VyZSDtmZzsmqntlZjrqbQg6rCE64uoKQoKIyBpc19kZXJpdmVkX2Zyb20gOiBGRCAsIEZEIOynke2VqSAtPiBCb29sCiMgaXNfZGVyaXZlZF9mcm9tKGZkLCBTKeuKlCBmZOqwgCBT66Gc67aA7YSwIOycoOuPhCDqsIDriqXtlZjrqbQg7LC4LCDqt7jroIfsp4Ag7JWK7Jy866m0IOqxsOynkwpkZWYgaXNfZGVyaXZlZF9mcm9tKGZkLCBTKToKICAgICMgMC4gUyDstpzroKUKICAgIHByaW50KCJTID0geyIsIGVuZD0nJykKICAgIGZvciBGRCBpbiBTOgogICAgICAgIHByaW50KCcgJy5qb2luKEZEWzBdKSwgIi0+IiwgICcgJy5qb2luKEZEWzFdKSwgZW5kPSIsICIgaWYgUy5pbmRleChGRCkgIT0gUy5pbmRleChTWy0xXSkgZWxzZSAiIikKICAgIHByaW50KCJ9IikKCiAgICAjIDEuIOyjvOyWtOynhCBmZOyXkOyEnCDsooztla0g672R7JWE64K0IOumrOyKpO2KuChS7ZiV7Iud7Jy866GcIOunjOuTpOq4sCkKICAgIFIgPSBmZFswXQogICAgIyBwcmludCgiPT09PT09PT1mZCB0byBMaXN0PT09PT09PT0iKQogICAgIyBwcmludChSKQoKICAgICMgMi4gY2xvc3VyZSDtlajsiJgg7J207Jqp7ZW0IOyijO2VreyXkCDsnojripQg7IaN7ISx7J2YIO2BtOuhnOyggCDqtaztlZjquLAKICAgIGYgPSBpby5TdHJpbmdJTygpICMgY2xvc3VyZSgp7JWI7JeQIOyeiOuKlCBwcmludOyViOuCmOyYpOqyjCDtlZjroKTqs6Ag7JSACiAgICB3aXRoIHJlZGlyZWN0X3N0ZG91dChmKToKICAgICAgICBYID0gY2xvc3VyZShTLCBSKQogICAgCiAgICAjIHByaW50KCI9PT09PT09PWNsb3N1cmUgY2FsY3VyYXRlPT09PT09PT0iKQogICAgIyBwcmludChSKQoKICAgICMgMy4gWOqwkiDslYjsl5AgZmTsnZgg7Jqw7ZWt7J20IOyeiOuKlOyngCDtmZXsnbgKICAgIGRlcml2ZWQgPSBzZXQoZmRbMV0pLmlzc3Vic2V0KHNldChYKSkKICAgIHByaW50KCcgJy5qb2luKGZkWzBdKSwgIi0+IiwgJyAnLmpvaW4oZmRbMV0pLCAiIGlzIGRlcml2ZWQgZnJvbSBTIDoiLCBkZXJpdmVkKQoKICAgICMgNC4g6rKw6rO8IOqwkiDrsJjtmZgKICAgIHJldHVybiBkZXJpdmVkCgoKCiMgKDMpICAzLjIuN+ygiCDqtIDroKgg7KO87Ja07KeEIEZEIOynke2VqeydtCDquLDsobQgRkTsp5HtlansnZggYmFzaXPsnbjsp4Ag6rKA7IKsCgojIGlzX2Jhc2lzX29mIDo6IEZEIOynke2VqSAsIEZEIOynke2VqSAtPiBCb29sCiMgaXNfYmFzaXNfb2YoQiwgUynripQgQuqwgCBT7J2YIGJhc2lz7J2066m0IChtaW5pbWFs7J20IOyVhOuLjCDqsr3smrDrj4Qg7Y+s7ZWoKSDssLgsIOq3uOugh+yngCDslYrsnLzrqbQg6rGw7KeTCmRlZiBpc19iYXNpc19vZihCLCBTKToKICAgICMgMC4gQiwgUyDstpzroKUKICAgIHByaW50KCJTID0geyIsIGVuZD0nJykKICAgIGZvciBGRCBpbiBTOgogICAgICAgIHByaW50KCcgJy5qb2luKEZEWzBdKSwgIi0+IiwgICcgJy5qb2luKEZEWzFdKSwgZW5kPSIsICIgaWYgUy5pbmRleChGRCkgIT0gUy5pbmRleChTWy0xXSkgZWxzZSAiIikKICAgIHByaW50KCJ9IikKCiAgICBwcmludCgiQiA9IHsiLCBlbmQ9JycpCiAgICBmb3IgRkQgaW4gQjoKICAgICAgICBwcmludCgnICcuam9pbihGRFswXSksICItPiIsICAnICcuam9pbihGRFsxXSksIGVuZD0iLCAiIGlmIEIuaW5kZXgoRkQpICE9IEIuaW5kZXgoQlstMV0pIGVsc2UgIiIpCiAgICBwcmludCgifSIpCgogICAgIyAxLiDrkZAgRkTrk6TsnZgg7KeR7ZWpIEIsIFPsnZgg7KKM7ZWt7JeQIOyeiOuKlCDsho3shLEg672R7JWEIO2Vqeynke2VqeycvOuhnCDrp4zrk6wKICAgIGxlZnRzID0gW10KICAgIGZvciBGRCBpbiBCOgogICAgICAgIGlmKEZEWzBdIG5vdCBpbiBsZWZ0cyk6CiAgICAgICAgICAgIGxlZnRzLmFwcGVuZChGRFswXSkKICAgIGZvciBGRCBpbiBTOgogICAgICAgIGlmKEZEWzBdIG5vdCBpbiBsZWZ0cyk6CiAgICAgICAgICAgIGxlZnRzLmFwcGVuZChGRFswXSkKCiAgICAjIHByaW50KCI9PT09PT09PUNvbWJpbmUgbGVmdFMgYW5kIGxlZnRCPT09PT09PT0iKQogICAgIyBwcmludChsZWZ0cykKCiAgICAjIDIuIELsmYAgU+qwgeqwgSBsZWZ0c+ydmCDsho3shLHrk6QgY2xvc3VyZe2VqOyImCDsgqzsmqntlZjsl6wg67mE6rWQIOuRmOydtCDqsJnsnLzrqbQgYmFzaXMgTyDti4DrpqzrqbQgWAogICAgYmFzaXMgPSBUcnVlCiAgICBmb3IgYXR0ciBpbiBsZWZ0czoKICAgICAgICBmID0gaW8uU3RyaW5nSU8oKSAjIGNsb3N1cmUoKeyViOyXkCDsnojripQgcHJpbnTslYjrgpjsmKTqsowg7ZWY66Ck6rOgIOyUgAogICAgICAgIHdpdGggcmVkaXJlY3Rfc3Rkb3V0KGYpOgogICAgICAgICAgICBpZiBjbG9zdXJlKFMsIGF0dHIpICE9IGNsb3N1cmUoQiwgYXR0cik6CiAgICAgICAgICAgICAgICBiYXNpcyA9IEZhbHNlCiAgICAgICAgICAgICAgICBicmVhawogICAgICAgIAogICAgIyAzLiDstpzroKUg67CPIOuwmO2ZmAogICAgcHJpbnQoIkIgaXMgYmFzaXMgb2YgUyA6IiwgYmFzaXMpCiAgICByZXR1cm4gYmFzaXMKCiAgICAKICAgIAoKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVRFU1Q9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgojICgxKS0xLiBjbG9zdXJl7ZWo7IiYIOyYiOygnCAxIChFeGFtcGxlIDMuOCkKUyA9IFsKICAgICAoWydBJywgJ0InXSwgWydDJ10pLCAKICAgICAoWydCJywgJ0MnXSwgWydBJywgJ0QnXSksIAogICAgIChbJ0MnLCAnRiddLCBbJ0InXSksIAogICAgIChbJ0QnXSwgWydFJ10pCiAgICAgXQpSID0gWydBJywgJ0InXQpwcmludCgiPT0gRXhhbXBsZSBmb3IgY2xvc3VyZSB0ZXN0IDE9PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIpClggPSBjbG9zdXJlKFMsIFIpCnByaW50KCIiKQoKIyAoMSktMi4gY2xvc3VyZe2VqOyImCDsmIjsoJwgMiAoTXlFeGFtcGxlKQpSID0gWydDJywgJ0YnXQpwcmludCgiPT0gRXhhbXBsZSBmb3IgY2xvc3VyZSB0ZXN0IDI9PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIpClggPSBjbG9zdXJlKFMsIFIpCnByaW50KCIiKQoKCiMgKDIpLTEuIGlzX2RyaXZlZF9mcm9t7ZWo7IiYIOyYiOygnCAxIChNeUV4YW1wbGUpClMgPSBbCiAgICAgKFsnQScsICdCJ10sIFsnQyddKSwgCiAgICAgKFsnQicsICdDJ10sIFsnQScsICdEJ10pLCAKICAgICAoWydDJywgJ0YnXSwgWydCJ10pLCAKICAgICAoWydEJ10sIFsnRSddKQogICAgIF0KZmQgPSAoWydBJywgJ0InXSwgWydDJywgJ0QnXSkKCnByaW50KCI9PSBFeGFtcGxlIGZvciBpc19kZXJpdmVkX2Zyb20gdGVzdCAxPT09PT09PT09PT09PT09PT09PT09IikKaXNfZGVyaXZlZF9mcm9tKGZkLCBTKQpwcmludCgiIikKCiMgKDIpLTIuIGlzX2RyaXZlZF9mcm9t7ZWo7IiYIOyYiOygnCAyIChNeUV4YW1wbGUpClMgPSBbCiAgICAgKFsnQScsICdCJ10sIFsnQyddKSwgCiAgICAgKFsnQicsICdDJ10sIFsnQScsICdEJ10pLCAKICAgICAoWydDJywgJ0YnXSwgWydCJ10pLCAKICAgICAoWydEJ10sIFsnRSddKQogICAgIF0KZmQgPSAoWydBJywgJ0InXSwgWydDJywgJ0QnLCAnRiddKQoKcHJpbnQoIj09IEV4YW1wbGUgZm9yIGlzX2Rlcml2ZWRfZnJvbSB0ZXN0IDI9PT09PT09PT09PT09PT09PT09PT0iKQppc19kZXJpdmVkX2Zyb20oZmQsIFMpCnByaW50KCIiKQoKCiMgKDMpLTEuIGlzX2Jhc2lzX29mIO2VqOyImCDsmIjsoJwgMSAoZXhhbXBsZSAzLjExKQpTID0gWwogICAgKFsnQSddLCBbJ0InXSksIAogICAgKFsnQiddLCBbJ0MnXSksIAogICAgKFsnQyddLCBbJ0EnXSkKICAgIF0KCkIgPSBbCiAgICAoWydBJ10sIFsnQiddKSwgCiAgICAoWydCJ10sIFsnQSddKSwgCiAgICAoWydCJ10sIFsnQyddKSwgCiAgICAoWydDJ10sIFsnQiddKQogICAgXQoKcHJpbnQoIj09IEV4YW1wbGUgZm9yIGlzX2Jhc2lzX29mIHRlc3QgMT09PT09PT09PT09PT09PT09PT09PSIpCmlzX2Jhc2lzX29mKEIsIFMpCnByaW50KCIiKQoKIyAoMyktMi4gaXNfYmFzaXNfb2Yg7ZWo7IiYIOyYiOygnCAyKE15RXhhbXBsZSkKUyA9IFsKICAgIChbJ0EnXSwgWydCJ10pLCAKICAgIChbJ0InXSwgWydDJ10pLCAKICAgIChbJ0MnXSwgWydBJ10pCiAgICBdCgpCID0gWwogICAgKFsnQSddLCBbJ0InXSksIAogICAgKFsnQiddLCBbJ0EnXSksIAogICAgKFsnRCddLCBbJ0YnXSksIAogICAgKFsnQyddLCBbJ0InXSkKICAgIF0KCnByaW50KCI9PSBFeGFtcGxlIGZvciBpc19iYXNpc19vZiB0ZXN0IDI9PT09PT09PT09PT09PT09PT09PT0iKQppc19iYXNpc19vZihCLCBTKQpwcmludCgiIikK