3
\$\begingroup\$

I am doing some assignments on my own and one of them is about a Island Counting program, I have come up with a working program but the problem is its very slow.

When running test on for example maps that are 30x30 in size is its around 24 ms, which is not bad but pretty slow. because when I test on maps that are 10000x10000 in size if finishes after approximatly 9 seconds and it need to be much faster.

Here is my code:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;

public class oar {
    public static void main(String[] args) throws FileNotFoundException{
        long startTime = System.nanoTime();
        File inputfile = new File("islands.7.in");
        Scanner sc = new Scanner(inputfile);
        //Scanner sc = new Scanner(System.in);
        int x = sc.nextInt();
        int y = sc.nextInt();
        char[][] map = new char[x][y];
        for (int i = 0; i < x; i++) {
            String row = sc.next();
            for (int j = 0; j < y; j++) {
                map[i][j] = row.charAt(j);
            }
        }
        int count = 0;
        boolean[][] visited = new boolean[x][y];
        for (int i = 0; i < x; i++) {
            for (int j = 0; j < y; j++) {
                if (!visited[i][j] && map[i][j] == '@') {
                    bfs(map, visited, i, j);
                    count++;
                }
            }
        }
        System.out.println(count);
        long endTime = System.nanoTime();
        long elapsedTime = endTime - startTime;
        long tiden = elapsedTime/1000000000;
        System.out.println("Elapsed time: " + tiden + " seconds");
    }

    static void bfs(char[][] map, boolean[][] visited, int i, int j) {
        int x = map.length;
        int y = map[0].length;
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[] { i, j });
        visited[i][j] = true;
        while (!queue.isEmpty()) {
            int[] curr = queue.poll();
            int row = curr[0];
            int col = curr[1];
            if (row > 0 && !visited[row - 1][col] && map[row - 1][col] == '@') {
                queue.offer(new int[] { row - 1, col });
                visited[row - 1][col] = true;
            }
            if (row < x - 1 && !visited[row + 1][col] && map[row + 1][col] == '@') {
                queue.offer(new int[] { row + 1, col });
                visited[row + 1][col] = true;
            }
            if (col > 0 && !visited[row][col - 1] && map[row][col - 1] == '@') {
                queue.offer(new int[] { row, col - 1 });
                visited[row][col - 1] = true;
            }
            if (col < y - 1 && !visited[row][col + 1] && map[row][col + 1] == '@') {
                queue.offer(new int[] { row, col + 1 });
                visited[row][col + 1] = true;
            }
        }
    }
}

I currently use a bfs algorithm, are there any better techniques i can use?

Here are the input for example: Its in the format of x y and then under is the map, where '@' are land and '~' are water.

8 13
~@@~~~@~~~~~~
~@@~~@@~~~~~~
~~~~~@~~~@~@~
~~~@@~~~@@@@~
~~~~~~~@@~~@~
~~~~~~@@@@~~~

and the answer is 4 islands

Now this is a very small map, I have a mapbuilder program that generates much bigger maps and on those the program is very slow.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
import java.util.*;
        ...
        Scanner sc = new Scanner(inputfile);

It turns out the * wildcard refers to this class. Prefer to list {Scanner, LinkedList} classes explicitly, as an aid to grep and to eyeballs.

Also, since you're getting ready to merge down to main, it's time to delete the System.in comment.


You chose map dimensions of x, y, which is kind of OK. Consider renaming to w, h or width, height.

You put the (0, 0) origin at top rather than bottom, which seems like it's worth a // comment. Or better, just assign to map[i][y - j].


The call to bfs omits the counter variable which the loop is very nicely maintaining. Consider changing the Public API of bfs to accept a color parameter, which would simply be the counter. This might simplify debugging and allow for more informative map analysis displays.

The idea is that, instead of merely saying "yup, this is land that Hansel & Gretel placed a breadcrumb on", we can uniquely name each island with a distinct integer, and use that at display time. This can make a big difference when you're considering maps or algorithms that might use the 4- or 8- cell neighborhood, and you're trying to identify where flood fill leaked from one region into an unexpected region.

The visited matrix would be promoted from boolean to integer.


main() is clearly doing more than one thing. Break out a pair of read_map, color_map helpers.

It is unclear why you include map reading as part of the elapsed time measurement, given that the bfs algorithm is the one of interest.


        queue.offer(new int[] { i, j });

I don't understand this line; please add a // comment.

The common idiom would be queue.addLast(), which is obviously being evaluated for side effects since it is of type void. Why do we instead call a boolean valued function and then choose to discard its return value? Is this some java checked-exception craziness where there's a burden to coping with potential exceptions?

Apparently the same thing is going on when we .poll() instead of .removeFirst().


Please declare a MANIFEST_CONSTANT for this magic character: '@'.


I see that we define an Island w.r.t. the 4-cell neighborhood. Please document that.

There's four lines of source to handle each of four directions. This is tedious. Minimally you should extract a trivial helper so we have one-line calls.

Better would be to define a vector of (delta_x, delta_y) offsets, and iterate through that.


You told me the bfs loop processes in excess of eleven million cells per second, and that is not enough. Sounds pretty quick to me, but OK.

There's a lot of allocations going on, and you didn't provide any GC stats, so it's hard to know where to drill down.

The (x, y) Point allocations are perhaps unavoidable? That is, we could have a pool of Points at the app level and recycle them, but I don't see any reason why that would be more efficient than what the JVM is doing.

The LinkedList .offer() allocations, OTOH, seem ripe for improvement. When we .poll() we're doing random read against DRAM, hoping it plays nicely with the several levels of cache. I note that you have less than 4 billion cells on the map, so a point's location can fit within 32 data bits. You're probably messing with 64-bit object pointers.

We have a queue of unknown size, which could be a valid motivation for LinkedList. I propose that we instead store the queue in an ArrayList having a small initial size, and at the app level we double its size each time it gets full (or factor of 1.3 instead of 2, whatever). It never shrinks. We maintain {front, back} integer indexes into this Circular Queue. Note that sequential raster scans of an ArrayList perform better on Intel hardware than scans of LinkedList, since fetch prediction is easy and we're asking DRAM for adjacent values so the {CAS, RAS} timing can stream them out at closer to the bus bandwidth.

I imagine that max queue depth mostly scales with map width, and can be a bit more than map width while exploring oddly shaped islands.


Here is a much crazier performance idea: read in the map lazily. That is, only read a line of input when forced to by an array de-reference.

Upon noticing that "count of processed cells" in first raster is equal to map width, output that raster and delete it, recycle the memory. The idea is to have a narrow band of the map in play at any given moment, hopefully just a fraction of the ten-thousand rasters. Of course, island shapes can work against this, especially if they are designed by an adversary.


Here's another idea, which again depends on the data pattern of typical input maps. Reduce the resolution. That is, turn a 6x2 map into a 3x1 map of cells colored according to the max() function. So if any cell in a 2x2 grid is land, the corresponding supergrid shall be land.

Solve this smaller problem with BFS flood fill, identifying N islands. Now you have N sub-problems to solve at normal resolution, each needing just a reduced memory footprint. If the technique proves helpful on typical inputs, notice that you are free to reduce resolution more than once.


Perhaps typical islands are uncooperative and they thwart those attempts to use less RAM.

Deterministically insert lines of water into your map, both horizontal and vertical. Again you have some simpler sub-problems to solve. Having done that, come back to stitch the islands back together, repairing damage done by your lines. You will want to switch from boolean to int color in order to correctly accomplish that.


Come to think of it, there's no particular reason to introduce such lines.

What I'm trying to do is get to a spot where we can conveniently use a standard pre-packaged spanning tree algorithm to find connected components of an undirected graph.

Now strictly speaking the map we initially read in is such a graph, with each node having at most a degree of 4. It just wouldn't be attractive to run Prim over that; flood fill is a better match for bulk data reduction.

But suppose we run bounded BFS flood fill, which terminates upon identifying the K-th island cell, where we have perhaps K = 1000. We're giving each pseudo-island a distinct integer identifier, and we understand that we'll have to come back to merge small pseudo-islands into giant proper islands.

Shooting a spanning tree through those nodes would be a good way to merge together each connected component.

What does this data-reduction buy us? Flexibility on problem size and memory footprint, even for extremely large input maps. We could run a JVM with low memory setting, or run on a tiny Raspberry Pi, and later come back to finish the job, stitching fragments into the larger islands required for the end product.


The code achieves many of its design objectives. We have explored avenues for improvement.

I would be willing to delegate or accept maintenance tasks on this code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.