问题描述
给定一个算术表达式形如1+3-5-4+6,表达式中的运算数全部都是正数,运算符全部是加号或者减号。
现在可以给算术表达式加任意多的括号,使得表达式的值最大。
如对于1+3-6-9+4-5-7+8,可以1+3-(6-9)+4-(5-7)+8,最优的方案是1+3-(6-(9+4)-5-(7+8))
数据格式:
输入:第一行输入一个n表示运算数的个数,第二行输入一个表达式。
输出:一个数字,表示表达式的最大值。
数据范围:运算数个数为1e5。
解析
最优答案不唯一,我们只需要考虑如何构造出最优答案。
- 最优答案中,左括号只加在减号后面,因为加在加号后面跟不加没有区别
- 最优答案中,右括号不能加在若干个加号中间,例如3-4+5+6+7-8,其中5,6,7之间肯定没有括号。可以用移动括号的办法来解释:左括号不能加在加号后面(第一条已证),右括号如果加在若干个加号之间,总可以通过移动右括号来使算式更大,也就是说,右括号放在加号之间时不稳定的。
先看几个例题:
1-2-3 => 1-(2-3) 通过加括号可以是减号变成加号
1-2+3-5 => 1-(2+3-5)
1-2+3-2 => 1-(2)+3-(2) 对于a-b+c-d的形式,如果d>c,那么需要把d括进来,否则在c前面放上右括号
1-2+3-2+5-100 =>1-(2+3-(2+5)-100) 对上面那条规则进行修正,此例中虽然2<3,但是后面还有个5
分两类情况:
- 如果表达式形如a-b-c...,那么a-(b-c...)括号中的全部符号除了b都能够取到绝对值
- 如果表达式形如a-b+c...,那么需要分两类行情况讨论。如果一括到底,可得收益x;如果及时中断,可以递归解决子问题,得到收益y。因为问题规模较大,递归有可能栈溢出,所以可以使用动态规划倒序处理。
最优答案中,括号肯定不会嵌套超过两层:这是因为减号两次嵌套相当于没有嵌套。
贪心的原则就是,既然无论如何都要给右面留下左括号,那么左括号的位置必然是最优的。
此算法复杂度O(n)
还有一种更加直观的O(n^3)算法,max(f,t)和min(f,t)表示[f,t]闭区间上的最大值和最小值,也是动态规划。
下面的代码使用两种算法实现,经过两种算法互相验证,基本可以断言两种算法都是正确的。
import java.util.Arrays;
import java.util.Random;
public class Main {
int[] a;
char[] op;
Random random = new Random(0);
void generateProblem() {
int n = random.nextInt(60) + 1;
a = new int[n];
op = new char[n];
for (int i = 0; i < n; i++) {
a[i] = random.nextInt(200);
if (i == 0) op[i] = '+';
else {
op[i] = random.nextBoolean() ? '+' : '-';
}
}
}
//暴力方法求正确答案
class Bruteforce {
int min[][] = new int[a.length][a.length];
int max[][] = new int[a.length][a.length];
int solve() {
for (int i = 0; i < a.length; i++) {
min[i][i] = a[i];
max[i][i] = a[i];
}
for (int step = 1; step < a.length; step++) {
for (int i = 0; i + step < a.length; i++) {
int f = i, t = i + step;
int s = a[f];
for (int j = f + 1; j <= t; j++) {
s += (op[j] == '+' ? 1 : -1) * a[j];
}
min[f][t] = max[f][t] = s;
for (int j = f + 1; j <= t; j++) {
if (op[j] == '-') {
int nowMax = max[f][j - 1] - min[j][t];
int nowMin = min[f][j - 1] - max[j][t];
min[f][t] = Math.min(min[f][t], nowMin);
max[f][t] = Math.max(max[f][t], nowMax);
}
}
}
}
return max[0][a.length - 1];
}
}
//最好的方法
class Best {
int[] a;
char[] op;
int max[];//max[i]表示从i到末尾表达式的最大值
int suffix[];//min[i]表示从i到末尾表达式的最小值
//压缩正号
void compress() {
int i = 0;
a = Arrays.copyOf(Main.this.a, Main.this.a.length);
op = Arrays.copyOf(Main.this.op, Main.this.op.length);
for (int j = 1; j < a.length; j++) {
if (op[i] == '+' && op[j] == '+') {
a[i] += a[j];
} else {
i++;
op[i] = op[j];
a[i] = a[j];
}
}
a = Arrays.copyOf(a, i + 1);
op = Arrays.copyOf(op, i + 1);
}
int solve() {
compress();
max = new int[a.length];
suffix = new int[a.length];
suffix[suffix.length - 1] = a[suffix.length - 1];
for (int i = suffix.length - 2; i >= 0; i--) {
suffix[i] = a[i] + suffix[i + 1];
}
max[a.length - 1] = a[a.length - 1];
for (int i = a.length - 2; i >= 0; i--) {
if (op[i + 1] == '+') {
max[i] = max[i + 1] + a[i];
} else {
if (i + 2 >= a.length) {
max[i] = a[i] - a[i + 1];
continue;
}
if (op[i + 2] == '-') {//连续两个减号
max[i] = a[i] - a[i + 1] + suffix[i + 2];
} else {//-+-
//一括到底
int s = a[i] - a[i + 1] - a[i + 2] + (i + 3 < suffix.length ? suffix[i + 3] : 0);
//只扩一个
int ss = a[i] - a[i + 1] + max[i + 2];
max[i] = Math.max(s, ss);
}
}
}
return max[0];
}
}
String tos() {
StringBuilder builder = new StringBuilder();
builder.append(a[0]);
for (int j = 1; j < a.length; j++) {
builder.append(op[j]).append(a[j]);
}
return builder.toString();
}
Main() {
for (int i = 0; i < 10000; i++) {
generateProblem();
System.out.println(tos());
int ans = new Bruteforce().solve(), mine = new Best().solve();
System.out.println("ans:" + ans + " mine:" + mine);
if (ans != mine) {
throw new RuntimeException("error");
}
}
}
public static void main(String[] args) {
new Main();
}
}