一次由 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() 方法清空缓冲区,不然会输出不了结果。
  • 最后记得关闭输入输出流,养成一个好习惯,这样即使忘记清空缓冲区,也会因为关闭输出流而自动清空缓冲区。
相关文章
|
机器学习/深度学习 存储 搜索推荐
利用机器学习算法改善电商推荐系统的效率
电商行业日益竞争激烈,提升用户体验成为关键。本文将探讨如何利用机器学习算法优化电商推荐系统,通过分析用户行为数据和商品信息,实现个性化推荐,从而提高推荐效率和准确性。
782 14
|
5月前
|
数据采集 人工智能 监控
大模型微调数据质量评估指南:如何为你的AI挑选“好食材”
本文系统介绍大模型微调数据质量的科学评估框架,提出“复杂性、可用性、多样性”三大核心维度,并结合推理损失逆向验证,提供可落地的五步评估法与实操工具(如LLaMA-Factory Online),助力团队以更少高质量数据获得更优模型效果。
|
人工智能 Java API
ai-api-union项目,适配各AI厂商api
本项目旨在实现兼容各大模型厂商API的流式对话和同步对话接口,现已支持智谱、豆包、通义、通义版DeepSeek。项目地址:[https://gitee.com/alpbeta/ai-api-union](https://gitee.com/alpbeta/ai-api-union)。通过`ChatController`类暴露两个接口,入参为`ChatRequest`,包含会话ID、大模型标识符和聊天消息列表。流式对话返回`Flux&lt;String&gt;`,同步调用返回`String`
|
存储 人工智能 Serverless
AI助手测评 | 3步快速构建主动式智能导购AI助手
本文介绍了如何利用阿里云的百炼平台构建主动式智能导购AI助手。在当前经济形势下,企业通过AI技术可以有效降低成本并提升服务质量。主动式智能导购AI助手不仅具备专业知识和耐心,还能24小时不间断服务用户,帮助企业节省夜班客服费用。通过创建API-KEY、部署函数计算应用和集成百炼商品检索应用,企业可以在短短几步内快速构建这一智能系统。此外,文章还提供了详细的部署步骤和测评建议,确保企业在实际应用中能够顺利实施。
|
存储 人工智能 Java
迭代加深搜索
迭代加深搜索(Iterative Deepening Search, IDS)是一种结合了广度优先搜索(BFS)和深度优先搜索(DFS)的搜索策略,它通过重复执行深度限制的深度优先搜索来实现。每次迭代,深度限制增加,直到达到目标节点或搜索空间耗尽。下面是 V 哥的一些理解,分享给大家
602 1
【图片公式识别】图片公式转Word与LaTeX文档:智能识别与转换
【图片公式识别】图片公式转Word与LaTeX文档:智能识别与转换
1160 4
|
人工智能 自然语言处理 开发者
ChatGPT在国内的使用限制,国内的ChatGPT替代工具
ChatGPT在国内的使用限制,国内的ChatGPT替代工具
647 0
|
算法 C++
【洛谷 P1055】[NOIP2008 普及组] ISBN 号码 题解(字符串)
该编程题目要求编写程序检查输入的ISBN号码的识别码是否正确。ISBN号码格式为`x-xxx-xxxxx-x`,其中`x`是数字,最后一位是通过特定算法计算得出的识别码。算法是将前9位数字乘以1到9的加权值,求和后对11取模,模为10时识别码为大写`X`,否则为对应模值的数字。程序接收一个符合格式的ISBN号码,验证识别码并输出`Right`(如果正确)或修正后的正确ISBN号码。提供的AC代码使用C++实现这一功能。
689 0
|
Python
解决Anaconda报The channel is not accessible源通道不可用问题
最近在通过pycharm开发python程序,引用anaconda环境建立虚拟环境时报错,报UnavailableInvalidChannel: The channel is not accessible or is invalid.应该是镜像源访问通道无法访问或无效。现将解决办法记录如下:
17465 0
解决Anaconda报The channel is not accessible源通道不可用问题