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

news/2024/12/5 6:54:10/

戳气球实验报告

目录

一、题目

二、分析原问题并做调整

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

四、确定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气球吹破为1for (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气球吹破为1for (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.ppmy.cn/news/398704.html

相关文章

闲聊下最近哦

随便聊聊 聊聊最近工作或日常上一家公司一直比较忙,人也比较懒,一直没有写博客,最近换了下工作,争取坚持写博客吧 聊聊最近工作或日常 上一家公司一直比较忙,人也比较懒,一直没有写博客,最近换了下工作,争取坚持写博客吧 上家公司做了几年多了,上半年离职换了个工作,现阶段这…

pycocotools报错,NameError: name ‘unicode’ is not defined

在深度学习训练过程中的评估阶段使用pycocotools时出现错误&#xff1a; if type(resFile) str or type(resFile) unicode: NameError: name ‘unicode’ is not defined 据网上说应该是python2和3版本的问题&#xff0c;Python2 的unicode函数在 Python3 中不再使用。 解决方…

我来侃手机--连载一之开篇就论N73

最近这一段时间以来&#xff0c;我一直在关注手机方面的资讯&#xff0c;从性能、参数、行情等各个方面深入了解&#xff0c;从一开始的孤陋寡闻&#xff0c;到现在也是半个行家了&#xff0c;当然毕竟是业余选手&#xff0c;还不能和各大网站靠写手机评测文章混饭的大哥相提并…

Symbian 介绍

Symbian由摩托罗拉、西门子、诺基亚等几家大型移动通讯设备商共同出资组建的一个合资公司&#xff0c;专门研发手机操作系统。而Symbian操作系统的前身是EPOC&#xff0c;而EPOC是 Electronic Piece of Cheese取第一个字母而来的&#xff0c;其原意为"使用电子产品时…

[转载]14-28岁必看,还算青年的你该用什么手机

如果您没有耐心读完如此冗长的文章&#xff0c;您可直接阅读一下重点段落── 第5页&#xff1a;22岁毕业&#xff1a;诺基亚N81 22岁毕业&#xff1a;诺基亚N81 22岁的你可能已经大四了&#xff0c;作为一个狂热的数码爱好者&#xff0c;普通的非智能手机肯定无法满足你的…

智能手机开发

本文来自学生大本营刘超的笔记&#xff0c;原文出处链接&#xff1a;http://student.csdn.net/space.php?uid130412&doblog&id16270。 手机可分为智能手机开发和feather phone手机。开发平台可分为开放式平台和封闭式平台&#xff0c;开放式平台包括symbian、windows…

《疯狂的程序员》八

71 和燕儿分手后&#xff0c;绝影竟大方地给自己无限期地放了个长假。所以人就是这样&#xff0c;绝影想&#xff1a;早知如此&#xff0c;当初跟燕儿在一起的时候就该给自己放个长假&#xff0c;好好陪陪她&#xff0c;说不定也不会搞到这一步。以前是因为在公司&#xff0c;现…

疯狂的程序员 80-最后

&#xff08;80&#xff09; 夭折 在绝影的印象中&#xff0c;救火队长这个角色一向都是由自己来扮演的&#xff0c;想想以前在公司&#xff0c;临到验收的时候&#xff0c;才发现软件里面居然还有巨大的Bug&#xff0c;这种事情&#xff0c;哪次不是自己挺身而出&#xff0c;“…