纸牌博弈问题

纸牌,博弈,问题 · 浏览次数 : 300

小编点评

```java public class Cards { public static int cardGame(int[] A, int n) { if (n == 0) { return 0; } if (n == 1) { return A[0]; } if (n == 2) { return Math.max(A[0], A[1]); } // 创建两个二维数组,存储先手和后手的递归结果 int[][] firstMap = new int[n][n]; int[][] secondMap = new int[n][n]; // 初始化对角线的值(先手递归结果) for (int i = 0; i < n; i++) { firstMap[i][i] = A[i]; } // 初始化对角线下班区域的值(后手递归结果) // 因为start和end都是有范围的,所以左下半区是无效区域,无需考虑 for (int i = 1; i < n; i++) { int r = 0; int c = i; while (c < n) { firstMap[r][c] = Math.max(A[r] + secondMap[r + 1][c], A[c] + secondMap[r][c - 1]); secondMap[r][c] = Math.min(firstMap[r + 1][c], firstMap[r][c - 1]); r++; c++; } } // 返回最终的得分,即max(firstMap[0][n - 1], secondMap[0][n - 1]) return Math.max(firstMap[0][n - 1], secondMap[0][n - 1]); } } ``` **排版示例:** ``` A[0] A[1] A[2] A[3] A[4] A[5] A[6] A[7] A[8] ```

正文

纸牌博弈问题

作者:Grey

原文地址:

博客园:纸牌博弈问题

CSDN:纸牌博弈问题

题目描述

有一个整型数组 A,代表数值不同的纸牌排成一条线。玩家 a 和玩家 b 依次拿走每张纸牌,
规定玩家 a 先拿,玩家 b 后拿,
但是每个玩家每次只能拿走最左或最右的纸牌,
玩家 a 和玩家 b 都绝顶聪明,他们总会采用最优策略。
请返回最后获胜者的分数。

注:给定纸牌序列 A 及序列的大小 n,请返回最后分数较高者得分数(相同则返回任意一个分数)。保证 A 中的元素均小于等于1000。且 A 的大小小于等于300。

题目链接:牛客-纸牌博弈

暴力递归解

定义两个递归函数,第一个递归函数是

int first(int[] A, int n, int start, int end)

这个递归函数表示的含义是:先手在数组 A 的 start 到 end 范围内,经过 n 轮,能获得的最大的分数是多少。

base case 是,当 start == end 的时候,即数组 A 只有一个元素,此时先手直接拿走这个元素,最大分数就是此时先手拿走的唯一元素值,即

        if (start == end) {
            return A[start];
        }

第二个递归函数是

int second(int[] A, int n, int start, int end)

这个递归函数表示的含义是:后手在数组 A 的 start 到 end 范围内,经过 n 轮,能获得的最大的分数是多少。

base case 是,当 start == end 的时候,即数组 A 只有一个元素,此时这个元素肯定要被先手拿走,那么后手只能返回 0,即

        if (start == end) {
            return 0;
        }

接下来是普遍情况,对于先手函数来说,有两个位置可以选,一个是 start 位置,另外一个是 end 位置,如果选了 start 位置,那么先手在下一轮就是后手,即

A[start] + second(A, n, start + 1, end)

同理,如果选 end 位置,先手在下一轮是后手,即

A[end] + second(A, n, start, end - 1)

先手函数在做上述两个决策的过程中,一定要取最大值,即

Math.max(A[start] + second(A, n, start + 1, end), A[end] + second(A, n, start, end - 1));

所以,先手函数的完整代码如下

    public static int first(int[] A, int n, int start, int end) {
        if (start == end) {
            return A[start];
        }
        return Math.max(A[start] + second(A, n, start + 1, end), A[end] + second(A, n, start, end - 1));
    }

接下来是后手函数的普遍情况,对于后手函数来说,没有先选的权力,只能让先手选完自己才能选,先手如果选了 start 位置,后手面对的选择是

first(A, n, start + 1, end)

先手如果选了 end 位置,后手面对的选择就是

first(A, n, start, end - 1)

后手在下一轮就是先手,所以要保证先手的上述选择最小,即

Math.min(first(A, n, start + 1, end), first(A, n, start, end - 1));

定义了先手函数和后手函数,主函数做如下调用即可

    public static int cardGame(int[] A, int n) {
        // 没有元素,直接返回0分
        if (n == 0) {
            return 0;
        }
        // 只有一个元素,无论如何,只能得到 A[0] 分
        if (n == 1) {
            return A[0];
        }
        // 只有两个元素,选最大的那个
        if (n == 2) {
            return Math.max(A[0], A[1]);
        }
        // 普遍情况:先手后手中最大的那个
        return Math.max(first(A, n, 0, A.length - 1), second(A, n, 0, A.length - 1));
    }

超时

image

动态规划解

根据上述暴力递归函数可知,两个递归函数都可变参数都是 2 个,且可变参数的变化范围是固定的,所以我们可以用两个二维数组来分别表示两个递归函数的结果,

// 保存先手函数的递归过程解
int[][] firstMap = new int[n][n];
// 保存后手函数的递归过程解
int[][] secondMap = new int[n][n];

由于递归过程的两个可变参数 start 和 end 是有范围的,且 start 不可能大于 end,所以,上述两个二维数组的左下半区都是无效区域,无需考虑。

在暴力递归过程中,当 start == end 的时候,返回 A[start] 值,所以,针对 firstMap,其对角线(start == end)的值都是确定的

        // 对角线
        for (int i = 0; i < n; i++) {
            firstMap[i][i] = A[i];
        }

经过上述过程,可以得到两个二维数组的如下区域的内容

image

接下来就是普遍情况,

基于暴力递归过程可以很方便得到两个二维数组之间的递推关系

        // 对角线下班区域不用管
        // 对角线上半区域
        for (int i = 1; i < n; i++) {
            int r = 0;
            int c = i;
            while (c < n) {
                firstMap[r][c] = Math.max(A[r] + secondMap[r + 1][c], A[c] + secondMap[r][c - 1]);
                secondMap[r][c] = Math.min(firstMap[r + 1][c], firstMap[r][c - 1]);
                r++;
                c++;
            }
        }

完整代码如下

import java.util.*;

public class Cards {

    public static int cardGame(int[] A, int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return A[0];
        }
        if (n == 2) {
            return Math.max(A[0], A[1]);
        }
        int[][] firstMap = new int[n][n];
        int[][] secondMap = new int[n][n];
        // 对角线
        for (int i = 0; i < n; i++) {
            firstMap[i][i] = A[i];
        }
        // 对角线下班区域不用管
        // 对角线上半区域
        for (int i = 1; i < n; i++) {
            int r = 0;
            int c = i;
            while (c < n) {
                firstMap[r][c] = Math.max(A[r] + secondMap[r + 1][c], A[c] + secondMap[r][c - 1]);
                secondMap[r][c] = Math.min(firstMap[r + 1][c], firstMap[r][c - 1]);
                r++;
                c++;
            }
        }
        return Math.max(firstMap[0][n - 1], secondMap[0][n - 1]);
    }
}

更多

算法和数据结构笔记

与纸牌博弈问题相似的内容:

纸牌博弈问题

纸牌博弈问题 作者:Grey 原文地址: 博客园:纸牌博弈问题 CSDN:纸牌博弈问题 题目描述 有一个整型数组 A,代表数值不同的纸牌排成一条线。玩家 a 和玩家 b 依次拿走每张纸牌, 规定玩家 a 先拿,玩家 b 后拿, 但是每个玩家每次只能拿走最左或最右的纸牌, 玩家 a 和玩家 b 都绝顶

我为什么选择Wiki.js记笔记?

很长一段时间里,我都被困扰着,感觉陷入了笔记的泥潭,而积累的如此多的笔记也没有形成我自己的知识体系。 之前的记笔记方式 笔记的来源 微信公众号 技术博客 纸质书籍 官网文档 PDF 自己的零散想法 网页 之前的笔记软件 有好几个: 为知笔记 浏览器书签 MarkDown 文档 Calibre 电子书

纸条折痕问题

纸条折痕问题 作者:Grey 原文地址: 博客园:纸条折痕问题 CSDN:纸条折痕问题 题目描述 请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到

使用 Python 旋转PDF页面、或调整PDF页面顺序

在将纸质文档扫描成PDF电子文档时,有时可能会出现页面方向翻转或者页面顺序混乱的情况。为了确保更好地浏览和查看PDF文件,本文将分享一个使用Python来旋转PDF页面或者调整PDF页面顺序的解决方案。 要实现Python对PDF页面进行设置,我们需要用到第三方库 Spire.PDF for Pyt

ebpf 单行程序学习

# ebpf 单行程序学习 ## 背景 ``` 公司方神借给我一本: 《BPF之巅:洞悉linux系统和应用性能》纸质书 拿回家晚上在沙发上看了几天。 感觉书很厚看的不是很系统。 仅能凭自己的感觉总结一下这些天的读书感悟。 本来计划是2023年的春节 7 天长假系统的学习ebpf 但是因为学习Lin

500行代码手写docker开篇-goland远程编译环境配置

(1)500行代码手写docker开篇-goland远程编译环境配置 本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。 代码最终运行效果 本系列源码已经上

500行代码手写docker-以新命名空间运行程序

(2)500行代码手写docker-以新命名空间运行程序 本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。 本章的源码已经上传到github,地址如下:

500代码行代码手写docker-设置网络命名空间

# (4)500代码行代码手写docker-设置网络命名空间 > 本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。 本章的源码已经上传到github,地址

500行代码手写docker-实现硬件资源限制cgroups

# (5)500行代码手写docker-实现硬件资源限制cgroups > 本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。 本章的源码已经上传到gith

开源!开源一个flutter实现的古诗拼图游戏

去年(2023年)年底我初学flutter,看了一些文档和教程,想找个东西*练练手。 小时候看过一个关于历史名人儿时事迹的短片,有一集是讲*总理的,有一个细节我记得很清楚:幼年***经常要做一个游戏--有一堆纸片,每片纸上一个字,他要一个一个字拼起*拼成一首诗。 很多年前我就想,或许可以把这个游戏做