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;
}
}