石子游戏--动态规划

本文记录各种各样的石子游戏题目和解法,石子游戏大多动态规划方法来处理,作为一个两人游戏,又可以从博弈的角度考虑。

题目中的英文名字太拗口,我们讨论时均以甲乙作为称呼

石子游戏一

 亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

 亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

 假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

先不考虑偶数问题,假如石子堆可以是奇数的,使用动态规划的思想先定义dp数组 

    dp[i][j] 表示当前从石子堆的i--j中,可以获取的最大数量。

       那么 dp[i][j] 就应该是下面两个情况下的较大者

 1 选前面的, piles[i] + 剩下是石子中在乙选择后,下一步能甲拿到的石子数量

 2选后面的,piles[j]+剩下是石子中在乙选择后,下一步能甲拿到的石子数量

由于乙也会发挥最佳水平,当剩下 plies[i+1]----piles[j] 时,乙也会做一次最佳决策,这样又陷入递归的噩梦里,因此这个递推公式还不完整.

引入一个数组sum[i][j], 代表piles[i] --- plies[j] 所有石子的和

那么当轮到乙挑选时,假设乙发挥最佳水平可以拿到x个石子,那么甲就可以获取剩下的sum[i+1][j] -x个。

因此上述地推公式变成

   1 选前面的, piles[i] + sum[i+1][j] - dp[i+1][j]

             2 选后面的,   plies[j] +sum[i][j-1]  - dp[i][j-1]

class Solution {
public:
    bool stoneGame(vector& piles) {
        int n = piles.size();
        vector> dp(n,vector(n,0));
        vector> sum(n,vector(n,0));

        for(int i=0; isum[0][n-1];
    }
};

另外当总数是偶数时, 第一个拿取的人一定能赢

石子游戏 二
 

亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]。游戏以谁手中的石子最多来决出胜负。

亚历克斯和李轮流进行,亚历克斯先开始。最初,M = 1

在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M。然后,令 M = max(M, X)

游戏一直持续到所有石子都被拿走。

假设亚历克斯和李都发挥出最佳水平,返回亚历克斯可以得到的最大数量的石头。

同样采用动态规划的思路,正向思路即记忆化搜索,代码复杂但是好理解

反向思路需要逆向遍历数组

这里给出记忆化搜索方法,要特别注意边界值问题。

M的最大值应为数组长度的一半,因为 1+2+4+8<16 ,而且至少要为1,所以加1处理 

class Solution {
public:
    int stoneGameII(vector& piles) {
        int n = piles.size();
        K = n/2+1; // M的最大值
        dp = vector>(n+1,vector(K+1,0));
        sums = vector(n+1,0);
        for(int i=n-1;i>=0;i--){
            sums[i] = sums[i+1] + piles[i];
        }

        return dfs(piles, 0, 1);
    }

    int dfs(vector& piles, int i, int M) {
        if (i > piles.size()-1)
            return 0;
        
        int res = 0;
        int sum = 0;

        for(int k = 0; k<2*M && i+k > dp;
    vector sums;
    int K; //m 的最大值
};

从后向前遍历

class Solution {
public:
    int stoneGameII(vector& piles) {
        int n = piles.size();
        // 从后往前的累加和
        int sum = 0;
        int k = n/2+1;
        vector> d(n,vector(k+1,0));

        for (int i = n-1; i >= 0; --i)
        {
            sum += piles[i];
            for (int j = 1; j <= k; ++j)
            {
                // 超过最大的堆数
                if (i+2*j >= n)
                {
                    d[i][j] = sum;
                }
                else
                {   // 选1个,选2个,,, 选2M个
                    for (int x = 1; x <= 2*j; ++x)
                    {
                        d[i][j] = max(d[i][j], sum - d[i+x][max(j, x)]);
                    }
                }
            }
        }
        return d[0][1];
    }
};

石子游戏 三

Alice 和 Bob 用几堆石子在做游戏。几堆石子排成一行,每堆石子都对应一个得分,由数组 stoneValue 给出。

Alice 和 Bob 轮流取石子,Alice 总是先开始。在每个玩家的回合中,该玩家可以拿走剩下石子中的的前 1、2 或 3 堆石子 。比赛一直持续到所有石头都被拿走。

每个玩家的最终得分为他所拿到的每堆石子的对应得分之和。每个玩家的初始分数都是 0 。比赛的目标是决出最高分,得分最高的选手将会赢得比赛,比赛也可能会出现平局。

假设 Alice 和 Bob 都采取 最优策略 。如果 Alice 赢了就返回 "Alice" Bob 赢了就返回 "Bob",平局(分数相同)返回 "Tie" 。

这题可以作为上一道题的简化版,需要注意的是数字里有负数,

1 初始化时需要将默认值设为一个较大的负数

2 因为有负数,所以最后三个数字不能采用贪心的方法全部取得,同样需要判断取一个取两个和全取的情况

本题还可以在dp数组后面添加三个0, 避免if判断

class Solution {
public:
    string stoneGameIII(vector& stoneValue) {
        int n = stoneValue.size();
        vector dp(n,-1000000);
        int sum = 0;
        // dp[i] = sum[i+k] - dp[i+k] k = 1...3;
        for(int i=n-1; i>=0; i--) {
            sum += stoneValue[i];
            if (n-i <=3) {
                dp[i] = sum;
            }
            for(int k = 1; k <= 3 && i+k sum)
            return "Alice";
        else 
            return "Tie";
     }
};

石子游戏 四

Alice 和 Bob 两个人轮流玩一个游戏,Alice 先手。

一开始,有 n 个石子堆在一起。每个人轮流操作,正在操作的玩家可以从石子堆里拿走 任意 非零 平方数 个石子。

如果石子堆里没有石子了,则无法操作的玩家输掉游戏。

给你正整数 n ,且已知两个人都采取最优策略。如果 Alice 会赢得比赛,那么返回 True ,否则返回 False 。

这里变成先拿光石子的人获胜,并不是在比大小,原理是一样的,对方赢我就输,对方输我就赢。

dp[i] 表示当前还剩 i 个石子时, 拿取的人会不会赢。

dp[i] |=  !dp[i-k]   (k 为完全平方数)  临界情况,dp[0] = false;

 

class Solution {
public:
    bool winnerSquareGame(int n) {
        vector dp(n+1,0);

        for(int i = 1; i<=n; i++) {
            for(int j =1; j*j <=i; j++) {
                dp[i] = (dp[i] || (!dp[i-j*j])); // 对方赢了就是我输了,反之亦然
            }
        }

        return dp[n];
    }
};

石头游戏 五

几块石子 排成一行 ,每块石子都有一个关联值,关联值为整数,由数组 stoneValue 给出。

游戏中的每一轮:Alice 会将这行石子分成两个 非空行(即,左侧行和右侧行);Bob 负责计算每一行的值,即此行中所有石子的值的总和。Bob 会丢弃值最大的行,Alice 的得分为剩下那行的值(每轮累加)。如果两行的值相等,Bob 让 Alice 决定丢弃哪一行。下一轮从剩下的那一行开始。

只 剩下一块石子 时,游戏结束。Alice 的分数最初为 0 。

返回 Alice 能够获得的最大分数 。

  分割数组,取和较小的那部分, 自然想到需要一个 sum[i][j] 数组保存i --- j的数组的元素和

dp[i][j] 表示 当剩余 i---j时, 能够获得的最大数量

使用 变量k作为分割的中点, i<=k

则 dp[i][j]   = max  {      dp[i][k] + sum[i][k]      if   sum[i][k]

                                    dp[k+1][j] + sum[k+1][j]    if   sum[i][k] >sum[k+1][j] 

                                    max(dp[i][k], dp[k+1][j])   +sum[i][k]  if   sum[i][k] >sum[k+1][j] 

                             }

因为较大的那部分丢弃后,较小的一部分立刻累加到结果上,所以需要加sum值

class Solution {
public:
    int stoneGameV(vector& stoneValue) {
        int n = stoneValue.size();
        vector> dp(n,vector(n,0));
        vector> sum(n, vector(n,0));
        for(int i=0; i=0; i--){
            for(int j=i+1; j sum[k+1][j])
                       dp[i][j] = std::max(dp[i][j], dp[k+1][j] + sum[k+1][j]); // 剩下的行立即累加 + dp 值
                    else if (sum[i][k] < sum[k+1][j]) {
                        dp[i][j] = std::max(dp[i][j],dp[i][k] + sum[i][k]);
                    } else {
                        dp[i][j] = std::max(dp[i][k], dp[k+1][j]) + sum[i][k];
                    }
                }
            }
        }

        return dp[0][n-1];
    }
};

注意到我们的 i是逆序处理的,这是因为dp[i][j] 的值需要 dp[i][k] 和 dp[k+1][j],  计算i时 需要 i后面的值,所以需要逆序。举个例子 当计算 dp[0][8] 时 你可能需要 dp[0][4] 和 dp[5][8], 如果全按照正序, 此时dp[0][4]已经计算完成,但是dp[5][8]仍未计算,因此迭代无法进行下去

但可惜在 leetcode 1563 上 由于超时 有三个case没过,

1   可以优化的点是 求解sum数组时,其实不用两层循环,使用 sum[i] 表示 从 0 到 i 的所有元素之和,当需要 i -- j之间的元素之和时 使用 sum[j] -sum[i-1] 代替即可

2 据说把vector换成数组就可以通过了,不过没有兴趣尝试。

另外题解的评论区提出,使用记忆化搜索会减少很多状态的访问,可以减少耗时,这里暂时不知道原因,感觉没有剪枝的记忆化搜索和直接dp迭代应该是相同的。

石子游戏 六

Alice 和 Bob 轮流玩一个游戏,Alice 先手。

一堆石子里总共有 n 个石子,轮到某个玩家时,他可以 移出 一个石子并得到这个石子的价值。Alice 和 Bob 对石子价值有 不一样的的评判标准 。双方都知道对方的评判标准。

给你两个长度为 n 的整数数组 aliceValues 和 bobValues 。aliceValues[i] 和 bobValues[i] 分别表示 Alice 和 Bob 认为第 i 个石子的价值。

所有石子都被取完后,得分较高的人为胜者。如果两个玩家得分相同,那么为平局。两位玩家都会采用 最优策略 进行游戏。

请你推断游戏的结果,用如下的方式表示:

  • 如果 Alice 赢,返回 1 。
  • 如果 Bob 赢,返回 -1 。
  • 如果游戏平局,返回 0 。

这一题改变了石子的权值,变成了甲乙双方有不同的权重,最终比双方的大小,我们来分析一下:

对于任意的 i , 假设第一个人选择这个位置,那么他的收益是自己能获得该位置的权值 a[i] ,以及对手拿不到对应的权值,即减少了对手的得分,因此总的收益是a[i] + b[i]

同样的对于第二个人来说收益也是a[i]+b[i], 这样甲乙就有相同的目标,就是尽量收益最大的位置,这就转变成了一个贪心问题。

但是又出现一个问题是,当几个位置的收益一样,即a[i]+b[i] 相同时,该怎么挑选?这时,甲会尽量选择得分高的,乙也会选择对他来说得分高的,在局部还是一个贪心问题,就是处理的时候有一点麻烦。

class Solution {
public:
    int stoneGameVI(vector& aliceValues, vector& bobValues) {
        n = aliceValues.size();
        a = vector>(n,{0,0,0});
        for(int i=0; i& v1, const vector& v2) {
                if (v1[2] > v2[2]) return true;
                if (v1[2] == v2[2]) return v1[0] > v2[0];
                return false;
            };

        std::sort(a.begin(),a.end(),fun1); //按照甲的偏好进行排序
        int cur = 0;
        for(int i=0; i0) { //选第一个有效的即可
                        res += a[j][0];            
                        cur = j+1;
                        break;
                    }
                }
            } else {// 乙选
                while(cur pick_v) {
                            pick_v = a[j][1];
                            pick_n = j;
                        }
                    } else if (a[j][2] == -1){ // 由于乙在范围内挑选,可能出现空洞
                        continue;
                    } else {
                        break;
                    }
                }
                a[pick_n][2] = -1;
                res -= pick_v;
            }
        }

        if (res >0) return 1;
        if (res <0) return -1;
        return 0;
    }

private:
    vector> a;
    int n;
};

可以通过,但是耗时还比较高。

一部分题解上表示,当收益相同的时候,不需要考虑乙获得的分数,当按照收益从大到小排好之后,甲和乙从前往后挨个挑选即可,即甲总选下标为偶数的值。

但这一部分证明我还不是太明白。

石子游戏 七

石子游戏中,爱丽丝和鲍勃轮流进行自己的回合,爱丽丝先开始 。

有 n 块石子排成一排。每个玩家的回合中,可以从行中 移除 最左边的石头或最右边的石头,并获得与该行中剩余石头值之 和 相等的得分。当没有石头可移除时,得分较高者获胜。

鲍勃发现他总是输掉游戏(可怜的鲍勃,他总是输),所以他决定尽力 减小得分的差值 。爱丽丝的目标是最大限度地 扩大得分的差值 。

给你一个整数数组 stones ,其中 stones[i] 表示 从左边开始 的第 i 个石头的值,如果爱丽丝和鲍勃都 发挥出最佳水平 ,请返回他们 得分的差值 。

这一题与石子游戏一相似,是前后挑选,又与后面的几种相似,因为要求最后的差值。考虑到题目的描述,我们肯定是需要计算 i---j之间的石头的和,这里可以使用一个前缀和数组来计算,i---j 之间stones的和为 sum[j] - sum[i-1], 节省一维的空间和时间。

建立dp数组,令dp[i][j] 表示当剩下 i----j 时,挑选的人可获得的分数 和另一个获得的分数的最大差值,(当然也可能比不过第二个人,这时候代表的最小的差异,计算方法都是不变的)

当需要计算 dp[i][j] 时,有两个选择

1 选左边的,本次获得分数 sum[i+1][j] , 对手将领先 dp[i+1][j] 分,

2 选右边的,本次获得分数 sum[i][j-1], 对手将领先dp[i][j-1] 分,

dp[i][j] 表示当前最佳选择后与对手的差值,因此无论是1还是2,使用的应该都是 sum - dp的值

dp[i][j] =std::max(sum[i+1][j] - dp[i+1][j],

                            sum[i][j-1] - dp[i][j-1);

这里为了计算方便我们分别把sum数组和dp数组的范围扩展到 1----n;

    int stoneGameVII(vector& stones) {
        int n = stones.size();
        vector> dp(n+1,vector(n+1,0));
        vector sum(n+1,0);
        for(int i=1;i<=n;i++)
            sum[i] = sum[i-1] + stones[i-1];

        for(int i=n; i>0; i--) { // i倒序是因为计算i时 需要dp[i+1][j],同样的道理j必须正序
            for(int j=i+1; j<=n; j++){
                //pick left sum(i+1,j)
                int left = sum[j] - sum[i] - dp[i+1][j];
                
                // pick right sum(i,j-1);
                int right = sum[j-1] -sum[i-1] - dp[i][j-1];

                dp[i][j] = std::max(left, right);
            }
        }

        return dp[1][n];
    }

你可能感兴趣的:(动态规划,算法)