첫 세 문제를 풀어내는 데 좀 많은 시간을 써서, 대회 시간 내에 풀지는 못했지만, 만약 저에게 2시간만 더 있었다면 이 문제를 풀어낼 수 있었을 겁니다...

1. 생각의 시작

일단 간단한 재귀를 생각해보는 것으로 출발했습니다. 대략 두 칸 넘어갈 때마다 횟수가 3배 정도 되는 방법들이 나왔는데, 가장 마지막에 생각했던 간단한 재귀는 다음과 같습니다.

1234567 - - → 127 - 3456 → 1 27 3456 → 13456 27 - → 1 234567 - → - 234567 1 → ...

한편 여기서 중요한 성질들을 조금 잡아냈습니다.

1. 반드시 "1 234567 -"이 되어야만 "- 234567 1"로 넘어가면서 진행할 수 있다.

2. 모든 이동은 가역적이기 때문에, "- - 1234567 → - 234567 1"의 과정은 앞에서 만든 것을 활용할 수 있으므로, 이 과정을 정확히 반대로 수행하면 "- 234567 1 → - - 1234567"을 만들 수 있다.

특히 2번 성질은 구현량을 효과적으로 줄여주기 때문에 매우 중요했습니다.

2. 좌절

위에 나와 있는 재귀의 식을 구해보면

f(n) = 2(g(n) + 2 + h(n)) + 1

g(n) = g(n-2) + 2 + h(n-2)

h(n) = h(n-2) + 1 + f(n-5) + 1 + h(n-2) + 1 + h(n-2)

정도의 식이 나왔습니다. 만점의 희망을 갖고 dp를 계산한 결과는..!

N=23 → 941745, N=24 → 1743913. 140점 정도짜리 풀이가 나왔습니다. 하지만 저는 부분 점수 따위에 만족하고 싶지 않았습니다. 그리고, 사실 "간단한 재귀"라고 했지만 구현이 생각보다 귀찮기 때문에, 완전한 풀이를 만들자고 결심했습니다. 뭐 결과적으로는 이게 실수였던 것 같습니다만... 저는 괜찮습니다. 이미 본선을 네 번이나 나갔기에 미련 없습니다.

3. 규칙 찾기

N=26일 때 만점을 받을 수 있는 길이와의 차이가 6배 이상 되었기 때문에, 방향을 선회해서 N이 작을 때의 최적해를 구해보고, 규칙을 찾아서 답을 만들어낼 수 있는 알고리즘을 구현해보기로 했습니다. BFS 코드에서 checker 코드를 만드는 것도 약간의 수정이면 가능해서, 만드는 김에 checker까지 만들었습니다.

from queue import Queue
N = 11

sd = [(0,1),(0,2),(1,0),(1,2),(2,0),(2,1)]
inv_sd = [2,4,0,5,1,3]
chkd = {}
def f(state, x):
    towers = [[],[],[]]
    for i in range(N):
        towers[state%3].append(i)
        state //= 3
    src, des = sd[x]
    nn, mm = len(towers[src]), len(towers[des])
    if nn == 0:
        return -1
    ppap = towers[src][nn//2]
    if (mm < 1 or towers[des][(mm-1)//2] < ppap) and (mm < 2 or ppap < towers[des][(mm-1)//2+1]):
        towers[src].remove(ppap)
        towers[des].insert((mm+1)//2, ppap)
    else:
        return -1
    nstate = 0
    for i in range(3):
        for j in towers[i]:
            nstate += i*(3**j)
    return nstate

def ppp(state):
    towers = [[],[],[]]
    for i in range(N):
        towers[state%3].append(i+1)
        state //= 3
    print(towers)

if __name__ == '__main__':
    start = 0
    end = (3**N-1)
    bfsq = Queue()
    bfsq.put(start)
    chkd[start] = (0, -1)
    while not bfsq.empty():
        tmp = bfsq.get()
        # print(tmp, chkd[tmp])
        if tmp == end:
            break
        prv = chkd[tmp][0]
        for i in range(6):
            ttmp = f(tmp, i)
            if ttmp != -1 and ttmp not in chkd:
                chkd[ttmp] = (prv+1, i)
                bfsq.put(ttmp)
    y = end
    ans = []
    aans = []
    while y!=start:
        tmp = chkd[y]
        ans.append(tmp[1])
        aans.append(y)
        y = f(y, inv_sd[tmp[1]])
    print(ans[::-1])
    print(''.join([chr(ord('A')+i) for i in ans[::-1]]))
    print(len(ans))
    idx = 0
    for z in aans[::-1]:
        idx += 1
        print(idx, end=' ')
        ppp(z)

# checker
# if __name__ == '__main__':
#     cur = 0
#     N = int(input())
#     with open("output.txt", "r") as ff:
#         ans = ff.readline().strip()
#     # ans = input()
#     cnt = 0
#     for c in ans:
#         xx = ord(c) - ord('A')
#         cur = f(cur, xx)
#         cnt += 1
#         # print(cnt, end=' ')
#         # ppp(cur)
#         if cur == -1:
#             print("boom")
#             exit(0)
#     print(cnt, end=' ')
#     ppp(cur)

이렇게 답을 뽑아놓고, 100개 정도 되는 타워들의 패턴을 보면서, 큰 덩어리가 움직이는 형태가 나오는지를 관찰했습니다. 특히, 예를 들면 N=7인 경우 1과 7이 항상 타워에 들어 있는 상황이라면, 1과 7이 없더라도 똑같은 이동 결과가 나오게 됩니다. 1234567 - - → 127 - 3456를 23456 - - → 2 - 3456과 똑같이 생각할 수 있다는 것이죠. 이것이 "간단한 재귀"에서 재귀 함수를 호출하게 되는 부분입니다. 심지어 이동도 똑같기 때문에 정답 string도 어느 기둥에서 어느 기둥으로 옮기는지만 잘 바꿔주면 됩니다.

1 [[1, 2, 3, 5, 6, 7], [4], []]
2 [[1, 2, 3, 6, 7], [4, 5], []]
3 [[1, 2, 6, 7], [4, 5], [3]]
4 [[1, 2, 7], [4, 5], [3, 6]]
5 [[1, 2, 5, 7], [4], [3, 6]]
6 [[1, 2, 5, 7], [], [3, 4, 6]]
7 [[1, 2, 7], [], [3, 4, 5, 6]]

8 [[1, 7], [2], [3, 4, 5, 6]]
9 [[1], [2, 7], [3, 4, 5, 6]]

10 [[1, 5], [2, 7], [3, 4, 6]]
11 [[1, 5], [2, 4, 7], [3, 6]]
12 [[1], [2, 4, 5, 7], [3, 6]]
13 [[1, 6], [2, 4, 5, 7], [3]]
14 [[1, 3, 6], [2, 4, 5, 7], []]
15 [[1, 3, 5, 6], [2, 4, 7], []]
16 [[1, 3, 5, 6], [2, 7], [4]]
17 [[1, 3, 6], [2, 7], [4, 5]]
18 [[1, 6], [2, 3, 7], [4, 5]]
19 [[1], [2, 3, 6, 7], [4, 5]]
20 [[1, 5], [2, 3, 6, 7], [4]]
21 [[1, 5], [2, 3, 4, 6, 7], []]
22 [[1], [2, 3, 4, 5, 6, 7], []]

23 [[], [2, 3, 4, 5, 6, 7], [1]]

24 [[], [2, 3, 4, 6, 7], [1, 5]]
25 [[4], [2, 3, 6, 7], [1, 5]]
26 [[4, 6], [2, 3, 7], [1, 5]]
27 [[4, 6], [2, 7], [1, 3, 5]]
28 [[4], [2, 6, 7], [1, 3, 5]]
29 [[], [2, 6, 7], [1, 3, 4, 5]]
30 [[6], [2, 7], [1, 3, 4, 5]]
31 [[6], [2, 4, 7], [1, 3, 5]]
32 [[], [2, 4, 6, 7], [1, 3, 5]]
33 [[3], [2, 4, 6, 7], [1, 5]]
34 [[3, 6], [2, 4, 7], [1, 5]]
35 [[3, 4, 6], [2, 7], [1, 5]]
36 [[3, 4, 5, 6], [2, 7], [1]]
37 [[3, 4, 5, 6], [2], [1, 7]]
38 [[3, 4, 5, 6], [], [1, 2, 7]]
39 [[3, 4, 6], [], [1, 2, 5, 7]]
40 [[3, 6], [4], [1, 2, 5, 7]]
41 [[3, 6], [4, 5], [1, 2, 7]]
42 [[3], [4, 5], [1, 2, 6, 7]]
43 [[], [4, 5], [1, 2, 3, 6, 7]]
44 [[], [4], [1, 2, 3, 5, 6, 7]]
45 [[], [], [1, 2, 3, 4, 5, 6, 7]]

N=7일 때를 간단히 보자면, 1234567 - - → 127 - 3456을 23456 - - → 2 - 3456로 환원할 수 있습니다. 또한, 이후 1 27 3456 → 1 234567 - 역시 1 - 3456 → 1 3456 -로 환원할 수 있습니다. 이렇게 하면 1234567 - - → 1 234567 -가 끝났고, 위에서 언급했듯이 전체 답도 만들어낼 수 있습니다.

많은 시행착오 끝에 다음과 같은 5가지 점화식으로 모든 것을 나타낼 수 있었습니다.

f(9,^,0,0) = f(7,^,0,0) + 2 + f(6,v,^,0) + 1 + f(6,^,v,0)
f(9,0,0,^) = f(8,v,^,0) + 1 + f(8,^,v,0)
f(9,^,v,0) = f(7,^,v,0) + 1 + f(6,0,0,^) + 2 + f(7,^,^,0)
f(9,^,^,0) = f(6,0,0,^) + 2 + f(5,^,^,0) + 1 + f(6,^^,0,^) + f(6,0,^^,^) + f(5,^,^,0) + 2 + f(6,0,0,^)
f(9,^^,0,^) = f(7,0,0,^) + 2 + f(6,^,^,0) + 1 + f(5,0,0,^) + 2 + f(4,^,^,0) + 1 + f(5,^^,0,^)

f(9,^,0,0): 123456789A - - => 1 23456789A -
f(9,0,0,^): 23456789A - 1 => - 23456789A 1
f(9,^,v,0): 123456789A B - => 1 23456789AB -
f(9,^,^,0): 13456789AB 2 - => 1 23456789AB -
f(9,^^,0,^): 13456789ABC - 2 => 13 456789ABC 2

실제 구현과는 조금 다른데, 세 번째 케이스가 바닥이 목표 기둥이 아닌 곳에 있어야 더 빠를 때가 있어서 해당 부분을 flag로 넣어 최적화를 했습니다.

저 점화식을 쓸 때 기둥 위치까지 같이 썼다면 좀 더 구현이 빨랐을 수도 있겠네요.

4. 구현, 그리고 대회 종료

구현을 하다가 결국 완성된 코드를 만들지 못하고 대회가 종료되었습니다. 이후 열심히 코딩을 했지만 2시간이 더 걸렸습니다. 아이디어 완성 이후부터만 따지더라도 전체 코딩 시간이 4시간은 넘는 것 같습니다.

처음에 N이 작은 경우에 대한 하드코딩을 좀 덜 했더니 1007835라는 조금 아쉬운 길이가 나왔습니다. 작은 경우에 대한 예외처리를 열심히 추가했더니 976371짜리 만점 답안이 나왔습니다. 출제자가 확인한 972051보다는 큰데, 더 줄이기 위해서는 아마 세 번째 케이스에 대한 완벽한 최적화가 필요할 것 같습니다.

아래는 답을 만드는 코드입니다.

#include <bits/stdc++.h>
using namespace std;
pair<int,int> i2p[6] = {{0,1},{0,2},{1,0},{1,2},{2,0},{2,1}};
map<pair<int,int>, int> p2i;
int ggr[6] = {2,4,0,5,1,3};
void init(){
	for(int i=0;i<6;i++) p2i[i2p[i]] = i;
}
string cv(string s, int a, int b, int c){
	string res = "";
	int k = s.size();
	int xxx[3] = {a, b, c};
	for(int i=0;i<k;i++){
		pair<int,int> tmp = i2p[s[i] - 'A'];
		res += (char)('A'+p2i[{xxx[tmp.first], xxx[tmp.second]}]);
	}
	return res;
}
string geoguro(string s){
	string res = "";
	int k = s.size();
	for(int i=k-1;i>=0;i--) res += (char)('A'+ggr[s[i]-'A']);
	return res;
}
string f1(int n, int a, int b, int c);
string f2(int n, int a, int b, int c);
string f3(int n, int a, int b, int c, bool flag);
string f4(int n, int a, int b, int c);
string f5(int n, int a, int b, int c);
int main(){
	// freopen("output.txt", "w", stdout);
	init();
	int N = 26;
	cout << "Case #1\n";
	cout << N << "\n";
	string ans;
	ans += f1(N-1, 0, 1, 2);
	ans += "B";
	ans += geoguro(f1(N-1, 2, 1, 0));
	cout << ans << "\n";
	cout << ans.size();
}
string f1(int n, int a, int b, int c){
	if(n==0) return "";
	if(n==1) return cv("A", a, b, c);
	if(n==2) return cv("AA", a, b, c);
	if(n==3) return cv("BAAF", a, b, c);
	if(n==4) return cv("BBAAEFA", a, b, c);
	string res = "";
	res += f1(n-2, a, c, b);
	res += (char)('A'+p2i[{a,b}]);
	res += (char)('A'+p2i[{a,b}]);
	res += geoguro(f3(n-3, a, c, b, true));
	res += f3(n-3, a, b, c, false);
	// cout << "f1 " << n << " " << a << " " << b << " " << c << ": " << res << "\n";
	return res;
}
string f2(int n, int a, int b, int c){
	if(n==0) return "";
	if(n==1) return cv("A", a, b, c);
	if(n==2) return cv("BAF", a, b, c);
	if(n==3) return cv("ABBCFFA", a, b, c);
	string res = "";
	res += geoguro(f3(n-1, c, a, b, false));
	res += f3(n-1, c, b, a, true);
	return res;
}
string f3(int n, int a, int b, int c, bool flag){
	if(n==0) return flag?"":cv("F", a, b, c);
	if(n==1) return flag?cv("DAF", a, b, c):cv("AF", a, b, c);
	if(n==2) return flag?cv("DAFA", a, b, c):cv("AFA", a, b, c);
	string res = "";
	if(flag){
		res += f3(n-2, a, c, b, false);
	}
	else{
		res += f3(n-2, a, c, b, true);
	}
	res += (char)('A'+p2i[{a,b}]);
	res += f2(n-3, c, a, b);
	res += (char)('A'+p2i[{c,b}]);
	res += (char)('A'+p2i[{c,b}]);
	res += f4(n-2, a, b, c);
	return res;
}
string f4(int n, int a, int b, int c){
	if(n==0) return "";
	if(n==1) return cv("A", a, b, c);
	if(n==2) return cv("BAF", a, b, c);
	if(n==3) return cv("ABBCFFA", a, b, c);
	if(n==4) return cv("BAABCDAECFAAF", a, b, c);
	if(n==5) return cv("ABDAAFBDCDAECEFBAAEFA", a, b, c);
	string res = "";
	res += f2(n-3, a, c, b);
	res += (char)('A'+p2i[{a,b}]);
	res += (char)('A'+p2i[{a,b}]);
	res += f4(n-4, c, b, a);
	res += (char)('A'+p2i[{a,c}]);
	res += f5(n-3, b, c, a);
	res += (char)('A'+p2i[{b,a}]);
	res += geoguro(f5(n-3, a, c, b));
	res += (char)('A'+p2i[{c,b}]);
	res += f4(n-4, a, c, b);
	res += (char)('A'+p2i[{a,b}]);
	res += (char)('A'+p2i[{a,b}]);
	res += f2(n-3, c, b, a);
	return res;
}
string f5(int n, int a, int b, int c){
	if(n==0) return "";
	if(n==1) return cv("BAE", a, b, c);
	if(n==2) return cv("ABAE", a, b, c);
	if(n==3) return cv("ABBAEF", a, b, c);
	if(n==4) return cv("BAFBBDAFEF", a, b, c);
	string res = "";
	res += f2(n-2, a, b, c);
	res += (char)('A'+p2i[{a,c}]);
	res += (char)('A'+p2i[{a,c}]);
	res += f4(n-3, b, c, a);
	res += (char)('A'+p2i[{a,b}]);
	res += f2(n-4, c, b, a);
	res += (char)('A'+p2i[{c,a}]);
	res += (char)('A'+p2i[{c,a}]);
	res += f4(n-5, b, a, c);
	res += (char)('A'+p2i[{c,b}]);
	res += f5(n-4, a, b, c);
	return res;
}

끝으로 마지막 디버깅 단계에서 썼던, 실제 이동 과정이 들어있는 메모를 첨부합니다.

# f5
13456...WXYZ - 2
13YZ 456..WX 2
1Z 456..WX 23Y
1Z 4 2356..WXY
1 4Z 2356..WXY
1 456..WZ 23XY
13X 456..WZ 2Y
136..WX 45Z 2Y
136..WX 45YZ 2
13456..WXYZ - 2

# f1
123..XYZ - -
12Z - 3..XY
1 2Z 3..XY
13..X 2Z Y
13..X 2YZ -
1 23..XYZ -

# f3
1234..XY Z -
1234..XY - Z
12Y - 34..XZ
1Y 2 34...XZ
14..XY 2 3Z
14..XY 23Z -
1 234..XYZ -

# f2
2..YZ - 1
Z - 12..Y
- Z 12..Y
- 2..YZ 1

# f4
1345..XYZ 2 -
13YZ 2 45..X
1Z 23Y 45..X
1Z 235..XY 4
1 235..XY 4Z
1 23 45..XYZ
13 2 45..XYZ
135..XY 2 4Z
135..XY 2Z 4
13Y 2Z 45..X
1 23YZ 45..X

+ Recent posts