2

I am trying to write an algorithm to check if a nonogram has been solved. I have already create multiple classes to have helper methods. The design for what I have so far is a class for Clues where the constructor looks like this:

public Clues(int[][] rowClues, int[][] colClues) { // constructor stuff }

I am storing the clues in these 2D arrays. For example, this puzzle would have the clues stored like this:

int[][] rowClues =
    new int[][] {
      new int[] {0, 2},
      new int[] {1, 2},
      new int[] {0, 3},
      new int[] {0, 3},
      new int[] {1, 1},
    };

int[][] colClues =
    new int[][] {
      new int[] {1, 1},
      new int[] {0, 1},
      new int[] {0, 3},
      new int[] {0, 3},
      new int[] {3, 1},
    };

I created another class called Puzzle that is composed of a board (for the game) and the clues. I am trying to write a function called isSolved to check if the Puzzle has been solved, but I am struggling on how to come up with an algorithm. I currently have this:

    public boolean isSolved() {

    boolean flag = false;
    for (int i=0; i<clue.getColCluesLength(); i++) {
      flag = checkColumn(getBoard().getBoardColumn(i), getClues().getColClues(i));
      if (flag == false) {
        return false;
      }
    }
    for (int i=0; i<clue.getRowCluesLength(); i++) {
      flag = checkRow(getBoard().getBoardRow(i), getClues().getRowClues(i));
      if (flag == false) {
        return false;
      }
    }
    return flag;
  }
 

  public boolean isRowSolved() {
    for (int i=0; i< clue.getRowCluesLength(); i++) {
      for (int j=0; j< clue.getRowClues(i).length; j++) {

      }
    }
    return false;
  }

The board (2D array) I have stores int's, and certain values represent elimination, space, and shaded.

I'm thinking I could compare the existing array in the board with the clues array, but I'm not sure how to exactly do that.

Some helper methods for this part are: int[] getRowClues(int index), int getColRowCluesLength(), int getWidth() (# of cells horizontally in each row of puzzle), getBoard().isShaded(), getBoard().isEliminated(), getBoard().isSpace()

Sarah
  • 33
  • 7
  • 1
    Welcome to Stack Overflow! I must say I have never heard of a nonogram before, but I enjoyed solving the one you linked! So essentially you need to scan each row and column, then when you reach a shaded space, you start counting to see how many are shaded next to each other and see if that matches up with the clue that you're currently looking at. Do you have any specific issues implementing something like that? – Henry Twist Apr 20 '21 at 14:35
  • Yes, exactly! I'm having trouble actually translating this logic into code. As you can see above I have a nested loop that goes through the row's clues (2D array) (also not sure if I did that right), and then I'm thinking to check if it's shaded by doing `if (getBoard().isShaded(i, j))`. But in there, I'm not sure how to check if it matches up with the clue because it's not like the specific indexes are important. (if clue is `1, 1`, I just need to check there are 2 cells shaded that are not next to each other) – Sarah Apr 20 '21 at 14:42
  • @Sarah To clarify: you need an algorithm to solve it or just to check if a solution is correct? – Rocco Apr 20 '21 at 14:48
  • Just to check if it is correct! – Sarah Apr 20 '21 at 14:50
  • @Sarah Ok, much easier :) you need then to scan each row / column and check if the black/white pattern matches the related clue patter, for this you need something like a state machine. An alternative idea could be to translate row/columns in a list of strings (where 1 can be black and 0 white) and clues into string patterns and use the built-in pattern matching for strings. – Rocco Apr 20 '21 at 14:59

2 Answers2

1

A solution is correct if every column and every row satisfies all the clues. Clearly, a correct solution cannot contain blank cells, that is, a cell must be either shaded or eliminated. So to validate a solution, you must iterate over every column and every row, and check whether the clues are satisfied.

A small note: the isSolved() method could be simplified to:

public boolean isSolved(){
    return isRowSolved() && isColSolved();
}

Now to check a row or a column, there are various approaches you could use. Here's one that simply constructs a new 'clue' based on a column, and then compares this new clue with the actual clue.

private boolean checkColumn(int[] column, int[] clue){
  int count = 0; // Count sequence of shaded cells
  List<Integer> newClue=new LinkedList<>();
  for(int i=0; i<column.length; i++){
    if(column[i] == SHADED){ //Found a new shaded cell, increment counter
      count++;
    }else if(column[i] == ELIMINATED){ //Found a ELIMINATED cell. If counter >0, we completed a sequence
      if(count > 0){
        newClue.add(count);
        count=0; //Reset counter
      }
    }else{
      return false; //blank cell
    }
  }
  if(count > 0) //Remember to add the last one too
    newClue.add(count);
    
  //Finally, we can check whether the given clue matches the clue constructed from the column:
  int[] newClueArray = newClue.stream().mapToInt(i->i).toArray();
  return Arrays.equals(clue, newClueArray)
}

The method above returns true if the given column satisfies the given clue, false otherwise. False is also returned if the column contains a blank cell.

Note: I don't know whether a nonogram can contain columns which do not contain any shaded cells (i.e. all cells are eliminated). If this happens, than I presume that the clue for such a column is new int[]{0}, in which case you would have to verify that newClue in the above example code is an empty list.

Joris Kinable
  • 2,232
  • 18
  • 29
  • Thank you so much for this! A few questions: I don't have a method that returns the column of a board (for the first parameter of `checkColumn()`). I'm trying to add one right now, but I'm struggling to get the column. I believe it should be return `board[index]` or something like that, but how would getting a row work in a 2D array? Also, for the line `int[] newClueArray = newClue.toArray(new int[newClue.size()]);` there's an error that says "cannot resolve method". Should I be importing something? – Sarah Apr 20 '21 at 16:18
  • 1
    My bad, `int[] newClueArray = newClue.toArray(new int[newClue.size()])` is invalid java code, I corrected my answer. See this [post](https://stackoverflow.com/questions/960431/how-to-convert-listinteger-to-int-in-java) as to why that particular line was wrong. You don't really need a method to get the column array explicitly, this was just an example. In your code, to check the j-th column, you would iterate from i=0 to the length of your board over board[i][j]. In the 2D array board[ROW][COLUMN] you get the row by indexing a row and iterating over the column, and vice versa to get a column. – Joris Kinable Apr 20 '21 at 16:46
  • Thank you! I'm still trying to wrap my head around how to use this method..I edited my question to include a modified `isSolved()` method to use `checkColumn()`. It seems faulty. I ended up making new methods to get the column/rows of the board. Would you mind looking over that? – Sarah Apr 20 '21 at 16:58
  • This is how I implemented `getBoardColumn` and `getBoardRow`: https://pastebin.com/QPY1t8SK – Sarah Apr 20 '21 at 16:59
  • 1
    I'd recommend that you experiment a bit. Simply create a 2D array, and try printing its rows and columns. I could give you the answer, but there's not much learning in that. Here's a nice [tutorial](https://www.cs.cmu.edu/~mrmiller/15-110/Handouts/arrays2D.pdf) on 2D arrays in java. After that, see whether you can modify my answer by changing `private boolean checkColumn(int[] column, int[] clue)` into `private boolean checkColumn(int columnIndex)`. You don't need to copy the column, as stated before you can simply work with `board[i][j]` directly. – Joris Kinable Apr 20 '21 at 17:19
  • After working on it for a bit, I was able to figure out a way to only take in 1 parameter! The program is half working - but I'm having some trouble with the algorithm. It always returns false since when a clue array includes a "0", it doesn't account for that and the newArray doesn't have "0" when it should have one for the array comparison at the end. So, I was able to try to get around that by prepending 0's to the linkedlist. The algorithm seems to work fine for rows, but columns is being super buggy..the algorithm should be identical, right? – Sarah Apr 20 '21 at 19:56
  • Technically, you could remove the 0's from the clue array as these 0's don't carry any information. So your first rowClues would become: int[][] rowClues = new int[][] { new int[] {2}, new int[] {1, 2}, new int[] {3}, new int[] {3}, new int[] {1, 1}, }; Alternatively, you indeed need to prepend 0's the the LinkedList as you did. The algorithm for rows is indeed identical to the algorithm for columns. In the example solution I gave, you would pass a row as argument, instead of a column. – Joris Kinable Apr 20 '21 at 20:23
  • Hmm, alright thank you. I'm not sure why my column doesn't work :( I'll have to keep testing. – Sarah Apr 20 '21 at 20:31
  • If the answer answered your question, consider upvoting & accepting the answer such that other users can see that the question has been answered, and such that others can benefit from both the question and corresponding answer. – Joris Kinable Apr 22 '21 at 18:07
1
public class Nonogram {

    public static void main(String[] args) {
        char[][] input = {
                {'W', 'W'},
                {'B', 'B'},
                {'B', 'B'},
                {'W', 'B'}
        };
        int[][] rowInstructions = {{}, {2}, {2}, {1}};
        int[][] colInstructions = {{2}, {2}};
        System.out.println(nonoChecker(input, rowInstructions, colInstructions));
    }

    public static boolean nonoChecker(char[][]input, int[][] rowInstructions,
                                   int[][] colInstructions) {
        for (int i = 0; i<rowInstructions.length; i++) {
            List<Integer> rowEncoding = rowEncoding(input, i);
            int[] encoded = rowEncoding.stream().mapToInt(c->c).toArray();
            if (!Arrays.equals(encoded, rowInstructions[i])) {
                return false;
            }
        }
        for (int i = 0; i<colInstructions.length; i++) {
            List<Integer> columnEncoding = columnEncoding(input, i);
            int[] encoded = columnEncoding.stream().mapToInt(c->c).toArray();
            if (!Arrays.equals(encoded, colInstructions[i])) {
                return false;
            }
        }
        return true;
    }
    public static List<Integer> rowEncoding(char[][] input, int row) {
        List<Integer> result = new LinkedList<>();
        int count = 0;
        for (int j = 0; j < input[row].length; j++) {
            if (input[row][j] == 'B') {
                count++;
            } else if (count > 0) {
                result.add(count);
                count = 0;
            }
        }
        if (count > 0) {
            result.add(count);
        }
        return result;
    }

    public static List<Integer> columnEncoding(char[][] input, int col) {
        List<Integer> result = new LinkedList<>();
        int count = 0;
        for (int i = 0; i < input.length; i++) {
            if (input[i][col] == 'B') {
                count++;
            } else if (count > 0) {
                result.add(count);
                count = 0;
            }
        }
        if (count > 0) {
            result.add(count);
        }
        return result;
    }

}
Coder
  • 183
  • 3
  • 16