【问题标题】:Read 600,000 input from the console in less than 2 seconds在不到 2 秒的时间内从控制台读取 600,000 条输入
【发布时间】:2016-07-31 00:44:18
【问题描述】:

目标

我正在解决this 问题:

小女孩和最大和

小姑娘很喜欢数组查询的问题。

有一天,她遇到了一个众所周知的问题:你有一个 n 个元素的数组(数组的元素从索引开始 从 1);此外,有 q 个查询,每个查询由一对定义 整数 li, ri (1 ≤ li ≤ ri ≤ n)。您需要为每个查询找到 索引从 li 到 ri 的数组元素的总和。

小女孩觉得这个问题很无聊。她决定 在以某种方式回复查询之前重新排序数组元素 这使得查询回复的总和尽可能大。你的任务是 求这个最大和的值。

输入 第一行包含两个空格分隔的整数 n (1 ≤ n ≤ 2·105) 和 q (1 ≤ q ≤ 2·105) — 元素的数量 数组和查询的数量,对应。

下一行包含 n 个空格分隔的整数 ai (1 ≤ ai ≤ 2·105) — 数组元素。

以下 q 行中的每一行都包含两个空格分隔的整数 li 和 ri (1 ≤ li ≤ ri ≤ n) — 第 i 个查询。

输出 在一行中打印一个整数 - 的最大总和 数组元素重新排序后的查询回复。

对于测试 7(请参阅问题末尾的测试结果),输入是一个大小为 200,000 的数组,其中包含 200,000 个查询(具有 rl 值)。

输入看起来像这样:

200000 200000
189622 189286 194361 184457 182376 183471 197548 184736 195806 ... 200,000 integers

188738 290041
33738 90041
122738 390041
... 200,000 line

您可以download a sample input file,也可以创建自己的示例输入。数字本身并不重要。


问题

我需要在不超过 2 秒的执行时间的情况下读取 600,000 行输入。问题是,它甚至没有在 2 秒内读取前 200,000 个输入。

如何加快我的代码在 2 秒内读取所有 600,000 个输入?


代码

这是我的第一次尝试:

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int q = scanner.nextInt();
        int[] array = new int[n];
        for (int i=0; i<n; i++) {
            array[i] = scanner.nextInt();
        }
        int[][] qArray = new int[q][2];
        for (int i=0; i<q; i++) {
            qArray[i][0] = scanner.nextInt();
            qArray[i][1] = scanner.nextInt();
        }

        int[] index = new int[n];
        Arrays.sort(array);
        for (int i=0; i<q; i++) {
            for (int j = qArray[i][0]-1; j<qArray[i][1]; j++) {
                index[j]++;
            }
        }
        Arrays.sort(index);
        long sum =0;
        for (int i = 0; i<n; i++) {
            sum += index[i]*array[i];
        }
        System.out.println(sum);
    }
}

这是我的第二次尝试:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        try {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            String input = bufferedReader.readLine();
            String[] SplitInput = input.split(" ");
            int n = Integer.parseInt(SplitInput[0]);
            int q = Integer.parseInt(SplitInput[1]);

            String input2 = bufferedReader.readLine();

            int[][] qArray = new int[q][2];
            for (int i=0; i<q; i++) {
                input = bufferedReader.readLine();
                SplitInput = input.split(" ");
                qArray[i][0] = Integer.parseInt(SplitInput[0]);
                qArray[i][1] = Integer.parseInt(SplitInput[1]);
            }

            String[] SplitInput2 = input2.split(" ");
            int[] array = new int[n];
            for(int i=0; i<n; i++){
                array[i] = Integer.parseInt(SplitInput2[i]);
            }

            int[] index = new int[n];
            Arrays.sort(array);
            for (int i=0; i<q; i++) {
                for (int j = qArray[i][0]-1; j<qArray[i][1]; j++) {
                    index[j]++;
                }
            }
            Arrays.sort(index);
            long sum = 0;
            for (int i=0; i<n; i++) {
                sum += index[i]*array[i];
            }
            System.out.println(sum);
        }
        catch (NumberFormatException ex) {
            System.out.println("Not a number !");
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试结果

尝试 1

7 时间:2000 毫秒,内存:20612 KB 判决:TIME_LIMIT_EXCEEDED

尝试 2

7 时间:2000 毫秒,内存:41340 KB 判决:TIME_LIMIT_EXCEEDED

您可以查看我的完整测试结果herehere。同样,问题出在测试 7 上。

【问题讨论】:

  • 那么问题到底是什么?您是否通过分析器运行代码以查看时间?
  • 您要求我们在我们无权访问的平台上、在我们无权访问的输入文件上使用包含明显错误的代码调试问题。既然你提到它,这个问题确实应该被杀死。
  • 我无法访问完整的输入,但第一行是 200000 200000 第二行是用空格分隔的 200,000 个整数,然后 200,000 行每行有两个用空格分隔的整数
  • 啊,我明白你现在在问什么了。我认为大多数人(包括我一开始)都无法完全区分您在所有这些中提出的问题。我编辑了您的问题以使其更易于阅读,并再次重申您的问题(粗体)。我还提名你重新开放。我希望你能找到你的答案!在此期间,我建议您通过机器上的代码分析器运行您的代码。这将帮助您缩小性能瓶颈。
  • 遗憾的是,chat.stackoverflow.com 上的人建议您将其发布到 CodeReview 而不是 StackOverflow 上

标签: java performance time execution-time


【解决方案1】:

免责声明,我说过我能够帮助你,我是,但我不能解决你。我无法将其限制在 2 秒内,因为我没有正确理解问题本身。从技术上讲,我了解您的算法的作用,但我在概念上理解它有问题,这使我无法找到 大优化。 我找到了大优化。见答案底部。

备注:我已经在较小的测试页面上看到了您的结果,您的第一次测试持续 200 多毫秒是绝对没有理由的。我只是不明白。它们在我的计算机上都在 2 毫秒内持续运行(使用 Java 内部 System.nanotime() 方法)。我相信测试包括JVM的启动。如果确实如此,我是否可以建议您切换到更优化的语言,例如 C 或 C++?这意味着该测试在一定程度上针对解释语言进行了操纵。

算法

第一个问题是您的算法本身。这很慢:您实际上是在迭代 200,000 × x ints(从您的文件来看,这平均是一个很高的值)。在最坏的情况下,您将迭代 200,000 × 200,000 = 40,000,000,000 个整数。难怪你有 20 秒左右的时间。

这太过分了。理想情况下,您应该能够使用优化(如使用地图)来减少双循环。您有大量可用内存 (256 MB),请使用它。你已经这样做了;多做点。

大优化在这里的某个地方。我相信,您应该通过跳过此索引机制并使用更好的机制来找到另一种优化,而不是逐个索引递增。我相信这就是问题存在的原因:找到那个算法而不是其他算法。我不喜欢这样,但我不评价它。

读取数据

我测试了通过输入读取数据,你的方法很慢。我责怪你使用Scanner

我建议您使用这种结构和这种拆分方法,它在我的计算机上运行的总时间

try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
  int[] counts = split(reader.readLine(), new int[2]);
  int n = c[0], q = c[1];
  int[] array = split(reader.readLine(), new int[n]);
  int[][] queries = new int[q][]; // Note, no size in the second part of the array creation.
  for (int i = 0; i < q; i++) {
    queries[i] = split(reader.readLine(), new int[2]);
  }
  ...
}

使用针对您的用例优化的适当拆分方法:

static int[] split(String s, int[] a) {
  int n = 0, aIndex = 0;
  for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) {
    char c = s.charAt(sIndex);
    if (c == ' ') { // Separator
      a[aIndex++] = n;
      n = 0;
    } else if ('0' <= c && c <= '9') { // Number
      n = n * 10 + (c - '0'); // Add a digit to the current number
    }
  }
  a[aIndex] = n;
  return a;
}

小优化

从概念上讲,您有以下代码:

for (int i = 0; i < q; i++) {
  // Fill qArray
}

for (int i = 0; i < q; i++) {
  // Work with index.
}

这两个循环可以合并,甚至进一步消除您对qArray 的需要。你读取数据,然后直接处理它。如果循环彼此相邻,这并不重要,但在这之间,您在第一次尝试中对数组中的内容进行排序,并且在第二次尝试中对数组进行排序并解析输入。一方面,这使您的数据远离 CPU 缓存,但另一方面您正在处理 I/O。我不知道哪个更好。

您的代码中有错误

我试图重新思考问题并找到解决方案,但您的答案与我的答案不同。我实际上在您的代码中发现了一个错误。我无法通过您的文件获得与您相同的结果。

在最后一个循环中,即 sum 循环中,您将所有内容存储在一个 long 中,但实际上可能会导致 int 溢出。所以你应该这样计算:

sum += (long)(index[i]) * array[i];

找到了!

关于您的代码,正如我所说,您遇到了问题,因为您可能会收到超过 400 亿条指令。我可以用您在下面看到的内容来扁平化您的解决方案。而且我现在一直达到 500 毫秒。

public static void main(String[] args) throws IOException {
  long nanos = System.nanoTime();
  myMain();
  nanos = System.nanoTime() - nanos;
  System.out.printf("Time: %sms%n", MILLISECONDS.convert(nanos, NANOSECONDS));
}

static void myMain() throws IOException {
  try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
    int[] counts = split(reader.readLine(), new int[2]);
    int n = counts[0], q = counts[1];
    int[] array = split(reader.readLine(), new int[n]);
    int[] indices = new int[n];
    for (int i = 0; i < q; i++) {
      int[] query = split(reader.readLine(), new int[2]);
      indices[query[0] - 1]++;
      if (query[1] < n) {
        indices[query[1]]--;
      }
    }
    for (int i = 1; i < n; i++) {
      indices[i] += indices[i - 1];
    }
    sort(array, 200_000);
    sort(indices, 200_000);
    long sum = 0;
    for (int i = 0; i < n; i++) {
      sum += (long)array[i] * indices[i];
    }
    System.out.println(sum);
  }
}

static void sort(int[] array, int n) {
  int[] counts = new int[n+1];
  for (int element: array) {
    counts[element]++;
  }
  int current = 0;
  for (int i = 0; i < counts.length; i++) {
    Arrays.fill(array, current, current + counts[i], i);
    current += counts[i];
  }
}

static int[] split(String s, int[] a) {
  int n = 0, aIndex = 0;
  for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) {
    char c = s.charAt(sIndex);
    if (c == ' ') {
      a[aIndex++] = n;
      n = 0;
    } else if ('0' <= c && c <= '9') {
      n = n * 10 + (c - '0');
    }
  }
  a[aIndex] = n;
  return a;
}

享受吧!

如果您对此优化有任何疑问,请不要犹豫;)

【讨论】:

  • 感谢您迄今为止所做的所有事情,我会将所有这些想法合并到一个程序中,当我找到最佳解决方案时,我将在此处发布
  • @robert 我喜欢一些谜题,所以我必须得到它。请在我编辑的答案中找到我的改进。
  • btw printfprintprintln 慢 :) 前几天我尝试了非常大的输出,发现 printprintf
  • @OlivierGrégoire 您也可以在循环内重用query(保存new int[2])。由于转义分析,可能没有太大区别,但由于它是一个直接的更改(并且您的 split 方法已经获得了输入)它不会使代码变得更丑。
  • 是的,我知道我可以做得更好,但是当我持续超过 2 秒的限制时,我并没有在优化方面付出更多努力。对此很抱歉,但当然,如果您想尽快完成,总会有你们提到的改进。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-27
  • 2011-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多