算法课设 戳气球问题实验报告 动态规划

news2025/2/12 0:58:35

戳气球实验报告

目录

一、题目

二、分析原问题并做调整

三、分析子问题及其递推关系

四、确定dp数组的计算顺序

五、复杂度分析

六、具体实现代码

七、填表示例寻找最优解和最优方案

八、总结

九、致谢

一、题目

有n个气球,编号为0到n-1,每个气球都有一个分数,存在nums数组中。每次戳气球i可以得到的分数为 nums[left] * nums[i] * nums[right],left和right分别表示i气球相邻的两个气球。当i气球被戳爆后,其左右两气球即为相邻。要求戳爆所有气球,得到最多的分数。
备注:
1.你可以假设nums[-1] = nums[n] = 1。-1和n位置上的气球不真实存在,因此不能戳爆它们。
2.0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

二、分析原问题并做调整

运用动态规划算法的一个重要条件:子问题必须独立。对于此问题,必须巧妙地定义 dp 数组的含义,避免子问题产生相关性,才能推出合理的状态转移方程。要想定义 dp 数组,这里需要对问题进行一个简单地转化。题目说可以认为 nums[-1] = nums[n] = 1,那么我们先直接把这两个边界加进去,形成一个新的数组 points:

int maxCoins(vector<int>& nums) {
    int n = nums.size();
    Vector<int> points(n+2);// 两端加入两个虚拟气球
    points[0] = points[n + 1] = 1;//题目给的条件0和n+1气球吹破为1
    for (int i = 1; i <= n; i++) {
        points[i] = nums[i - 1];改变问题后points[i]就应该等于原问题的nums[i-1],此处i从1开始到n结束,因为我们对points已经考虑过了0和n+1的 情况了,而nums是从0~n-1 
    }
    // ...
}

现在气球的索引变成了从 1 到 n,points[0] 和 points[n+1] 可以认为是两个「虚拟气球」。因此我们可以改变问题:在一排气球 points 中,请你戳破气球 0 和气球 n+1 之间的所有气球(不包括 0 和 n+1),使得最终只剩下气球 0 和气球 n+1 两个气球,最多能够得到多少分?

三、分析子问题及递推关系

我们用dp[i][j] = x 表示,戳破气球 i 和气球 j 之间(开区间,不包括 i 和 j)的所有气球,可以获得的最高分数为 x。根据这个定义,题目要求的结果就是 dp[0][n+1] 的值,而 base case 就是 dp[i][j] = 0,其中 0 <= i <= n+1, j <= i+1,因为这种情况下,开区间 (i, j) 中间根本没有气球可以戳。
// base case 已经都被初始化为 0
vector<vector > dp(n + 2, vector(n + 2));
//构造动态二维dp[][]数组,转化后的问题是有n+2个气球(2个虚拟)
现在我们要根据这个 dp 数组来推导状态转移方程了,实际上就是在思考怎么「做选择」,也就是这道题目最有技巧的部分:想求戳破气球 i 和气球 j 之间的最高分数,如果「正向思考」,就不是动态规划了。我们需要「反向思考」,想一想气球 i 和气球 j 之间最后一个被戳破的气球可能是哪一个?其实气球 i 和气球 j 之间的所有气球都可能是最后被戳破的那一个,不防假设为 k。回顾动态规划的步骤,这里其实已经找到了状态和选择:i 和 j 就是两个状态,最后戳破的那个气球 k 就是选择。
根据刚才对 dp 数组的定义,如果最后一个戳破气球 k,dp[i][j] 的值应该为:
dp[i][j] = dp[i][k] + dp[k][j]
+ points[i]*points[k]*points[j]
在这里插入图片描述

结合这个图,就能体会出 dp 数组定义的巧妙了。由于是开区间,dp[i][k] 和 dp[k][j] 不会影响气球 k;而戳破气球 k 时,旁边相邻的就是气球 i 和气球 j 了,最后还会剩下气球 i 和气球 j,这也恰好满足了 dp 数组开区间的定义。要想最后戳破k气球,那么开区间(i,k)和(k,j)内的气球也得先戳破。最后只剩i,j,k三个气球,则分数为 points[i]*points[k]*points[j]。戳破开区间 (i, k) 和开区间 (k, j) 的气球最多能得到的分数就是 dp[i][k] 和 dp[k][j],这恰好就是我们对 dp 数组的定义。
那么,对于一组给定的 i 和 j,我们只要穷举 i < k < j 的所有气球 k,选择得分最高的作为 dp[i][j] 的值即可,这也就是状态转移方程:
// 最后戳破的气球是哪个?

for (int k = i + 1; k < j; k++) {     //穷举戳破i到j之间的气球 ,i<k<j,故k从i+1到j-1 
                    int sum = points[i] * points[k] * points[j];
                    sum += dp[i][k] + dp[k][j];
                    dp[i][j] = max(dp[i][j], sum);
                }

递归方程为:
dp[i][j]=
max{ dp[i][k] + dp[k][j]+points[i]*points[k]*points[j] },i<j-1
0,i>=j-1

四、确定dp数组的计算顺序

除了获得了状态转移方程外,对于 k 的穷举仅仅是在做选择,但是应该如何穷举状态i 和 j ?

for (int i = ...; ; )
    for (int j = ...; ; )
        for (int k = i + 1; k < j; k++) {     //穷举戳破i到j之间的气球 ,i<k<j,故k从i+1到j-1 
                    int sum = points[i] * points[k] * points[j];
                    sum += dp[i][k] + dp[k][j];
                    dp[i][j] = max(dp[i][j], sum);
}

return dp[0][n+1];
关于「状态」的穷举,最重要的一点就是:状态转移所依赖的状态必须被提前计算出来。这道题举例,dp[i][j] 所依赖的状态是 dp[i][k] 和 dp[k][j],那么我们必须保证:在计算 dp[i][j] 时,dp[i][k] 和 dp[k][j] 已经被计算出来了(其中 i < k < j)。那么应该如何安排 i 和 j 的遍历顺序,来提供上述的保证呢?有一个技巧:根据 base case 和最终状态进行推导。
备注:最终状态就是指题目要求的结果,对于这道题目也就是 dp[0][n+1]。
我们先把 base case 和最终的状态在 DP table 上画出来:
在这里插入图片描述

对于任一 dp[i][j],我们希望所有 dp[i][k] 和 dp[k][j] 已经被计算,画在图上就是这种情况:

在这里插入图片描述

为了达到这个要求,我们小组打算自底向上,从左到右依次遍历:
在这里插入图片描述

那么经过递归方程分析,我们要满足i,j之间有气球才可以戳破,那么至少j>=i+2才能满足中间有气球。则要有气球可戳,则i最大也就是n+1-2=n-1。j可能是i+1

for (int i = n - 1; i >= 0; i--) {                 
            for (int j = i + 2; j <= n + 1; j++) {     
                for (int k = i + 1; k < j; k++) {     //穷举i到j之间的气球 ,i<k<j,故k从i+1到j-1 
                    int sum = points[i] * points[k] * points[j];
                    sum += dp[i][k] + dp[k][j];
                    dp[i][j] = max(dp[i][j], sum);
                }
            }
        }
        return dp[0][n + 1];//返回最大值,戳破0~n+1之间的气球 

五、复杂度分析

时间复杂度:O(n3),其中 O(n) 是气球数量。状态数为 n2,状态转移复杂度为 O(n),最终复杂度为 O(n2×n)=O(n3);
空间复杂度:O(n2),其中 n 是气球数量。

六、具体实现代码

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
#define N 100
int maxCoins(vector<int>& nums) {  //参数是用vector做的动态数组nums,也是初始问题 
    int n = nums.size();     //数组长度,其实也就是气球个数n(0~n-1一共是n个) 
    vector<vector<int> > dp(n + 2, vector<int>(n + 2));     //构造动态二维dp[][]数组 
    vector<vector<int> > s(n + 2, vector<int>(n + 2));
    vector<int> points(n + 2);    //添加虚拟气球后改变问题的一维points[]数组 ,虚拟的2个气球是辅助功能,我们只需要戳两端中间的气球,即是开区间 
    points[0] = points[n + 1] = 1;  //题目给的条件0和n+1气球吹破为1
    for (int i = 1; i <= n; i++) {
        points[i] = nums[i - 1];    //改变问题后points[i]就应该等于原问题的nums[i-1],此处i从1开始到n结束,因为我们对points已经考虑过了0和n+1的 情况了,而nums是从0~n-1 
    }
    for (int i = n - 1; i >= 0; i--) {                  //i从下往上,i从倒数第二个开始,遍历到0。 
        for (int j = i + 2; j <= n + 1; j++) {//j从左往右,遍历到n+1 ,分析关系的j=i+2 
            s[i][j] = i;
            for (int k = i + 1; k < j; k++) {     //穷举i到j之间的气球 ,i<k<j,故k从i+1到j-1 
                int sum = points[i] * points[k] * points[j];
                sum += dp[i][k] + dp[k][j];
                //dp[i][j] = max(dp[i][j], sum);
                if (sum > dp[i][j]) {
                    dp[i][j] = sum;
                    s[i][j] = k;
                }
            }
        }
    }
    cout << "k表:" << endl;
    for (int i = 0; i <= n + 1; i++) {
        for (int j = 0; j <= n + 1; j++) {
            cout.width(7);
            cout << s[i][j];
        }
        cout << endl;
    }
    cout << "分数表为:" << endl;
    for (int i = 0; i <= n + 1; i++) {
        for (int j = 0; j <= n + 1; j++) {
            cout.width(7);
            cout << dp[i][j];
        }
        cout << endl;
    }
    return dp[0][n + 1];//返回最大值,戳破0~n+1之间的气球 

}
int main() {
    int n;
    cout << "请输入气球个数:" << endl;
    cin >> n;
    cout << "请输入各气球分数:" << endl;
    vector<int> nums(n);
    for (int i = 0; i < n; i++) {
        cin >> nums[i];
    }
    cout << "获得的最大分数为:" << maxCoins(nums);
    return 0;
}

在这里插入图片描述

七、填表示例寻找最优解和最优方案

通过递归方程dp[i][j]=
max{ dp[i][k] + dp[k][j]+points[i]*points[k]*points[j] },i<j-1
0,i>=j-1
我们可以把dp[][]表填好
dp[][]表如下:
i\j 0 1 2 3 4 5
0 0 0 30 200 510 520(优)
1 0 0 150 450 510
2 0 0 90 108
3 0 0 30
4 0 0
5 0

K(rec) 0 1 2 3 4 5
0 0 0 1 1 1 1
1 0 0 2 3 4
2 0 0 3 4
3 0 0 4
4 0 0
5 0

自底向上,从左到右填表过程:
dp[3][5]=dp[3][4]+dp[4][5]+P3P4P5=0+0+5*6=30(k=4),

dp[2][4]=dp[2][3]+dp[3][4]+P2P3P4=0+0+356=90(k=3),
dp[2][5]=max{
dp[2][3]+dp[3][5]+P2P3P5=0+30+35=45,
dp[2][4]+dp[4][5]+P2
P4P5=90+0+36=108
}=108(k=4),

dp[1][3]=dp[1][2]+dp[2][3]+P1P2P3=0+0+1035=150(k=2),
dp[1][4]=max{
dp[1][2]+dp[2][4]+P1P2P4=0+90+1036=270,
dp[1][3]+dp[3][4]+P1P3P4=150+0+1056=450
}=450(k=3),
dp[1][5]=max{
dp[1][2]+dp[2][5]+P1P2P5=0+108+103=138,
dp[1][3]+dp[3][5]+P1
P3P5=150+30+105=230,
dp[1][4]+dp[4][5]+P1P4P5=450+0+10*6=510
}=510(k=4),

dp[0][2]=dp[0][1]+dp[1][2]+P0P1P2=0+0+103=30(k=1),
dp[0][3]=max{
dp[0][1]+dp[1][3]+P0
P1P3=0+150+105=200,
dp[0][2]+dp[2][3]+P0P2P3=30+0+35=45
}=200(k=1),
dp[0][4]=max{
dp[0][1]+dp[1][4]+P0
P1P4=0+450+106=510,
dp[0][2]+dp[2][4]+P0P2P4=30+90+36=138,
dp[0][3]+dp[3][4]+P0
P3P4=200+0+56=230
}=510(k=1)
dp[0][5]=max{
dp[0][1]+dp[1][5]+P0P1P5=0+510+10=520,
dp[0][2]+dp[2][5]+P0P2P5=30+108+3=141,
dp[0][3]+dp[3][5]+P0P3P5=200+30+5=235,
dp[0][4]+dp[4][5]+P0P4P5=510+0+6=516
}=520(k=1)
追溯过程:
dp[0][5】(k=1)→dp[1][5】(k=4)→dp[1][4】(k=3)→dp[1][3】(k=2)
那么戳爆气球的顺序为2→3→4→1
我们所要求得最优解为dp[0][5],其值为520.但是我们还要通过rec表来追溯其最优方案,也就是戳气球的顺序。
dp[0][5]的rec=1,则最终戳破的气球是1。则dp[0][5]=dp[0][1]+dp[1][5].那么我们可以追踪到dp[1][5],其rec=4,则戳破的气球为4。则dp[1][5]=dp[1][4]+dp[4][5].那么就可以追踪到dp[1][4],其rec=3,则戳破的气球为3。则dp[1][4]=dp[1][3]+dp[3][4],那么我们可以追踪到dp[1][3],其rec=2,则戳破的气球为2。最后dp[1][3]=dp[1][2]+dp[2][3],不可再追踪。由于我们是每次考虑最后被戳破的气球,因此所得顺序应该完全颠倒,对于此问题,戳破气球的顺序为2—3—4—1,也就是我们最优方案。

八、总结

这道题其实并不难,关键在于 dp 数组的定义,需要避免子问题互相影响,所以我们反向思考,将 dp[i][j] 的定义设为开区间,考虑最后戳破的气球是哪一个,以此构建了状态转移方程。对于如何穷举「状态」,我们使用了小技巧,通过 base case 和最终状态推导出 i,j 的遍历方向,保证正确的状态转移。总而言之,收获颇丰。

九、致谢

感谢老师,感谢学校,感谢国家!

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

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

相关文章

管理类联考——逻辑——知识篇——第三章 三段论(考2题)(以性质命题为基础,最常用推理)

第三章 三段论&#xff08;考2题&#xff09;&#xff08;以性质命题为基础&#xff0c;最常用推理&#xff09; 一、三段论的基本结构 基本结构1&#xff08;最简单&#xff0c;不考&#xff09;&#xff1a; 所有A是B 所有B是C 得&#xff1a;所有A是C 基本结构2&#xff…

网络安全系统教程+学习路线(自学笔记)

一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

MySQL查询优化大揭秘!看这些关键数据,让你的数据库速度飞起来!

大家好&#xff0c;我是小米&#xff0c;今天给大家分享一些关于MySQL查询优化的干货。在数据库开发和维护中&#xff0c;优化查询是至关重要的一环。通过合理的优化&#xff0c;我们可以让数据库的查询速度事半功倍。那么&#xff0c;在MySQL的查询计划中&#xff0c;有哪些关…

Spring五大类注解和方法注解

1.配置(重要)2.添加五大类注解2.1 Controller&#xff08;控制器存储&#xff09;2.2 Service&#xff08;服务存储&#xff09;2.3 repository&#xff08;仓库存储&#xff09;2.4 Component&#xff08;组件存储&#xff09;2.5 Configuration&#xff08;配置存储&#xff…

【Python】基础内容

简介 面向对象&#xff0c;解释型的编程语言使用缩进作为逻辑层次 运行效率较低 单行注释&#xff1a;以#开头&#xff1a;#注释内容多行注释&#xff1a;以一对三个双引号引起来的内容&#xff1a; “”“注释内容”“” 数据类型 type(被查看类型的数据)&#xff1a;查看…

KETTLE Driver class ‘org.gjt.mm.mysql.Driver‘ could not be found

kettle链接mysql&#xff1a;抛出异常 Driver class org.gjt.mm.mysql.Driver could not be found 这是因为你没有下载对应的mysql驱动程序包&#xff08;DRIVER.jar&#xff09;到你的kettle下&#xff1a; 1 查看你的mysql版本 C:\Users\22077>mysql --version mysql …

快速拼接字符串的新类StringJoiner~

初识StringJoiner类&#xff1a; StringJoiner 是 Java 8 新增的一个类&#xff0c;它不仅提供了一种快速、方便地将多个字符串拼接成一个字符串的方法&#xff0c;并且在拼接之时还可以指定分隔符、前缀和后缀&#xff0c;以及添加多个字符串&#xff0c;最终输出拼接后的字符…

9-基于stm32的MAX31865铂电阻PT100测温全套资料(原理图+教程+程序)

编号: 009 本项目可以通过PT100测温&#xff0c;测温范围为: -200~420C&#xff0c;采用1.8寸OLED显示该资料已经过实物验证&#xff0c;实物中是通过触发GPIO来测量当前的温度&#xff0c;程序注释非常详细&#xff0c;容易上手 经过实验验证&#xff0c;切实可行!配备详细代码…

p5.js 到底怎么设置背景图?

theme: smartblue 本文简介 点赞 关注 收藏 学会了 在 《p5.js 光速入门》 里我们学过加载图片元素&#xff0c;学过过背景色的用法&#xff0c;但当时没提到背景图要怎么使用。 本文就把背景图这部分内容补充完整&#xff0c;并且会提到在 p5.js 里使用背景图的一些注意点。…

森泰克sumtak控制器维修伺服驱动器维修SQ-12

日本森泰克sumtak控制器维修全系列型号。 控制器常见维修故障&#xff1a;短路&#xff0c;模块损坏&#xff0c;带不动负载&#xff0c;主轴准备未绪&#xff0c;驱动器未使能&#xff0c;编码器故障&#xff0c;主轴驱动模块故障&#xff0c;输出电压低&#xff0c;红色灯亮…

Java创建线程的四种方式和线程的生命周期(面试题彻底搞懂)

方式一&#xff1a;继承Thread类的方式&#xff1a; 创建一个继承于Thread类的子类 重写Thread类的run() --> 将此线程执行的操作声明在run()中 创建Thread类的子类的对象 通过此对象调用start()&#xff1a;①启动当前线程 ② 调用当前线程的run() 说明两个问题&#…

百度CDN配置TLS

概述 为了保障您互联网通信的安全性和数据完整性&#xff0c;百度智能云CDN提供TLS版本控制功能。您可以根据不同域名的需求&#xff0c;灵活地配置TLS协议版本。 TLS&#xff08;Transport Layer Security&#xff09;即安全传输层协议&#xff0c;在两个通信应用程序之间提…

关于Dockerfile的优化

如今各个公有镜像仓库中已经包含了成千上万的镜像文件&#xff0c;但并不是所有的镜像都是精简高效的。很多初学者刚开始都习惯使用FROM centos然后RUN 一堆yum install&#xff0c;这样还停留在虚拟机层面的使用&#xff0c;这样创建出来的镜像往往体积比较大。其实我们可以参…

Vmware 设置固定ip地址--桥接模式

前言&#xff1a; 若虚拟机没有设置固定ip地址&#xff0c;每次关机重启后都会更新ip地址。导致连接工具得跟着一起修改&#xff0c;每次修改很烦。 之前使用NAT模式&#xff0c;因为使用此模式后&#xff0c;每次打开网页都会转几秒钟后才会显示网页。所以才使用桥接模式&…

DP学习第一篇之爬楼梯

DP学习之爬楼梯 剑指 Offer II 088. 爬楼梯的最少成本 - 力扣&#xff08;LeetCode&#xff09; 1. 题目分析 可以从第0或者第1作为起始台阶、每次可以选择跳1或2步、到楼顶结束 2. 解题 a.解法一 状态表示 tips: 经验题目要求。以i位置为结尾&#xff0c;。。。 dp[i] :…

Frida技术:App逆向开发屠龙刀

Frida是一种基于JavaScript的动态分析工具,可以用于逆向开发、应用程序的安全测试、反欺诈技术等领域。Frida主要用于在已安装的应用程序上运行自己的JavaScript代码,从而进行动态分析、调试、修改等操作,能够绕过应用程序的安全措施,可以助力于对应用程序进行逆向分析。 …

OpenShift Virtualization - 从集群外部访问集群中的 VM(附视频)

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.12 的环境中验证 文章目录 方法1&#xff1a;通过 Service 的 NodePort 访问 VM方法2&#xff1a;通过外部 IP 访问 VM确认 OpenShift 集群环境为 Worker 节点添加 Linux Bridge创建使用 Li…

大文件上传功能在标签服务的简单应用和代码实现

各位看官大家好&#xff0c;今天给大家分享的又是一篇实战文章&#xff0c;希望大家能够喜欢。 目前「袋鼠云客户数据洞察平台」标签服务的群组按种类划分&#xff0c;可以分为三大类&#xff0c;分别是实时群组、动态群组以及静态群组。如果按创建方式划分则有两种&#xff0…

6.11 有名管道和无名管道

目录 进程间通讯介绍 System V IPC 无名管道 无名管道特点 无名管道创建-pipe 无名管道通信 无名管道-示例 有名管道特点 有名管道创建-mkfifo 有名管道读写-示例 进程间通讯介绍 无名管道&#xff08;pipe&#xff09; 有名管道 &#xff08;fifo&#xff09; 信号…

制造业供应商合作该如何协调?SRM供应商管理系统的出现改变一切

制造业是使用SRM系统频率最高的行业了&#xff0c;因为该行业需要与大量供应商合作和协调&#xff0c;以便及时获得所需的原材料和零件。同时&#xff0c;该行业生产周期长&#xff0c;需求通常较为稳定&#xff0c;需要稳定的供应链管理来确保生产效率和质量。因此&#xff0c…