每日一解 戳气球(困难的动归)

题目 戳气球

有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。

说明:

  • 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
  • 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

示例:
输入: [3,1,5,8]
输出: 167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/burst-balloons

思路

我自己想了半天动归的思路,但是都没有什么结果。看了看官解才意识到居然是一道时间复杂度O(n3)的题目,确实是一道很困难的题目。想要得到状态转移方程,就需要想到采用分治法降低问题难度。例如想到将戳破气球反过来,改为依次添加气球,其实不算难事,但是要运用分治法的思路来看待添加气球的过程。也就是得到如下的方程:

F(left, right) = nums[left] * nums[middle] * nums[right] + F(left, middle) + F(middle, right)

其中,left是左端点,right是右端点,middle为中间将要添加进去的那个气球。
也就是问题可以通过分治思路得到一个正确的求解方向:添加一个气球进去,求一下左中右三个气球的乘积,再递归去考虑中气球与左边气球构成的左右端点,再加入气球会是什么结果,和中气球与右气球中间再次加入气球会是什么结果。一直递归到左右两个端点气球是相邻的了,那么这个时候没右气球可以插入了,就有:

F(left, right) = 0 (left >= right - 1)

这个时候大体方向的思路是有了,但是很明显时间复杂度不止O(n3),因为还没有采用动态规划的空间换时间,存储会被重复计算到的内容。这个时候就需要分析一下会被重复计算到的值是什么。
拿题目中举的例子,输入为[3,1,5,8]来进行计算:

序号 0 1 2 3 4 5
1 3 1 5 8 1
备注 左端点 右端点

这里参考了一下官解的思路,设置了值为1的左端点和右端点,方便再插入第一个元素的时候进行计算。按照我们之前反向回推,”插入“气球的思路来看,最初的序列是这样:

序号 0 5
1 1
备注 左端点 右端点

现在要处理的问题是从1——4的数字中选一个插入,那么依次来看一下情况:

序号 0 1 5
1 3 1
备注 左端点 右端点

根据我们的公式,当插入的第一个气球,也即最后一个戳破的气球为序号1(值为3)的气球:
F(0, 5) = 1 * 3 * 1 + F(0, 1) + F(1, 5)
同理,依次看一下最后一次戳破的是序号2,3,4的气球:

序号 0 2 5
1 1 1
备注 左端点 右端点
序号 0 3 5
1 5 1
备注 左端点 右端点
序号 0 4 5
1 8 1
备注 左端点 右端点

此时F(0, 5)依次为:
F(0, 5) = 1 * 1 * 1 + F(0, 2) + F(2, 5)
F(0, 5) = 1 * 5 * 1 + F(0, 3) + F(3, 5)
F(0, 5) = 1 * 8 * 1 + F(0, 4) + F(4, 5)
我们最终答案的F(0, 5)自然就是这四种划分方法中得到值最大的那一种方法,那么公式的实际表述为:

F(left, right) = max(nums[left] * nums[middle] * nums[right] + F(left, middle) + F(middle, right))
其中left < middle < right.

同时可以注意到,反复需要用到计算的正是F(left, right)。其中left必然小于right。如果将此前求和得到的F(left, right)(例如F(0,4)和F(4,5))使用数组存储起来,自然可以节省很多不必要的计算开销。
在本题中,最终会得到的记录了F(left, right)值的dp_map为:

1 3 1 5 8 1
right 1 2 3 4 5
left
0 0 3 30 159 167
1 0 15 135 143
2 0 40 48
3 0 40
4 0

我们采用表中标记出的F(0, 4) ,即159来举例说明运算步骤:
0和4之间是可以插入1,2,3的,分别求一下其值:
F(0, 4) = F(0, 1) + F(1, 4) + nums[0] * nums[1] * nums[4] = 0 + 135 + 24 = 159
F(0, 4) = F(0, 1) + F(1, 4) + nums[0] * nums[1] * nums[4] = 3 + 40 + 8 = 51
F(0, 4) = F(0, 1) + F(1, 4) + nums[0] * nums[1] * nums[4] = 30 + 0 + 40 = 70
取最大值,那么F(0, 4)填入的值为159。

代码实现

按照上面说的思路,代码如下:

class Solution {
    
    
public:
	int maxCoins(vector<int>& nums) {
    
    
		nums.insert(nums.begin(), 1);
		nums.push_back(1);
		vector<vector<int>> dp_map = vector<vector<int>>(nums.size() - 1, vector<int>(nums.size(), 0));
		for (int i = 2; i < nums.size(); i++) {
    
    
			for (int j = 0; j < nums.size() - i; j++) {
    
    
				int max = 0;
				for (int k = j + 1; k < i + j; k++) {
    
    
					int answer = nums[j] * nums[k] * nums[i + j];
					answer += (dp_map[j][k] + dp_map[k][j + i]);
					if (answer > max) {
    
    
						max = answer;
					}
				}
				dp_map[j][j + i] = max;
			}
		}
		return dp_map[0][nums.size() - 1];
	}
};

其中i,j,k具体的意思和之前例子中的略有出入。因为dp_map采用从左上到右下的顺序去更新是最合适的,所以代码中,j表示left,k表示middle,而i + j表示right。

猜你喜欢

转载自blog.csdn.net/new_DYX/article/details/107439920