我正在尝试解决“15 个难题”,但出现“OutOfMemoryError”

发布于 2024-09-06 11:16:33 字数 8039 浏览 8 评论 0原文

有没有一种方法可以优化此代码以免耗尽内存?

import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Stack;

public class TilePuzzle {

    private final static byte ROWS = 4;
    private final static byte COLUMNS = 4;
    private static String SOLUTION = "123456789ABCDEF0";
    private static byte RADIX = 16;

    private char[][] board = new char[ROWS][COLUMNS];
    private byte x; // Row of the space ('0')
    private byte y; // Column of the space ('0') private String representation;
    private boolean change = false; // Has the board changed after the last call to toString?

    private TilePuzzle() {
        this(SOLUTION);
        int times = 1000;
        Random rnd = new Random();
        while(times-- > 0) {
            try {
                move((byte)rnd.nextInt(4));
            }
            catch(RuntimeException e) {
            }
        }
        this.representation = asString();
    }

    public TilePuzzle(String representation) {
        this.representation = representation;
        final byte SIZE = (byte)SOLUTION.length();
        if (representation.length() != SIZE) {
            throw new IllegalArgumentException("The board must have " + SIZE + "numbers.");
        }

        boolean[] used = new boolean[SIZE];
        byte idx = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = representation.charAt(idx++);
                byte number = (byte)Character.digit(digit, RADIX);
                if (number < 0 || number >= SIZE) {
                    throw new IllegalArgumentException("The character " + digit + " is not valid.");
                } else if(used[number]) {
                    throw new IllegalArgumentException("The character " + digit + " is repeated.");
                }
                used[number] = true;
                board[i][j] = digit;
                if (digit == '0') {
                    x = i;
                    y = j;
                }
            }
        }
    }

    /**
     * Swap position of the space ('0') with the number that's up to it.
     */
    public void moveUp() {
        try {
            move((byte)(x - 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's down to it.
     */
    public void moveDown() {
        try {
            move((byte)(x + 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's left to it.
     */
    public void moveLeft() {
        try {
            move(x, (byte)(y - 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's right to it.
     */
    public void moveRight() {
        try {
            move(x, (byte)(y + 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    private void move(byte movement) {
        switch(movement) {
        case 0: moveUp(); break;
        case 1: moveRight(); break;
        case 2: moveDown(); break;
        case 3: moveLeft(); break;
        }
    }

    private boolean areValidCoordinates(byte x, byte y) {
        return (x >= 0 && x < ROWS && y >= 0 && y < COLUMNS);
    }

    private void move(byte nx, byte ny) {
        if (!areValidCoordinates(nx, ny)) {
            throw new IllegalArgumentException("(" + nx + ", " + ny + ")");
        }
        board[x][y] = board[nx][ny];
        board[nx][ny] = '0';
        x = nx;
        y = ny;
        change = true;
    }

    public String printableString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j] + " ");
            }
            sb.append("\r\n");
        }
        return sb.toString();
    }

    private String asString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j]);
            }
        }
        return sb.toString();
    }

    public String toString() {
        if (change) {
            representation = asString();
        }
        return representation;
    }

    private static byte[] whereShouldItBe(char digit) {
        byte idx = (byte)SOLUTION.indexOf(digit);
        return new byte[] { (byte)(idx / ROWS), (byte)(idx % ROWS) };
    }

    private static byte manhattanDistance(byte x, byte y, byte x2, byte y2) {
        byte dx = (byte)Math.abs(x - x2);
        byte dy = (byte)Math.abs(y - y2);
        return (byte)(dx + dy);
    }

    private byte heuristic() {
        byte total = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = board[i][j];
                byte[] coordenates = whereShouldItBe(digit);
                byte distance = manhattanDistance(i, j, coordenates[0], coordenates[1]);
                total += distance;
            }
        }
        return total;
    }

    private class Node implements Comparable<Node> {
        private String puzzle;
        private byte moves; // Number of moves from original configuration
        private byte value; // The value of the heuristic for this configuration.
        public Node(String puzzle, byte moves, byte value) {
            this.puzzle = puzzle;
            this.moves = moves;
            this.value = value;
        }
        @Override
        public int compareTo(Node o) {
            return (value + moves) - (o.value + o.moves);
        }
    }

    private void print(Map<String, String> antecessor) {
        Stack toPrint = new Stack();
        toPrint.add(SOLUTION);
        String before = antecessor.get(SOLUTION);
        while (!before.equals("")) {
            toPrint.add(before);
            before = antecessor.get(before);
        }
        while (!toPrint.isEmpty()) {
            System.out.println(new TilePuzzle(toPrint.pop()).printableString());
        }
    }

    private byte solve() {
        if(toString().equals(SOLUTION)) {
            return 0;
        }

        PriorityQueue<Node> toProcess = new PriorityQueue();
        Node initial = new Node(toString(), (byte)0, heuristic());
        toProcess.add(initial);

        Map<String, String> antecessor = new HashMap<String, String>();
        antecessor.put(toString(), "");

        while(!toProcess.isEmpty()) {
            Node actual = toProcess.poll();
            for (byte i = 0; i < 4; ++i) {
                TilePuzzle t = new TilePuzzle(actual.puzzle);
                try {
                    t.move(i);
                } catch(RuntimeException e) {
                    continue;
                }
                if (t.toString().equals(SOLUTION)) {
                    antecessor.put(SOLUTION, actual.puzzle);
                    print(antecessor);
                    return (byte)(actual.moves + 1);
                } else if (!antecessor.containsKey(t.toString())) {
                    byte v = t.heuristic();
                    Node neighbor = new Node(t.toString(), (byte)(actual.moves + 1), v);
                    toProcess.add(neighbor);
                    antecessor.put(t.toString(), actual.puzzle);
                }
            }
        }
        return -1;
    }

    public static void main(String... args) {
        TilePuzzle puzzle = new TilePuzzle();
        System.out.println(puzzle.solve());
    }
}

Is there a way that I can optimize this code as to not run out of memory?

import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Stack;

public class TilePuzzle {

    private final static byte ROWS = 4;
    private final static byte COLUMNS = 4;
    private static String SOLUTION = "123456789ABCDEF0";
    private static byte RADIX = 16;

    private char[][] board = new char[ROWS][COLUMNS];
    private byte x; // Row of the space ('0')
    private byte y; // Column of the space ('0') private String representation;
    private boolean change = false; // Has the board changed after the last call to toString?

    private TilePuzzle() {
        this(SOLUTION);
        int times = 1000;
        Random rnd = new Random();
        while(times-- > 0) {
            try {
                move((byte)rnd.nextInt(4));
            }
            catch(RuntimeException e) {
            }
        }
        this.representation = asString();
    }

    public TilePuzzle(String representation) {
        this.representation = representation;
        final byte SIZE = (byte)SOLUTION.length();
        if (representation.length() != SIZE) {
            throw new IllegalArgumentException("The board must have " + SIZE + "numbers.");
        }

        boolean[] used = new boolean[SIZE];
        byte idx = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = representation.charAt(idx++);
                byte number = (byte)Character.digit(digit, RADIX);
                if (number < 0 || number >= SIZE) {
                    throw new IllegalArgumentException("The character " + digit + " is not valid.");
                } else if(used[number]) {
                    throw new IllegalArgumentException("The character " + digit + " is repeated.");
                }
                used[number] = true;
                board[i][j] = digit;
                if (digit == '0') {
                    x = i;
                    y = j;
                }
            }
        }
    }

    /**
     * Swap position of the space ('0') with the number that's up to it.
     */
    public void moveUp() {
        try {
            move((byte)(x - 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's down to it.
     */
    public void moveDown() {
        try {
            move((byte)(x + 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's left to it.
     */
    public void moveLeft() {
        try {
            move(x, (byte)(y - 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's right to it.
     */
    public void moveRight() {
        try {
            move(x, (byte)(y + 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    private void move(byte movement) {
        switch(movement) {
        case 0: moveUp(); break;
        case 1: moveRight(); break;
        case 2: moveDown(); break;
        case 3: moveLeft(); break;
        }
    }

    private boolean areValidCoordinates(byte x, byte y) {
        return (x >= 0 && x < ROWS && y >= 0 && y < COLUMNS);
    }

    private void move(byte nx, byte ny) {
        if (!areValidCoordinates(nx, ny)) {
            throw new IllegalArgumentException("(" + nx + ", " + ny + ")");
        }
        board[x][y] = board[nx][ny];
        board[nx][ny] = '0';
        x = nx;
        y = ny;
        change = true;
    }

    public String printableString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j] + " ");
            }
            sb.append("\r\n");
        }
        return sb.toString();
    }

    private String asString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j]);
            }
        }
        return sb.toString();
    }

    public String toString() {
        if (change) {
            representation = asString();
        }
        return representation;
    }

    private static byte[] whereShouldItBe(char digit) {
        byte idx = (byte)SOLUTION.indexOf(digit);
        return new byte[] { (byte)(idx / ROWS), (byte)(idx % ROWS) };
    }

    private static byte manhattanDistance(byte x, byte y, byte x2, byte y2) {
        byte dx = (byte)Math.abs(x - x2);
        byte dy = (byte)Math.abs(y - y2);
        return (byte)(dx + dy);
    }

    private byte heuristic() {
        byte total = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = board[i][j];
                byte[] coordenates = whereShouldItBe(digit);
                byte distance = manhattanDistance(i, j, coordenates[0], coordenates[1]);
                total += distance;
            }
        }
        return total;
    }

    private class Node implements Comparable<Node> {
        private String puzzle;
        private byte moves; // Number of moves from original configuration
        private byte value; // The value of the heuristic for this configuration.
        public Node(String puzzle, byte moves, byte value) {
            this.puzzle = puzzle;
            this.moves = moves;
            this.value = value;
        }
        @Override
        public int compareTo(Node o) {
            return (value + moves) - (o.value + o.moves);
        }
    }

    private void print(Map<String, String> antecessor) {
        Stack toPrint = new Stack();
        toPrint.add(SOLUTION);
        String before = antecessor.get(SOLUTION);
        while (!before.equals("")) {
            toPrint.add(before);
            before = antecessor.get(before);
        }
        while (!toPrint.isEmpty()) {
            System.out.println(new TilePuzzle(toPrint.pop()).printableString());
        }
    }

    private byte solve() {
        if(toString().equals(SOLUTION)) {
            return 0;
        }

        PriorityQueue<Node> toProcess = new PriorityQueue();
        Node initial = new Node(toString(), (byte)0, heuristic());
        toProcess.add(initial);

        Map<String, String> antecessor = new HashMap<String, String>();
        antecessor.put(toString(), "");

        while(!toProcess.isEmpty()) {
            Node actual = toProcess.poll();
            for (byte i = 0; i < 4; ++i) {
                TilePuzzle t = new TilePuzzle(actual.puzzle);
                try {
                    t.move(i);
                } catch(RuntimeException e) {
                    continue;
                }
                if (t.toString().equals(SOLUTION)) {
                    antecessor.put(SOLUTION, actual.puzzle);
                    print(antecessor);
                    return (byte)(actual.moves + 1);
                } else if (!antecessor.containsKey(t.toString())) {
                    byte v = t.heuristic();
                    Node neighbor = new Node(t.toString(), (byte)(actual.moves + 1), v);
                    toProcess.add(neighbor);
                    antecessor.put(t.toString(), actual.puzzle);
                }
            }
        }
        return -1;
    }

    public static void main(String... args) {
        TilePuzzle puzzle = new TilePuzzle();
        System.out.println(puzzle.solve());
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

那支青花 2024-09-13 11:16:33

问题的

根本原因是您正在创建并存储在 toProcess 队列和 antecessor 映射中的大量 String 对象。你为什么要这么做?

看看你的算法。看看您是否真的需要在每个节点中存储超过 200 万个节点和 500 万个字符串。

调查

这很难发现,因为程序很复杂。实际上,我什至没有尝试理解所有代码。相反,我使用了 VisualVM – Java 分析器、采样器和 CPU/内存使用情况监视器。

我启动了它:

并查看了内存使用情况。我注意到的第一件事是(明显的)事实,您正在创建大量对象。

这是应用程序的屏幕截图:

如您所见,使用的内存量是巨大的。在短短 40 秒内,就消耗了 2 GB 空间并填满了整个堆。

死胡同

我最初认为问题与 Node 类有关,因为即使它实现了 Comparable,它也没有实现 equals >。所以我提供了方法:

public boolean equals( Object o ) {
    if( o instanceof Node ) {
        Node other = ( Node ) o;
        return this.value == other.value && this.moves == other.moves;
    }
    return false;
}

但这不是问题。

实际的问题原来是上面提到的问题。

解决方法

如前所述,真正的解决方案是重新考虑您的算法。与此同时,无论采取什么其他措施,都只会拖延问题的解决。

但解决方法可能很有用。一种是重用您生成的字符串。您非常频繁地使用 TilePuzzle.toString() 方法;这最终会经常创建重复的字符串。

由于您要生成字符串排列,因此您可以在几秒钟内创建许多 12345ABCD 字符串。如果它们是相同的字符串,则创建数百万个具有相同值的实例是没有意义的。

String.intern() 方法允许重用字符串。医生说:

返回字符串对象的规范表示。

字符串池最初为空,由 String 类私有维护。

当调用 intern 方法时,如果池中已经包含由 equals() 方法确定的等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回对此 String 对象的引用。

对于常规应用程序,使用 String.intern() 可能不是一个好主意,因为它不允许 GC 回收实例。但在这种情况下,由于无论如何您都在映射和队列中保存引用,所以这是有道理的。

所以做出这个改变:

public String toString() {
    if (change) {
        representation = asString();
    }
    return representation.intern(); // <-- Use intern
}

几乎解决了内存问题。

这是更改后的屏幕截图:

现在,即使几分钟后,堆使用量也不会达到 100 MB。

额外备注

备注 #1

您使用异常来验证移动是否有效,这是可以的;但是当你捕获它们时,你只是忽略它们:

try {
    t.move(i);
} catch(RuntimeException e) {
    continue;
}

如果你无论如何都不使用它们,那么你可以通过不首先创建异常来节省大量计算。否则,您将创建数百万个未使用的异常。

进行此更改:

if (!areValidCoordinates(nx, ny)) {
    // REMOVE THIS LINE:
    // throw new IllegalArgumentException("(" + nx + ", " + ny + ")");

    // ADD THIS LINE:
    return;
}

并改用验证:

// REMOVE THESE LINES:
// try {
//     t.move(i);
// } catch(RuntimeException e) {
//     continue;
// }

// ADD THESE LINES:
if(t.isValidMovement(i)){
    t.move(i);
} else {
    continue;
}

备注 #2

您正在为每个新的 TilePuzzle 实例创建一个新的 Random 对象。如果整个程序只使用一个会更好。毕竟,您只使用单个线程。

备注 #3

该解决方法解决了堆内存问题,但创建了另一个涉及 PermGen 的问题。我只是增加了 PermGen 大小,如下所示:

java -Xmx1g -Xms1g -XX:MaxPermSize=1g TilePuzzle

备注 #4

输出有时是 49,有时是 50。矩阵打印如下:

1 2 3 4 
5 6 7 8 
9 A B C 
D E 0 F 

1 2 3 4 
5 6 7 8 
9 A B C 
D E F 0 

... 50 次

The problem

The root cause is the tons of String objects you are creating and storing in the toProcess Queue and the antecessor Map. Why are you doing that?

Look at your algorithm. See if you really need to store >2 million nodes and 5 million strings in each.

The investigation

This was hard to spot because the program is complex. Actually, I didn't even try to understand all of the code. Instead, I used VisualVM – a Java profiler, sampler, and CPU/memory usage monitor.

I launched it:

And took a look at the memory usage. The first thing I noticed was the (obvious) fact that you're creating tons of objects.

This is an screenshot of the app:

As you can see, the amount of memory used is tremendous. In as few as 40 seconds, 2 GB were consumed and the entire heap was filled.

A dead end

I initially thought the problem had something to do with the Node class, because even though it implements Comparable, it doesn't implement equals. So I provided the method:

public boolean equals( Object o ) {
    if( o instanceof Node ) {
        Node other = ( Node ) o;
        return this.value == other.value && this.moves == other.moves;
    }
    return false;
}

But that was not the problem.

The actual problem turned out to be the one stated at the top.

The workaround

As previously stated, the real solution is to rethink your algorithm. Whatever else can be done, in the meantime, will only delay the problem.

But workarounds can be useful. One is to reuse the strings you're generating. You're very intensively using the TilePuzzle.toString() method; this ends up creating duplicate strings quite often.

Since you're generating string permutations, you may create many 12345ABCD strings in matter of seconds. If they are the same string, there is no point in creating millions of instances with the same value.

The String.intern() method allows strings to be reused. The doc says:

Returns a canonical representation for the string object.

A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals() method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

For a regular application, using String.intern() could be a bad idea because it doesn't let instances be reclaimed by the GC. But in this case, since you're holding the references in your Map and Queue anyway, it makes sense.

So making this change:

public String toString() {
    if (change) {
        representation = asString();
    }
    return representation.intern(); // <-- Use intern
}

Pretty much solves the memory problem.

This is a screenshot after the change:

Now, the heap usage doesn't reach 100 MB even after a couple of minutes.

Extra remarks

Remark #1

You're using an exception to validate if the movement is valid or not, which is okay; but when you catch them, you're just ignoring them:

try {
    t.move(i);
} catch(RuntimeException e) {
    continue;
}

If you're not using them anyway, you can save a lot of computation by not creating the exceptions in the first place. Otherwise you're creating millions of unused exceptions.

Make this change:

if (!areValidCoordinates(nx, ny)) {
    // REMOVE THIS LINE:
    // throw new IllegalArgumentException("(" + nx + ", " + ny + ")");

    // ADD THIS LINE:
    return;
}

And use validation instead:

// REMOVE THESE LINES:
// try {
//     t.move(i);
// } catch(RuntimeException e) {
//     continue;
// }

// ADD THESE LINES:
if(t.isValidMovement(i)){
    t.move(i);
} else {
    continue;
}

Remark #2

You're creating a new Random object for every new TilePuzzle instance. It would be better if you used just one for the whole program. After all, you are only using a single thread.

Remark #3

The workaround solved the heap memory problem, but created another one involving PermGen. I simply increased the PermGen size, like this:

java -Xmx1g -Xms1g -XX:MaxPermSize=1g TilePuzzle

Remark #4

The output was sometimes 49 and sometimes 50. The matrices were printed like:

1 2 3 4 
5 6 7 8 
9 A B C 
D E 0 F 

1 2 3 4 
5 6 7 8 
9 A B C 
D E F 0 

... 50 times

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文