最近找实习面试、笔试不断,感觉自己的算法真心渣,赶快买几本书弥补一下。
书中许多算法是非常经典的, 并且对我的意义也特别大,所以我把这部分的东西放在博客上,算作纪念。
最大最长子序列
渊源
我上大一时,第一次碰到关于时间效率的问题就是这个了。我当时根本不会做,只能百度来谷歌去,结果搜到一堆神一样的实现,没有注释,指针、下标倒是用的掉渣天,完全是Show自己的C语言、C++的考试成绩和面试选择题的技巧。当时我看完这些,就开始讨厌算法,一来就是3年。
现在终于看到了《数据结构与算法分析——Java语言描述》(第二版)这本书,书中第二章讲解的想对详细的多,解决了我的疑惑和恐惧。
并且最大最长子序列是动态规划的经典问题(其实不加这个称号,也许初学者就不怕了),体现了根据现有数据来决策未来操作的思想。
问题描述
例如:有一个序列,例如 1 2 -4 1 3 -5.
求出最长的和最大的子序列和。如本例的结果就是:4(1, 3)
思路
方法1
暴力解决。计算出从每个i<length和i<j<length的和。 O(n3)
方法2
在上面的基础上优化,考虑到i确定时,j递增的过程中,计算和只需要上一步的结果加a[j]。 O(n2)
方法3
考虑到如下事实:
1. 将一个序列等分成两部分,最大和序列可能在左边,可能在右端,也可能跨越左右边(包含了左边最右端点,和右边最左端点)。我们可以对它分别求和,然后选择其中最大的值最为这个序列的最大值。
2. 当序列只有一元素时,该元素为正数,则它就是最大和;反之,0为最大和。
因此使用分治方法递归地解决。
O(nlogn)
方法四
考虑到如下事实:
1. 负数不应该成为一个最大最长子序列的第一个元素。
2. 负数和序列不应该成为一个最大最长子序列的头部。
3. 在一次扫描过程中,如果已扫描部分的和为负数,我们可以将其抛弃,并且在由正变负的过程中,我们可以记录下最大值出现的位置。
采用动态规划的方法,最终时间复杂度为
O(n)
.
解决方案
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.util.Scanner;
/**
* 求解最大最长子问题的四种解法。 From:《数据结构与算法分析 Java语言描述》 第2版
*/
abstract class LongestMaxSumSubSeq {
protected int[] sequence;
protected int length;
/**
* @param sequence
* 原序列
* @param length
* 原序列的实际长度(可能小于等于数组长度)
*/
public LongestMaxSumSubSeq(int[] sequence, int length) {
this.sequence = sequence;
this.length = length;
}
/**
* 计算最大最长子序列和。
*
* @return 最大最长子序列和
*/
public abstract int calculate();
}
/**
* 暴力枚举所有可能O(n^3)
*/
class Method1 extends LongestMaxSumSubSeq {
public Method1(int[] sequence, int length) {
super(sequence, length);
}
@Override
public int calculate() {
if (null == sequence) {
return 0;
}
int maxSum = 0;
// 选定起点
for (int i = 0; i < length; i++) {
// 选定终点
for (int j = i; j < length; j++) {
int curSum = 0;
// 对每一段求和,与最大值比较
for (int k = i; k <= j; k++) {
curSum += sequence[k];
}
if (curSum > maxSum) {
maxSum = curSum;
}
}
}
return maxSum;
}
}
/**
* 迭代枚举所有可能O(n^2)
*/
class Method2 extends LongestMaxSumSubSeq {
public Method2(int[] sequence, int length) {
super(sequence, length);
}
@Override
public int calculate() {
if (null == sequence) {
return 0;
}
int maxSum = 0;
// 起点
for (int i = 0; i < length; i++) {
int curSum = 0;
// 终点
for (int j = i; j < length; j++) {
curSum += sequence[j];
// 因为这一次仅仅是将终点后移一位,因此完全可以用上一次的加过来
if (curSum > maxSum) {
maxSum = curSum;
}
}
}
return maxSum;
}
}
/**
* 分治法求解O(n*log(n))
*/
class Method3 extends LongestMaxSumSubSeq {
public Method3(int[] sequence, int length) {
super(sequence, length);
}
@Override
public int calculate() {
if (null == sequence) {
return 0;
}
int maxSum = 0;
maxSum = recursedCalMax(0, length - 1);
return maxSum;
}
/**
* 二分治方法计算最大最长子序列
*
* @param start
* 第0个元素的位置
* @param end
* 最后一个元素的位置
* @return
*/
private int recursedCalMax(int start, int end) {
int max = 0;
// 递归基底:只有一个元素
if (start == end) {
return sequence[start] > 0 ? sequence[start] : 0;
}
// 递归步骤:二分
int center = (start + end) / 2;
/*
* 最大值可能在左边,可能在右边,也可能在中间,先分别求出, 然后比较。左右递归分治求出,中间部分两端扩展,单独求出。
* 三者彼此独立,计算顺序是无关的。
*/
// 求出左边、右边最大值
int maxLeft = recursedCalMax(start, center);
int maxRight = recursedCalMax(center + 1, end);
// 求中间部分,必须包含最左端最右元素,或者最右端最左元素
int maxLeftBorder = 0;
int maxRightBorder = 0;
// 临时储存当前和
int curLeftBorder = 0;
int curRightBorder = 0;
for (int i = center; i >= start; i--) {
curLeftBorder += sequence[i];
maxLeftBorder = curLeftBorder > maxLeftBorder ? curLeftBorder
: maxLeftBorder;
}
for (int i = center + 1; i <= end; i++) {
curRightBorder += sequence[i];
maxRightBorder = curRightBorder > maxRightBorder ? curRightBorder
: maxRightBorder;
}
max = Math.max(Math.max(maxLeft, maxRight), maxLeftBorder + maxRightBorder);
return max;
}
}
/**
* 动态规划法求解O(n)
*/
class Method4 extends LongestMaxSumSubSeq {
public Method4(int[] sequence, int length) {
super(sequence, length);
}
@Override
public int calculate() {
if (null == sequence) {
return 0;
}
int maxSum = 0;
int curSum = 0;
for (int i = 0; i < length; i++) {
// 负值、负序列不做开头
curSum += sequence[i];
if (curSum > maxSum) {
maxSum = curSum;
}
if (curSum < 0) {
// 负值和序列舍弃
curSum = 0;
}
}
return maxSum;
}
}
public class Main {
public static void calculateAndPrint(LongestMaxSumSubSeq method) {
int result = 0;
long startTime = 0L;
long endTime = 0L;
startTime = System.currentTimeMillis();
result = method.calculate();
endTime = System.currentTimeMillis();
System.out.println("-----" + method.getClass().getSimpleName()
+ "-----");
System.out.println("结果: " + result);
System.out.println("耗时: " + (endTime - startTime));
System.out.println();
}
private static Scanner getScanner(String[] args) {
Scanner scanner = null;
// 无参数
if (args.length == 0) {
scanner = new Scanner(System.in);
} else {
FileInputStream fin;
try {
fin = new FileInputStream(args[0]);
scanner = new Scanner(fin);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return scanner;
}
@SuppressWarnings("resource")
public static void main(String[] args) {
Scanner scanner = getScanner(args);
if (null == scanner) {
System.err.println("输入流错误");
return;
}
int length = scanner.nextInt();
int[] sequence = new int[length];
for (int i = 0; i < length; i++) {
sequence[i] = scanner.nextInt();
}
calculateAndPrint(new Method1(sequence, length));
calculateAndPrint(new Method2(sequence, length));
calculateAndPrint(new Method3(sequence, length));
calculateAndPrint(new Method4(sequence, length));
}
}