問題描述
給定一個算術表達式形如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();
}
}