一道贪心:加括号使算式的值最大


问题描述

给定一个算术表达式形如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();
}
}


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM