暴力递归转动态规划(二)

news2025/1/11 20:52:29

上一篇已经简单的介绍了暴力递归如何转动态规划,如果在暴力递归的过程中发现子过程中有重复解的情况,则证明这个暴力递归可以转化成动态规划。
这篇帖子会继续暴力递归转化动态规划的练习,这道题有点难度。

题目
给定一个整型数组arr[],代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌。规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左边或者最右边的牌,玩家A和玩家B都绝顶聪明,请返回最后获胜者的分数。

暴力递归
依然是先从暴力递归开始写起,一个先手拿,一个后手拿,两个人都绝顶聪明,都知道怎么拿可以利益最大化。
先手的拿完第一个之后,再拿的时候,就要从后手拿完的数组里再挑选了。
同理,如果后手的等先手的拿了之后,是不是就可以从剩余的数组里挑选最大利益的拿了。
依然先确定base case:
如果先手拿,最理想的状态就是当数组剩下最后一个数,依然可以被我拿走。
如果后手拿,最悲催的连数组最后一个数我都拿不到。
代码中f()函数是代表在数组L~ R范围上返回上先手拿能拿到的最大值返回。
g()函数代表在数组L ~ R范围上后手拿,能够获取的最大值。
需要注意的是身份的转变,如果先手拿之后,再拿的时候就会变成后手,第二个后手拿的时候,虽然我是后手,但是也是从数组中挑选利益最大的拿,留给先手拿的人的也是不好的,所以我会变成先手。

//先手方法
public static int f(int[] arr,int Lint R){
	//base case:先手拿,并且数组中剩一个元素,我拿走
	if(L == R){
		return arr[L];
	}
	//因为可以选择从左边拿和右边拿,从左边拿下一次就是L + 1开始,右边拿就是 R - 1 开始。
	//需要注意的是我从左或者从右拿完之后,再拿就是拿别人拿剩下的了,要以后手姿态获取其余分数,所以要调用g()方法
	int p1 = arr[L] + g(arr,L + 1,R);
	int p2 = arr[R] + g(arr, L, R -1);
	
	//两种决策中取最大值
	return Math.max(p1,p2);
}
//后手方法
public static int g(int[] arr,int L,int R){
	//剩最后一个也不是我的,毛都拿不到,return 0
	if(L == R){
		return 0;
	}
	//后手方法是在先手方法后,挑选最大值,那如果先手方法选择了L,则我要从L + 1位置选,
	//如果先手选择了R,那我要从R - 1位置开始往下选。
	//是从对手选择后再次选择最大值
	int p1 = f(arr,L + 1,R);
	int p2 = f(arr,L,R - 1);
	//因为是后手,是在先手后做决定,是被迫的,所以取Min。
	return Math.min(p1,p2);
}

先手后手方法已经确定,来看主流程怎么调用

public static int win1(int[] arr){
	//如果是无效数组,则返回一个无效数字 -1 
	if(arr == null || arr.length == 0){
		return -1;
	}
	int first = f(arr, 0 ,arr.length - 1);
	int second = g(arr,0,arr.length - 1);
	
	return Math.max(first,second);
}

暴力递归的分析和代码已经搞定,接下来我们通过分析暴力递归的调用过程来实现第一步的优化,找它的依赖,找它的重复解。
举一个具体的例子,arr[]范围 0~ 7,根据上面暴力递归的代码逻辑,我们来看看它的依赖关系和调用过程。如果确定了可变参数以及依赖关系,是不是就可以尝试着优化成动态规划。
在这里插入图片描述
根据代码逻辑,要么是取左边L + 1,要么是取右边 R - 1,所以可以确定可变参数是L和R,并且整个流程下来会发现有重复解的情况。
不过有些不同的是,这个是双层递归循环依赖调用,所以如果根据可变参数参数L,R来构建缓存表的话,则需要2个不同的缓存表分别记录。

优化
前面已经分析出整个暴力递归的调用过程,并发现了重复解,其中可变参数是L、R,根据L、R构建缓存表,因为是f()和g()的循环依赖调用,所以需要准备两张缓存表。

public static int win2(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;
        }
        int N = arr.length;
        int[][] fmap = new int[N][N];
        int[][] gmap = new int[N][N];

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                fmap[i][j] = -1;
                gmap[i][j] = -1;
            }
        }
        int first = f1(arr, 0, arr.length - 1, fmap, gmap);
        int second = g1(arr, 0, arr.length - 1, fmap, gmap);
        return Math.max(first, second);
    }

    public static int f1(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
    	// != -1,说明之前计算过该值,直接返回即可
        if (fmap[L][R] != -1) {
            return fmap[L][R];
        }
        int ans = 0;
        if (L == R){
            ans = arr[L];
        }else{
            int p1 = arr[L] + g1(arr, L + 1, R, fmap, gmap);
            int p2 = arr[R] + g1(arr, L, R - 1, fmap, gmap);

            ans = Math.max(p1, p2);
        }
        //这一步能够取得的最大值
        fmap[L][R] = ans;
        return ans;
    }


    public static int g1(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
        if (gmap[L][R] != -1){
            return gmap[L][R];
        }
        //因为如果 L == R,后手方法会返回0,默认ans也是等于0,省略一步判断
        int ans = 0;
        if (L != R){
            int p1 = f1(arr,L + 1,R,fmap,gmap);
            int p2 = f1(arr,L,R - 1,fmap,gmap);
            ans = Math.min(p1,p2);
        }

        gmap[L][R] = ans;
        return ans;
    }

二次优化
我们上面已经创建了缓存表,并找到了变量L、R,我们现在不妨举一个例子,并将缓存表画出来,来看一下表中每一列的对应关系,如果我们能找到这个缓存表的对应关系,是不是将表构建出来以后,就可以直接获取获胜者的最大值。
在这里插入图片描述
数组arr = {7,4,16,15,1} 因为有两张缓存表,所以需要将两张表的依赖关系都找出。接下来,回到最开始的暴力递归方法,根据代码逻辑一步一步找出依赖关系。

public static int win1(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;
        }
        int first = f(arr, 0, arr.length - 1);
        int second = g(arr, 0, arr.length - 1);
        return Math.max(first, second);
    }

    public static int f(int[] arr, int L, int R) {
        if (L == R) {
            return arr[L];
        }
        int p1 = arr[L] + g(arr, L + 1, R);
        int p2 = arr[R] + g(arr, L, R - 1);
        return Math.max(p1, p2);
    }

    public static int g(int[] arr, int L, int R) {
        if (L == R) {
            return 0;
        }
        int p1 = f(arr, L + 1, R);
        int p2 = f(arr, L, R - 1);
        return Math.min(p1, p2);
    }

从先手方法f()和后手方法g()的base case可以看出,如果当L == R时,f()方法中此时就是等于数组arr[L]本身的值,而g()中为0,又因为,每次我只选L或只选R,当L = R时就return了,所以我的L始终不会 > R。我们所要求的L ~ R 范围是整个数组0 ~ 4的值,此时图可以填充成这样。
在这里插入图片描述
再来接着往下看,如果此时LR随便给一个值,比如说当前fmap中L = 1,R = 3,来接着看它的依赖过程。
在这里插入图片描述
根据代码可以看出,它依赖的是g()方法中L +1和R - 1,所以对应在gmap中的依赖就是圆圈标记的部分。对应的,同样 L = 1 R = 3在gmap中也是依赖fmap对应的位置。
在这里插入图片描述
那现在有缓存表中每个位置的依赖关系,还有fmap和gmap当L == R时的值,是不是就可以推算出其他格子中的值。

代码

 public static int win3(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;
        }
        int N = arr.length;
        int[][] fmap = new int[N][N];
        int[][] gmap = new int[N][N];
		//根据base  case填充fmap,gmap都是0,数组初始化值也是0,不用填充
        for (int i = 0; i < N; i++) {
            fmap[i][i] = arr[i];
        }
		//根据对角线填充,从第一列开始
        for (int startCol = 1; startCol < N; startCol++) {
            int L = 0;
            int R = startCol;
            while (R < N) {
            	//将调用的g()和f()都替换成对应的缓存表
                fmap[L][R] = Math.max(arr[L] + gmap[L + 1][R], arr[R] + gmap[L][R - 1]);
                gmap[L][R] = Math.min(fmap[L + 1][R], fmap[L][R - 1]);
                L++;
                R++;
            }
        }
        //最后从L ~ R位置,取最大值
        return Math.max(fmap[0][N -1],gmap[0][N-1]);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/945504.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#2010 .NET4 解析 json 字符串

下载Newtonsoft.Json.dll using System; using System.Collections.Generic; using System.Linq; using System.Text;using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; namespace ConsoleApplication1 {class Program{static void Main(string[] args){strin…

C++编辑修改PDF

PDFWriter是一个易于使用的C创建、修改PDF文档的库 1.创建一个PDF文件 #include #include “PDFWriter.h” int main() { std::cout << “Hello World!\n”; PDFWriter pdfWriter; int retpdfWriter.StartPDF(“D:\mytestwriterpdf.pdf”, ePDFVersion13); if (ret eS…

DC/DC开关电源学习笔记(二)开关电源的分类

&#xff08;二&#xff09;开关电源的分类 1.DC/DC类开关电源2.AC/DC变换器3.电路结构分类4.功率开关管分类5.电路拓扑分类 开关电源可分为 AC/DC和DC/DC两大类。 作为二次电源的DC/DC变换器现已实现模块化&#xff0c; 且设计技术及生产工艺在国内外均已成熟和标准化&#xf…

【USRP】调制解调系列6:16APSK、32APSK 、基于labview的实现

APSK APSK是&#xff0c;与传统方型星座QAM&#xff08;如16QAM、64QAM&#xff09;相比&#xff0c;其分布呈中心向外沿半径发散&#xff0c;所以又名星型QAM。与QAM相比&#xff0c;APSK便于实现变速率调制&#xff0c;因而很适合目前根据信道及业务需要分级传输的情况。当然…

基于白冠鸡算法优化的BP神经网络(预测应用) - 附代码

基于白冠鸡算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于白冠鸡算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.白冠鸡优化BP神经网络2.1 BP神经网络参数设置2.2 白冠鸡算法应用 4.测试结果&#xff1a;5.Matlab代…

【ES6】Getter和Setter

JavaScript中的getter和setter方法可以用于访问和修改对象的属性。这些方法可以通过使用对象字面量或Object.defineProperty()方法来定义。 以下是使用getter和setter方法的示例&#xff1a; <!DOCTYPE html> <script>const cart {_wheels: 4,get wheels(){retu…

1239. 串联字符串的最大长度;2826. 将三个组排序;2563. 统计公平数对的数目

1239. 串联字符串的最大长度 核心思想&#xff1a;递归&#xff0c;选或者不选&#xff0c;定义dfs(i&#xff0c;pre)表示从i-n的满足要求的arr中选择字符串串联所能获得的最大长度为dfs(i,pre)&#xff0c;pre表示已经选过的字符串所组成的集合。然后就有两种情况选&#xf…

2023年天津市大学软件学院专升本报名考试人数

2023年天津市大学软件学院专升本科五校联合招生考试报名人数、参加考试人数、录取人数和录取率多少&#xff0c;请看下图详细说明

腾讯云学生服务器申请、学生认证入口及学生机价格表

腾讯云学生服务器申请、学生认证入口及学生机价格表&#xff0c;学生机申请流程&#xff0c;腾讯云学生服务器优惠活动&#xff1a;轻量应用服务器2核2G学生价30元3个月、58元6个月、112元一年&#xff0c;轻量应用服务器4核8G配置191.1元3个月、352.8元6个月、646.8元一年&…

Linux知识点 -- 网络基础(一)

Linux知识点 – 网络基础&#xff08;一&#xff09; 文章目录 Linux知识点 -- 网络基础&#xff08;一&#xff09;一、网络发展二、协议1.OSI七层模型2.TCP/IP五层&#xff08;或四层&#xff09;模型 三、网络传输基本流程1.局域网中的两台主机通信流程2.跨网段的两台主机间…

JOJO的奇妙冒险

JOJO,我不想再做人了。 推荐一部动漫 JOJO的奇妙冒险 荒木飞吕彦创作的漫画 《JOJO的奇妙冒险》是由日本漫画家荒木飞吕彦所著漫画。漫画于1987年至2004年在集英社的少年漫画杂志少年JUMP上连载&#xff08;1987年12号刊-2004年47号刊&#xff09;&#xff0c;2005年后在集英…

盖革计数管——核辐射测量仪核心部件

一&#xff1a;外形尺寸(最大值) 1、直径6.2mm 2、长度56mm 二&#xff1a;参数 1、起始计数电压:≦350V 2、坪区范围&#xff1a;380—480V 3、坪区范围斜率&#xff1a;≦15&#xff05;/100V 4、工作温度范围:-40℃—55℃ 5、放电电压:550V 6、死时间:20us 7、推荐工作电压4…

信息系统项目管理师(第四版)教材精读思维导图-第六章项目管理理论

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 目录 6.1 PMBOK的发展 6.2 项目基本要素 6.3…

51社区滑块验证码

url aHR0cHM6Ly9wYXNzcG9ydC41MS5jb20vP2dvdXJsPWh0dHBzOi8vd2FuLjUxLmNvbS92dWUvaW5kZXg 接口分析 圈起来的两个接口&#xff0c;一个拿滑块&#xff0c;一个验证。 参数分析 没用特别难的加密 验证识别 需要注意的是&#xff0c;我们先拿到的图片是混乱的&#xff0c;需…

人员闯入检测告警算法

人员闯入检测告警算法通过yolov5网络模型识别检测算法&#xff0c;人员闯入检测告警算法对未经许可或非法进入的人员进行及时识别告警&#xff0c;确保对危险区域的安全管理和保护。YOLO系列算法是一类典型的one-stage目标检测算法&#xff0c;其利用anchor box将分类与目标定位…

图解Spring三级缓存的工作流程

一级缓存&#xff1a;保存了完整的Bean实例&#xff0c;可以直接使用 二级缓存&#xff1a;保存了实例化以后还没有设置属性值的Bean实例&#xff0c;也就是一个空的对象&#xff0c;没有做依赖注入 三级缓存 &#xff1a;存放Bean工厂&#xff0c;它用来生成原始Bean对象并且…

「Redis」1. 数据类型的底层实现

前言&#xff1a;在这篇博文中&#xff0c;我们将简单总结在面试中怎么回答Redis数据类型的底层实现。 因为面试时间就那么点&#xff0c;言简意赅的描述自己会的知识显得尤为重要‼️ 文章目录 0.1. String 的底层实现原理0.2. 列表的底层实现原理0.3. 字典的底层实现原理0.4.…

【Interaction交互模块】AngularJointDrive角度关节驱动

文章目录 一、预设体位置二、案例&#xff1a;做一个“能开合的门” 1、在已建好的门框下&#xff0c;建门 2、设置参数 3、解决产生的问题 一、预设体位置 交互模块——可控制物体——物理关节——角度关节驱动 二、案例&#xff1a;做一个“能开合的门” 1…

深入了解fcntl函数:Linux系统编程中的文件控制

文章目录 概述介绍函数原型与参数 拓展&#xff1a;fcntl改文件属性总结 概述 摘要: fcntl函数是Linux系统编程中一个重要的函数&#xff0c;用于对文件描述符进行各种控制操作。本文将详细介绍fcntl函数的原型、各个参数的用法&#xff0c;以及阻塞和非阻塞模式切换的方法&am…