一次由 System.out.println() 引起的 MLE&TLE

简介: 笔者并非 ACM选手,但是由于最近备考 CCF 认证需要练练手,笔者是忠实的 Java 选手,于是就打算使用 Java 进行考试。随机到一道题 P5461 赦免战俘,看题第一感觉就是递归处理,不出意外的成功写出了递归解法,然后高高兴兴的就在 OJ 上提交,然后就是莫名其妙的 MLE。

莫名其妙 MLE


  笔者并非ACM选手,但是由于最近备考 CCF 认证需要练练手,笔者是忠实的 Java 选手,于是就打算使用 Java 进行考试。随机到一道题P5461 赦免战俘,看题第一感觉就是递归处理,不出意外的成功写出了递归解法,然后高高兴兴的就在 OJ 上提交,然后就是莫名其妙的 MLE


原始代码:

// 递归函数
public static void f(int[][] a, int x1, int x2, int y1, int y2) {
if (x2-x1==1 && y2-y1==1) {
a[x1][y1] = 0;
return;
} else {
for (int i = x1; i <= (x2-x1)/2+x1; ++i) {
for (int j = y1; j <= (y2-y1)/2+y1; ++j) {
a[i][j] = 0;
}
}
f(a, (x2-x1)/2+x1+1, x2, y1, (y2-y1)/2+y1);
f(a, x1, (x2-x1)/2+x1, (y2-y1)/2+y1+1, y2);
f(a, (x2-x1)/2+1+x1, x2, (y2-y1)/2+1+y1, y2);
}
}


第一次尝试结果:



第一次思考


  因为使用的是 int 类型的二维数组,而题目明确只需要存储 0 1。于是理所当然的想到将 int[ ][ ] 改为 byte[ ][ ],于是进行第二次尝试。


第一次改进代码:

// 更改后的递归函数
public static void f(byte[][] a, int x1, int x2, int y1, int y2) {}


第二次尝试结果:



第二次思考


  第二次尝试仍然没有解决问题,证明问题不是出在那里,看来得另辟蹊径了。难道是递归深度太深导致爆内存?于是决定改进算法,仔细观察题目给出样例,发现数据有规律,类似于杨辉三角,数据依赖于该数上方和右上方的数(eg.a[2][3] = (a[1][3] + a[1][4]) % 2,也就是不进位的加法,可以使用异或优化计算,所以可以优化为 a[2][3] = a[1][3] ^ a[1][4]),于是将原有代码全部推倒从头再来,经过一番努力终于完成新算法,于是进行第三次尝试。


第二次改进代码:

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
byte[][] a = new byte[2048][2048];
n = (1<<n); // 等价于 n = Math.pow(2, n);
a[0][n+1] = 1;// 需要一个初始化数
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
// 等价于 a[i-1][j] = (a[i-1][j] + a[i-1][j+1]) % 2;
a[i][j] = (byte)(a[i-1][j] ^ a[i-1][j+1]);
System.out.printf("%d ", a[i][j]);
}
System.out.println();
}
in.close();
}
}


第三次尝试结果:



第三次思考


  如此简洁的代码怎么可能 MLE 呢?数组占用内存很小,计算使用位运算优化,没道理会 MLE,唯一可能的就是 IO 产生的内存占用了,于是开始测试自己的想法,每次输出完后调用 System.gc(); 处理一下垃圾。


第三次改进代码:

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
a[i][j] = (byte)(a[i-1][j] ^ a[i-1][j+1]);
System.out.printf("%d ", a[i][j]);
}
System.out.println();
System.gc();// 测试是否 IO 引起的爆内存
}


第四次尝试结果:



第四次思考


  可以发现 MLE 的问题解决了,证明确实是IO 引起的爆内存,但是由于 System.gc(); 消耗时间太多又导致了 TLE,于是决定控制System.gc(); 先把题目 AC 了再说。但是经过多次调试都无法平衡时间和空间,要么TLE 要么 MLE


第五次尝试结果:





第五次思考


  无法取巧通过测试,就只能另辟蹊径了,突然想到输出文件的时候都需要一个缓冲区,平时都没怎么注意这个问题,存在即合理,官方这么做必要有它的道理。于是查看源码以及搜集资料。
  
发现 System.out.println(); 是一个同步方法,有一定的开销,在高并发的情况下,会严重影响性能,但这并不是主要问题。更多的是添加字符到缓冲区和打印的开销,这才是导致我的代码 MLE 的原因,因为我的代码每次只输出一个数字,而且产生很多次调用,产生极大开销。于是决定改用缓冲区暂存输出结果,最后输出结果。

使用 PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));代替 System.out.println();

最终代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.PrintWriter;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(new BufferedInputStream(System.in));
PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
int n = in.nextInt();
byte[][] a = new byte[2048][2048];
n = (1<<n);
a[0][n+1] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
a[i][j] = (byte)(a[i-1][j] ^ a[i-1][j+1]);
out.append(Byte.toString(a[i][j]));
out.append(" ");
}
out.append("\n");
}
out.flush();
out.close();
in.close();
}
}


最终结果:



总结


  • 使用 System.out.println() 进行标准输出时,开销较大,不适合频繁调用。
  • 同理,Scanner(System.in) 也不适合频繁调用(一次由 Scanner(System.in) 引起的 TLE)
  • 频繁输入调用使用 StreamTokenizer in = new      StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
  • 频繁输出调用使用 PrintWriter out = new      PrintWriter(new BufferedOutputStream(System.out));
  • 可手动调用 flush() 方法输出缓冲区,但是过于频繁的调用也有可能引发 TLE
  • 一般情况下,使用传统的System.out.println();我觉得足以应付,遇到频繁输出时再使用改进方法。当然为了防止输出问题导致TLE&MLE的话,可以直接使用PrintWriter out = new PrintWriter(new      BufferedOutputStream(System.out));记得在最后调用 flush() 方法清空缓冲区,不然会输出不了结果。
  • 最后记得关闭输入输出流,养成一个好习惯,这样即使忘记清空缓冲区,也会因为关闭输出流而自动清空缓冲区。
相关文章
|
5月前
|
Java
System.currentTimeMillis()方法总结
System.currentTimeMillis()方法总结
print与println的区别
print与println的区别
79 0
“System.out.println(的正确格式
“System.out.println(的正确格式
123 0
C. Registration system
C. Registration system
43 0
使用System.out.println()
使用System.out.println()
78 0
ZCMU - 1992: Swiss-system tournament
ZCMU - 1992: Swiss-system tournament
110 0
|
缓存 Java Linux
注意了!System.currentTimeMillis() 存在性能问题...
System.currentTimeMillis()是极其常用的基础Java API,广泛地用来获取时间戳或测量代码执行时长等,在我们的印象中应该快如闪电。 但实际上在并发调用或者特别频繁调用它的情况下(比如一个业务繁忙的接口,或者吞吐量大的需要取得时间戳的流式程序),其性能表现会令人大跌眼镜。
注意了!System.currentTimeMillis() 存在性能问题...
System类的常用方法(currentTimeMillis与arraycopy)
System类的常用方法 currentTimeMillis与arraycopy import java.util.Arrays; /* java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有: public st
|
Java 关系型数据库 Oracle
System.out.println
This Java tutorial is to explain what System.out.println is and how it works. It is love at first type.
1169 0
|
JavaScript Linux 前端开发