image.png

문제 설명

메이플 월드의 평화로운 숲속, 예티와 핑크빈은 오늘도 사이좋게 버섯 밭을 가꾸고 있었습니다.

그러던 어느 날, 버섯마다 숫자가 적혀 있다는 사실을 발견한 두 친구는 이 숫자를 활용해 새로운 게임을 만들기로 했습니다.

그들은 버섯 밭의 일부를 점령하며 경쟁하는 게임을 고안했고, 아래의 규칙에 따라 번갈아 가며 영역을 선택해 나갑니다.

게임 규칙

온라인에서 최근에 유행한 사과게임을 모티브로 만든 게임이다. 안에 쓰여있는 수의 합이 10이 되는 직사각형을 선택, 그 땅을 점령하는 방식이다. 최종적으로 더 많은 땅을 가지고 있는 사람이 이긴다. 서버에 제출을 통해 샘플 Ai 14개와의 대결 결과를 살펴볼 수 있고, 매일 한 번의 vs 전국의 참여자 대결을 진행하여, 유저들과의 대결 결과를 살펴볼 수 있다. 연습게임 기간에는 팀들이 바빴기에, 거의 나 혼자 참가하였다.

접근1

우선 게임판의 크기가 $10 \times17$으로 매우 작으므로, 모든 직사각형을 살펴볼 수 있다. 모든 직사각형을 살펴보는 경우의 수는 행에서 2개의 변, 변에서 2개의 변을 고르는 경우의 수와 같으므로, $_{10}C_2 \times _{17}C_2 = 6120$가지이다. 또한 각 직사각형의 내부의 합을 구해야하므로, 각 직사각형 크기정도의 연산이 추가로 소요된다. 따라서 $n\times m$크기 게임판의 모든 직사각형을 살펴보는 시간복잡도는 대략 $O(n^2m^2)$의 연산이 필요하다. $10^217^2$의 값은 $28900$으로 보통 컴퓨터의 1초 연산량을 1억이라 하면 매우 작은 값이므로, 고려할 수 있는 전략이다. 또한 여기서 각 직사각형 내부의 합을 구하는 연산은 2차원 누적합을 통해서 $O(nm)$에 전처리 후 각 쿼리를 $O(1)$에 처리할 수 있으므로, 실질적인 시간복잡도는 $O(nm)$이다. 나는 가장 처음 접근으로, 모든 직사각형을 살펴보며 그리디한 전략을 써서 매 게임판 상태에서 얻을 수 있는 가장 큰 점수를 먹는 전략을 써 보았다.

image.png

결과는 샘플 Ai기준 4승 10패. 그렇게 좋은 결과는 아니였다.

코드:

# ================================
# Game 클래스: 게임 상태 관리
# ================================
class Game:
    

    def __init__(self, board, first):
        self.board = board            # 게임 보드 (2차원 배열)
        self.first = first            # 선공 여부
        self.passed = False           # 마지막 턴에 패스했는지 여부
        self.board_point = []
        self.n = 10
        self.m = 17
        for i in range(self.n):
            for j in range(self.m):
                self.board_point.append((i,j))

    # 사각형 (r1, c1) ~ (r2, c2)이 유효한지 검사 (합이 10이고, 네 변을 모두 포함)
    def isValid(self, r1, c1, r2, c2):
        sums = 0
        r1fit = c1fit = r2fit = c2fit = False

        for r in range(r1, r2 + 1):
            for c in range(c1, c2 + 1):
                if self.board[r][c] != 0:
                    sums += self.board[r][c]
                    if r == r1:
                        r1fit = True
                    if r == r2:
                        r2fit = True
                    if c == c1:
                        c1fit = True
                    if c == c2:
                        c2fit = True
        return sums == 10 and r1fit and r2fit and c1fit and c2fit
    
    def calc_psum(self):
        psum = [[0]*(self.m+1) for _ in range(self.n+1)]
        for i in range(1, self.n+1):
            for j in range(1, self.m+1):
                psum[i][j] = self.board[i-1][j-1]
        
        for i in range(1, self.n):
            for j in range(self.m):
                psum[i][j+1] += psum[i][j]
        
        for i in range(self.n):
            for j in range(1, self.m):
                psum[i+1][j] += psum[i][j]
        
        return psum
    
    def psum_val(self, r1, c1, r2, c2, psum):
        return psum[r2+1][c2+1] - psum[r1][c2+1] - psum[r2+1][c2] + psum[r1][c1]
    
    # ================================================================
    # ===================== [필수 구현] ===============================
    # 합이 10인 유효한 사각형을 찾아 (r1, c1, r2, c2) 튜플로 반환
    # 없으면 (-1, -1, -1, -1) 반환 (패스 의미)
    # ================================================================
    def calculateMove(self, _myTime, _oppTime):
        psum = self.calc_psum()
        is_find = 0
        ret = [-1,-1,-1,-1]
        rect_size = 0
        
        for i in range(len(self.board_point)):
            for j in range(i+1, len(self.board_point)):
                p1 = self.board_point[i]
                p2 = self.board_point[j]
                if self.psum_val(*p1, *p2, psum) == 10:
                    if self.isValid(*p1, *p2):
                        if is_find:
                            rect = (p2[0] - p1[0] + 1) * (p2[1] - p1[1] + 1)
                            if rect_size < rect:
                                rect_size = rect
                                ret = list(p1 + p2)
                        else:
                            is_find = 1
                            ret = list(p1 + p2)
    
        return tuple(ret)
    # =================== [필수 구현 끝] =============================

    # 상대방의 수를 받아 보드에 반영
    def updateOpponentAction(self, action, _time):
        self.updateMove(*action, False)

    # 주어진 수를 보드에 반영 (칸을 0으로 지움)
    def updateMove(self, r1, c1, r2, c2, _isMyMove):
        if r1 == c1 == r2 == c2 == -1:
            self.passed = True
            return
        for r in range(r1, r2 + 1):
            for c in range(c1, c2 + 1):
                self.board[r][c] = 0
        self.passed = False

# ================================
# main(): 입출력 처리 및 게임 진행
# ================================
def main():
    while True:
        line = input().split()

        if len(line) == 0:
            continue

        command, *param = line

        if command == "READY":
            # 선공 여부 확인
            turn = param[0]
            global first
            first = turn == "FIRST"
            print("OK", flush=True)
            continue

        if command == "INIT":
            # 보드 초기화
            board = [list(map(int, row)) for row in param]
            global game
            game = Game(board, first)
            continue

        if command == "TIME":
            # 내 턴: 수 계산 및 실행
            myTime, oppTime = map(int, param)
            ret = game.calculateMove(myTime, oppTime)
            game.updateMove(*ret, True)
            print(*ret, flush=True)
            continue

        if command == "OPP":
            # 상대 턴 반영
            r1, c1, r2, c2, time = map(int, param)
            game.updateOpponentAction((r1, c1, r2, c2), time)
            continue

        if command == "FINISH":
            break

        assert False, f"Invalid command {command}"

if __name__ == "__main__":
    main()