https://leetcode.com/problems/predict-the-winner/description/
코드설명
게임이론(Game Theory) + 동적계획법(Dynamic Programming) + 깊이우선탐색(DFS) 을 활용합니다.
처음에는 완전탐색을 활용하여 해결하려했습니다만, 효율이 좋지 않습니다.
이유는, 게임이 20번 이기에 시간복잡도는 총 2^20 번 이기에 그렇습니다.
또한, 처음에 작성한 코드는 한계점이 존재합니다. 승패를 알 수는 있지만, 최대 몇점 차이로 이기는지는 알 수 없습니다.
자세한 풀이는 아래의 코드에 담았습니다.
코드
정답코드3입니다.
class Solution { int[] Nums; int[][] cache; public boolean predictTheWinner(int[] nums) { Nums = nums; cache = new int[nums.length][nums.length]; for(int i=0; i<nums.length; i++) Arrays.fill(cache[i], Integer.MIN_VALUE); return game(0, nums.length - 1) >= 0; } //현재 Nums에 left, right일때 (이번 차례인 Player의 점수) - (다음 차례인 Player의 점수)의 최대치를 반환합니다. int game(int left, int right){ if(left > right) return 0; if(cache[left][right] != Integer.MIN_VALUE) return cache[left][right]; int ret = 0; int pickLeft = Nums[left] - game(left + 1, right); int pickRight = Nums[right] - game(left, right - 1); ret = Math.max(pickLeft, pickRight); return cache[left][right] = ret; } }
게임이론(Game Theory)를 활용할경우입니다.
Player1의 입장에서는 최대 점수를 선택하고, Player2의 입장에서는 최소 점수를 선택합니다.
이때 원활하게 처리하기 위해 플레이어가 2명이므로 player1은 양수로 표현, player2는 음수로 표현하며 두 플레이어 간의 차이를 Player1은 최대화, Player2는 최소화 시킵니다(Player2는 음수가 본인의 점수이기에)
또한, 점수를 양수 음수로 표현하므로 Integer를 활용하여 null일 경우 무시합니다.
이 방법의 단점은, left와 right만 가지고는 이번 차례가 누군지 알아낼 수 없기에 turn을 사용해야하고, 그 이유로, 부분 문제의 수가 두 배 늘어납니다.
class Solution { public boolean predictTheWinner(int[] nums) { int n = nums.length; Integer[][] memo = new Integer[n][n]; return playGame(nums, 0, n - 1, 1, memo) >= 0; } private int playGame(int[] nums, int left, int right, int turn, Integer[][] memo) { if (left == right) { return nums[left] * turn; } if (memo[left][right] != null) { return memo[left][right]; } int pickLeft = nums[left] * turn + playGame(nums, left + 1, right, -turn, memo); int pickRight = nums[right] * turn + playGame(nums, left, right - 1, -turn, memo); memo[left][right] = turn == 1 ? Math.max(pickLeft, pickRight) : Math.min(pickLeft, pickRight); return memo[left][right]; } }
아래의 틀린코드1을 고친 코드입니다. 문제점은, 모든 플레이어의 입장에서 처리하는 것이 아닌, 플레이어1이 이길 수 있냐 없냐만을 고려하며 그에 맞게 처리합니다.
그렇기에 game() 함수를 isPlayer1Win으로 이름을 바꾸고, 아래와 같이 정의합니다.
isPlayer1Win() : Player1의 입장에서만 처리하며, Player1이 이긴다면 True를 반환하고, 진다면 False를 반환합니다.
이렇게 처리한 후, player1의 입장에서는 2가지 분기되는 재귀함수 중 한가지만 이김(true)를 반환하면 되고,
여기서 중요한점은, player2의 입장에서는 2가지 분기되는 재귀함수 중 두가지 모두 player1이 이겨야만 Player1이 이기는것이므로 && (AND) 연산으로 처리합니다. Player2 입장에서는 Player2가 이기는 선택을 할 것이기에, Player1 입장에서는 Player2의 턴에서도 모두 이겨야만 Player1이 이기는 것을 알 수 있습니다.
class Solution { int[] Nums; public boolean predictTheWinner(int[] nums) { Nums = nums; return isPlayer1Win(0, nums.length - 1, 0, 0, true); } //isPlayer1Win() : Player1의 입장에서만 처리하며, Player1이 이긴다면 True를 반환하고, 진다면 False를 반환합니다. boolean isPlayer1Win(int left, int right, int score1, int score2, boolean turn){ if(left == right){ if(turn == true) score1 += Nums[left]; if(turn == false) score2 += Nums[left]; //만약 score2가 더 크다면, score1 입장에서는 진것이고, score2입장에서는 이긴것이다. if(score1 < score2){ return false; } //score2입장에서는 진것이므로. else if(score1 >= score2){ return true; } } // if(left == right){ 더 좋은 방식입니다. // return score1 >= score2; // } boolean ret = false; //player1 차례일때. //만약 한개라도 return true가 반환된다면 player1이 승리이므로 OR연산을 한다. if(turn == true){ ret = false; ret |= isPlayer1Win(left + 1, right, score1 + Nums[left], score2, !turn); ret |= isPlayer1Win(left, right - 1, score1 + Nums[right], score2, !turn); return ret; } //player2 입장에서는, 2개의 선택에서 player1이 한개라도 지는 경우(false)일 경우가 존재하면 //그 방안을 선택할 것이므로 둘 다 true가 나와야 player1이 이깁니다. else if(turn == false){ ret = true; ret &= isPlayer1Win(left + 1, right, score1, score2 + Nums[left], !turn); ret &= isPlayer1Win(left, right - 1, score1, score2 + Nums[right], !turn); return ret; } return ret; } }
처음에 틀린 코드1입니다. 무엇인가 놓친 것 같습니다.
class Solution { int[] Nums; public boolean predictTheWinner(int[] nums) { Nums = nums; return game(0, nums.length - 1, 0, 0, true); } boolean game(int left, int right, int score1, int score2, boolean turn){ if(left == right){ if(turn == true) score1 += Nums[left]; if(turn == false) score2 += Nums[left]; //만약 score2가 더 크다면, score1 입장에서는 진것이고, score2입장에서는 이긴것이다. if(score1 < score2){ return !false; } //score2입장에서는 진것이므로. else if(score1 >= score2){ return !true; } } //만약 한개라도 return true가 반환된다면 player1이 승리이므로 OR연산을 한다. boolean ret = false; //player1 차례일때. if(turn == true){ ret |= game(left + 1, right, score1 + Nums[left], score2, !turn); ret |= game(left, right - 1, score1 + Nums[right], score2, !turn); } //player2 입장에서도 else if(turn == false){ ret |= game(left + 1, right, score1, score2 + Nums[left], !turn); ret |= game(left, right - 1, score1, score2 + Nums[right], !turn); } return ret; } }