LeetCode的石子游戏系列

整理一下亚历克斯和李用两个人闲来无聊玩的7场石子游戏(╯‵□′)╯︵┻━┻

总结

双方博弈的题一般都是动态规划解决。也不排除像第一场游戏有讨巧的解法,或者第6场用的贪心方法。
一般来讲,两端都可以取,考虑二维dp;只有一端,考虑一维dp;任意取,大概率贪心。
两端的情况,推荐顺序(游戏7->游戏5)
一端的情况,推荐顺序(游戏3->游戏4->游戏2)
对dp的设定有两种,一种是对于这部分石子,当前选手能拿到的最高分数,另一种是对于这部分石子,当前选手能拿到的最大差值。一般设为最大差值可以减少一些不必要的运算。

这里讲一下为什么用差值比较好
假设当前选手A取i位置的石子
最大差值
	dp[i] = 新增的分数-dp[i+1]
	解释dp[i+1]表示下一个选手B取得最大差值 = b-a
	新增的分数-(b-a) = a0+a-b = 当前选手A能得到的最大差值
最高分数
	dp[i] = 新增的分数+(dp[i+1]之后能给双方选手带来的最大分数-dp[i+1])
	解释dp[i+1]表示下一个选手B取得最大分数 = b
	新增的分数+(dp[i+1]之后能给双方选手带来的分数之和-dp[i+1]) = a0+(a+b-b) = a0+a=当前选手A能得到的最高分数
很明显可以看到对于dp[i+1]之后能给双方选手带来的分数之和若是条件稍微变一下,是不太容易求的。

具体题解

877. 石子游戏

本题用动态规划肯定是可以解的,但是本题有几个限制,让这题有了更简单的解法。一则,石子为奇数(肯定有胜负);二则,只需要判断胜负(不需要找到最优解),三则,石子堆数为偶数(每人拿的堆数都一样)。
这么解释,把一堆石子按奇偶数分,奇数位置的和,偶数位置的和,两者肯定有一个更大的,作为先手的话,他可以选取大一些的那个。
比如说,先手A想拿偶数位置的,只要上来拿0编号,对于后手B,只能拿1编号和n-1编号(均为奇数),然后B任意拿一个,肯定会暴露出一个偶数编号,A继续拿这个偶数编号的。
所以说先手总能选大一些的那一堆,即肯定会赢。

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        return true;
    }
};

1140. 石子游戏 II

// dp[i][j]表示剩余[i : len - 1]堆时,M = j的情况下,先取的人能获得的最多石子数
// dp[i][M]=max{dp[i][M],sum[i:n-1]-dp[i+x][max(M,x)]}
class Solution {
public:
    int stoneGameII(vector<int>& piles) {
        int n = piles.size();
        int preSum = 0;
        vector<vector<int>> dp(n, vector<int>(n+1));
        for(int i = n-1; i >= 0; -- i){
            preSum += piles[i];
            for(int M = 1; M <= n; ++ M){
                if(i + 2*M >= n) 
                    dp[i][M] = preSum;
                else{
                    for(int x = 1; x <= 2*M; ++ x)
                        dp[i][M] = max(dp[i][M], preSum - dp[i+x][max(M, x)]);
                }
            }
        }
        return dp[0][1];
    }
};

1406. 石子游戏 III

这个是最经典的石子游戏的动态规划解决办法。
对于当前选手来说,我有三种取法,那当前选手选择的应该是能让他与下一个选手差值最大的。

//dp[i]: 对于石子stones[i..n-1]序列,当前选手可以取到的最大差值
//dp[i] = max(
//		stone[i]-dp[i+1],
//		stone[i]+stone[i+1]-dp[i+2],
//		stone[i]+stone[i+1]+stone[i+2]-dp[i+3],
//)
class Solution {
public:
    string stoneGameIII(vector<int>& stoneValue) {
        int n = stoneValue.size();
        vector<int> dp(n+1, INT_MIN);
        dp[n] = 0; 
        for(int i = n-1; i >= 0; -- i){
            int pre = 0;
            for(int j = i+1; j <= i+3 && j <= n; ++ j){
                pre += stoneValue[j-1];
                dp[i] = max(dp[i], pre - dp[j]);
            }
        }
        return dp[0] == 0 ? "Tie" : dp[0] > 0 ? "Alice" : "Bob";  
    }
};

1510. 石子游戏 IV

这题石子没有分数,其实相当于也是从一端开始拿,也是一位dp。不难想象对于石子数为n,当前选手有K种拿法, k 2 < = n , k ϵ [ 1 , K ] k^2<=n, k\epsilon[1,K] k2<=n,kϵ[1,K],那当前选手想赢的话,只要有一个k满足dp[n-k*k]必输,那当前选手就必赢。

//dp[i]: i个石子当前选手的输赢状态
class Solution {
public:
    bool winnerSquareGame(int n) {
        vector<bool> dp(n+1, false);
        for(int i = 1; i <= n; ++ i){
            for(int k = 1; k*k <=  i; ++ k)
                if(!dp[i-k*k]){
                    dp[i] = true;
                    break;
                }
        }
        return dp[n];
    }
};

1563. 石子游戏 V

本题也是二维dp的问题,你可以把他理解成从左端或者从右端拿掉一堆石子的问题。
只不过这里用一个要遍历k(分隔线),至于是拿k的左半部分,还是k的右半部分就取决于左半部分的和相比右半部分的和哪个更大了。计算一段序列的和可以考虑用前缀和数组。
不过本题其实可以优化一下,复杂度降为n^2,这里暂时不讨论。

/*
dp[i][j]: 对于石子stone[i...j],Alice能得到的最大分数
在[i+1,j] 中遍历k,找到分数最大的那个作为dp[i][j]
对于分隔线k,将[i...j]分隔为[i..k-1]和[k...j]
当sum[i...k-1]>sum[k...j] 分数为sum[k...j]+dp[k][j]
当sum[i...k-1]<sum[k...j] 分数为sum[i...k-1]+dp[i][k-1]
当sum[i...k-1]=sum[k...j] 分数为sum[k...j]+max(dp[k][j],dp[i][k-1])
*/
class Solution {
public:
    int stoneGameV(vector<int>& stoneValue) {
        int n = stoneValue.size();
        int pre[505]={0};
        int dp[505][505];
        for(int i = 1; i <= n; ++ i)
            pre[i] += pre[i-1] + stoneValue[i-1];
        for(int i = n-2; i >= 0; -- i){
            for(int j = i+1; j < n; ++ j){
                dp[i][j] = INT_MIN;
                for(int k = i+1; k <= j; ++ k){
                    int L = pre[k] - pre[i];
                    int R = pre[j+1] - pre[k];
                    if(L > R)
                        dp[i][j] = max(dp[i][j], R + dp[k][j]);
                    else if(L < R)
                        dp[i][j] = max(dp[i][j], L + dp[i][k-1]);
                    else
                        dp[i][j] = max(dp[i][j], R + max(dp[i][k-1], dp[k][j]));
                }
            }
        }
        return dp[0][n-1];
    }
};

1686. 石子游戏 VI

本题是贪心解法。
假设有两个石子,对a来说价值是a1, a2,对b来说价值是b1, b2。
若a取1,价值差为a1-b2
若a取2,价值差为a2-b1
那取哪个只需要判断a1-b2 与a2-b1的大小,移项后即只要比较a1+b1与a2+b2的大小。
对先手来说,尽可能取这个石头总价值高的。
整个思路就有了,把石头按总价值排序,依次取就可以。

class Solution {
public:
    int stoneGameVI(vector<int>& aliceValues, vector<int>& bobValues) {
        vector<vector<int>> Stones;
        int n = aliceValues.size();
        for(int i = 0; i < n; ++ i)
            Stones.push_back(vector<int>{aliceValues[i]+bobValues[i], aliceValues[i], bobValues[i]});
        sort(Stones.begin(), Stones.end(), [](vector<int>& a, vector<int>& b){return a[0] > b[0];});
        int aliceSum = 0, bobSum = 0;
        for(int i = 0; i < n; ++ i){
            if(i % 2)
                bobSum += Stones[i][2];
            else
                aliceSum += Stones[i][1];
        }
        return aliceSum == bobSum ? 0 : (aliceSum > bobSum ? 1 : -1);
    }
};

1690. 石子游戏 VII

两端都可以取,二维dp;用到一段序列的和,前缀和数组。

/*
dp[i][j]:对于石子序列stone[i...j],当前选手能获得的最大分数差值
对于dp[i][j]
当前选手有两种取法
sum[i+1...j]-dp[i+1][j]
sum[i...j-1]-dp[i][j-1]
*/
class Solution {
public:
    int stoneGameVII(vector<int>& stones) {
        int n = stones.size();
        vector<int> sum(n+1, 0);
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for(int i = 1; i <= n; ++ i)
            sum[i] = sum[i-1] + stones[i-1];
        for(int i = n-2; i >= 0; -- i){
            for(int j = i + 1; j < n; ++ j){
                if(i == j-1) dp[i][j] = max(stones[i], stones[j]);
                else{
                    dp[i][j] = max(
                        sum[j+1]-sum[i+1]-dp[i+1][j],
                        sum[j]-sum[i]-dp[i][j-1]
                    );
                }
            }
        }
        return dp[0][n-1];
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值