Solution - 最大乘积


题目描述

给你 \(n\) 个整数 \(a_1、a_2、...、a_n\),从中任意挑选出 \(n\) 个数字,使得乘积最大,输出乘积最大值。

输入格式

输入有多组测试数据。

第一行为整数 \(t\) ,表示测试数据组数。

每组测试数据第一行为整数 \(n\),表示数字的数量。

每组测试数据第二行有 \(n\) 个整数 \(a_1、a_2、...、a_n\)

输出格式

每组测试数据输出只有 \(1\) 个整数,表示挑选 \(5\) 出个数字的乘积最大值。

样例输入
4
5
-1 -2 -3 -4 -5
6
-1 -2 -3 1 2 -1
6
-1 0 0 0 -1 -1
6
-9 -7 -5 -3 -2 1
样例输出
-120
12
0
945
数据范围与提示

\(1 ≤ t ≤ 100\)
\(5 ≤ n ≤ 10^5\)
\(-10^3 ≤ a_i ≤ 10^3\)


此题我来介绍一下 dp 做法。
首先看到数据范围我们就可以判断此题要用时间复杂度接近 \(\mathcal{O}(n)\) 的算法,所以我们联想的了线性 \(dp\)

所以我们定义
\(dp1_{i,j}\) 表示从前 \(i\) 个数选 \(j\) 个数字所得乘积的最大值。
\(dp2_{i,j}\) 表示从前 \(i\) 个数选 \(j\) 个数字所得乘积的最小值。
这里解释一下为什么要求最小值,如果当前遍历到的 \(a_i\) 是负数,肯定乘上前 \(i - 1\) 个数当中选 \(j - 1\) 个数乘积的最小值是最优的,但如果只状态转移最大值,就肯定得不到最优解。

分析到这里就很容易写出状态转移了。

状态转移

\[dp1_{i,j} = \begin{cases} \max(dp1_{i-1,j},\ dp2_{i-1,j-1}×a_i), & \text{if }a_i ≤ 0 \\ \max(dp1_{i-1,j},\ dp1_{i-1,j-1}×a_i), & \text{if }a_i > 0 \end{cases} \]

\[dp2_{i,j} = \begin{cases} \max(dp2_{i-1,j},\ dp1_{i-1,j-1}×a_i), & \text{if }a_i ≤ 0 \\ \max(dp2_{i-1,j},\ dp2_{i-1,j-1}×a_i), & \text{if }a_i > 0 \end{cases} \]

此算法时间复杂度为 \(\mathcal{O}(t×5n)\)

最后给出代码

#include 
#define ll long long
using namespace std;
ll n, a[100005];
ll dp1[100005][6], dp2[100005][6];
int main() {
	freopen("maximum.in", "r", stdin);
	freopen("maximum.out", "w", stdout);
	ll t;
	scanf("%lld", &t);
	while (t--) {
		memset(dp1, 128, sizeof(dp1));
		memset(dp2, 127, sizeof(dp2));
		scanf("%lld", &n);
		for (ll i = 1; i <= n; i++) {
			scanf("%lld", &a[i]);
		}
		for (ll i = 0; i <= n; i++) dp1[i][0] = 1, dp2[i][0] = 1;
		for (ll i = 1; i <= n; i++) {
			for (ll j = 1; j <= min(5ll, i); j++) {
				if (a[i] <= 0) dp1[i][j] = max(dp1[i - 1][j], dp2[i - 1][j - 1] * a[i]);
				else dp1[i][j] = max(dp1[i - 1][j], dp1[i - 1][j - 1] * a[i]);
				if (a[i] <= 0) dp2[i][j] = min(dp2[i - 1][j], dp1[i - 1][j - 1] * a[i]);
				else dp2[i][j] = min(dp2[i - 1][j], dp2[i - 1][j - 1] * a[i]);
			}
		}
		printf("%lld\n", dp1[n][5]);
	}
	return 0;
}