5

I'm writing an app for a family member who is a teacher. She asked for an app to allow her to enter a bunch of kids, set their handedness, set who they can't sit next to, specify how many seats per bench, and then generate a random layout for the kids such that no left-handers sit to the right of a right-hander, and the kids that shouldn't sit next to each other are not adjacent on a bench.

This isn't quite the same problem as a generic table seating algorithm because there are 2 ends to a bench, and because there is no "value" to the nodes to create any preferential groupings.

I decided to create a directed graph where the edges represent who can sit to the right of a given kid. Then I do a recursive DFS from every node without touching a node twice until I get a path where every node has been touched. One catch is that at the "end" of every bench, anyone can sit to their "right".

This algorithm seems to always work, which is nice. But the runtime seems to grow awfully once I get beyond say 10 kids on a single bench assuming the benches can seat say 20 kids. Am I doing something wrong, or is there some much better way to solve this? Java code follows.

Edit: Sorry I didn't make this clear, but I want to achieve a RANDOM seating arrangement each time, such that the kids don't get stuck in the same place or on the same bench or next to the same kids. Also I've got my app running against this algorithm here:

http://kcraigie.com/sittychart

Currently I'm enforcing an upper limit of 1,000,000 node touches so that my server doesn't get hosed. You can see that the algorithm seems to scale properly until you set the seats per bench to 9 or so at which point it immediately becomes unwieldy.

private static class Person {
    private String m_name = null;
    private Handedness m_handedness = null;
    private Set<Person> m_nonadjacents = null;
}

private static class Node {
    private Person m_person = null;
    private List<Node> m_possibleRightNodes = null;
    private boolean m_isInPath = false;
}

private Stack<Node> generateSeatingArrangement() {
    // Generate randomized directed graph, start with all nodes as root nodes
    for(Person leftPerson: people.values()) {
        Node node = new Node(leftPerson);
        nodes.put(leftPerson, node);
    }
    // Create all edges based on constraints
    for(Node leftNode: nodes.values()) {
        List<Node> possibleRightNodes = new LinkedList<>();
        for(Node rightNode: nodes.values()) {
            Person leftPerson = leftNode.getPerson();
            Person rightPerson = rightNode.getPerson();
            if(leftNode==rightNode) {
                log.fine("Can't seat '" + leftPerson.getName() + "' next to himself");
                continue;
            }
            if(leftPerson.getHandedness()==Person.Handedness.RIGHT_HANDED &&
               rightPerson.getHandedness()==Person.Handedness.LEFT_HANDED) {
                log.fine("Can't seat right-handed '" + leftPerson.getName()
                         + "' to the left of left-handed '" + rightPerson.getName() + "'");
                continue;
            }
            if(leftPerson.getNonadjacents().contains(rightPerson)) {
                log.fine("Can't seat '" + leftPerson.getName() + "' next to '" + rightPerson.getName() + "'");
                continue;
            }
            if(rightPerson.getNonadjacents().contains(leftPerson)) {
                // TODO: This should be redundant but not enforcing right now...
                log.fine("Can't seat '" + rightPerson.getName() + "' next to '" + leftPerson.getName() + "'");
                continue;
            }
            log.fine("Can seat '" + leftPerson.getName() + "' to the left of '" + rightPerson.getName() + "'");
            possibleRightNodes.add(rightNode);
        }
        Collections.shuffle(possibleRightNodes);
        leftNode.setPossibleRightNodes(possibleRightNodes);
    }
    List<Node> nodes2 = new LinkedList<>(nodes.values());
    Collections.shuffle(nodes2);

    // Perform recursive graph traversal
    Stack<Node> pathStack = new Stack<>();
    for(Node node: nodes2) {
        TraversalStatistics stats = new TraversalStatistics();
        boolean isPathFound = depthFirstSearchRecur(numSeatsPerBench, nodes2, pathStack, node, stats);
        if(isPathFound) {
            break;
        }
        pathStack.clear();
    }
}

// The resursive DFS method
private boolean depthFirstSearchRecur(int numSeatsPerBench,
                                      List<Node> allNodes,
                                      Stack<Node> pathStack,
                                      Node node,
                                      TraversalStatistics stats) {
    stats.numNodesTouched++;
    if(node.isInPath()) {
        stats.numLeavesReached++;
        return false;
    }
    pathStack.push(node);
    node.setIsInPath(true);
    if(pathStack.size() >= allNodes.size()) {
        return true; // We win!
    }
    if(pathStack.size() % numSeatsPerBench == 0) {
        // "End" of a bench, anyone can "sit to the right of" me
        for(Node node2: allNodes) {
            if(node == node2) {
                // Can't sit next to myself
                continue;
            }
            if(depthFirstSearchRecur(numSeatsPerBench, allNodes, pathStack, node2, stats)) {
                return true;
            }
        }
    } else {
        for(Node node2: node.getPossibleRightNodes()) {
            if(depthFirstSearchRecur(numSeatsPerBench, allNodes, pathStack, node2, stats)) {
                return true;
            }
        }
    }
    pathStack.pop();
    node.setIsInPath(false);
    return false;
}
kcraigie
  • 1,252
  • 6
  • 13
  • 1
    So the rules are (1) lefties cannot sit immediately to the right of righties, (2) righties cannot sit immediately to the left of lefties, (3) blacklisted pairs cannot sit next to each other. Any other rules? – Jonathan M Oct 28 '15 at 18:25
  • 1
    First, assign the left handed kids to the left side of the benches. Next, assign the blacklisted kids to the left side of the benches. Finally, assign the remaining kids and remaining blacklisted kids to the remaining bench seats. – Gilbert Le Blanc Oct 28 '15 at 18:51
  • @JonathanM Yes you are correct, and the additional rule is that anyone can sit to the "right" of someone at the end of the bench (meaning they can start the next bench) – kcraigie Oct 28 '15 at 20:34
  • @GilbertLeBlanc Unfortunately I'm going for a truly random assignment each time, rather than forcing the lefties to always start a bench, and without alienating the kids who can't sit next to most people by always putting them on an emptier bench – kcraigie Oct 28 '15 at 20:35
  • 1
    @kcraigie, so you're looking for something that is both efficient *and* random? – Jonathan M Oct 28 '15 at 21:26
  • @JonathanM Yes... are my hopes unrealistic? – kcraigie Oct 28 '15 at 22:42
  • 1
    @kcraigie, no, just wanted to clarify. Worst case is that there are no solutions for the data set, and you have to go through all permutations to discover that. – Jonathan M Oct 29 '15 at 17:45
  • @JonathanM Yeah, I was just wondering if there was any awful flaw in my algorithm but I think I'm looking necessarily at a brute-force traversal with a pretty bad worst case. – kcraigie Oct 29 '15 at 18:03

2 Answers2

1

I think you don't need to generate a list of possible left Nodes for each node. Instead, do it on the fly in an recursive algorithm with trackback.

On the first seat you seat the first node. Search through the list of nodes for the first possible neighbour and seat him there. Repeat until the plan is complete or you reach a dead end. In that case, go back one seat and seat the next possible person there (eg. if it was Node 4 change it to Node 5). Either try the next seat from there or, if not possible to find a next Node (e.g. the node there was already the last on the list) go back one step until there is a next node.

With this method, you should statistically have to compute n!/2 possibilities for a number of students n to find an answer.

PS: I hope you understand my descrition of the algorithm. If i had more time i would have made a diagram.

Gumbo
  • 1,716
  • 1
  • 15
  • 22
  • Thanks for the reply. That's essentially what my algorithm already does, but you're right in that it generates the graph first -- mainly so that I don't have to put the traversal information in the same class. But generating the graph takes a negligible amount of time. – kcraigie Oct 28 '15 at 20:25
1

I wouldn't automatically think graphs for this sort of problem. Building the graph gives a complexity of O(n^2), and a DFS is O(V+E) (with V being verticies, E being edges, which will be less than O(n^2), so you're still looking at O(n^2)).

If no left hander can be directly to the right of a right hander, then for a single bench, there's no way to order the kids so that any right hander is to the right at all of any left hander. So your result is always going to look something like:

l l l l l r r r r r

You can prove this by induction. If the correct solution has a right hander anywhere to the left of a left hander, i.e :

r ? l

Then there must be a kid '?' who is either left or right handed that doesn't break the rule. If the kid is right handed, it breaks it for the first left handed kid. If the kid is left handed, it breaks the rule for that kid.

So, what you need to do is sort your left and right handed kids, and look at who can and can't sit next to each other. In reality, if the kids relationships aren't like an episode of the bold and the beautiful, and you don't have a crazy graph of who can and can't sit next to each other, you could probably use a greedy algorithm. You would just start from the list of left handers, pulling out one kid at a time. If they won't sit next to the last one, then pull out the next, and try again. This would give you a complexity of O(n), but you might not get a solution everytime.

Alternatively, you could use a recursive algorithm to put together compatible groups of kids one pair at a time, and build up a solution that way. This would obviously be a more complex algorithm, but you'd always get an answer.

If you've got more than one bench though, then this won't work at all.

AndyN
  • 2,075
  • 16
  • 25
  • Thanks for the insight. I agree with your assessment that if I make certain assumptions and take certain shortcuts I can avoid the complexity of the graph traversal. But first off, I need multiple benches. Second, I want a truly random seating assignment each time, rather than anyone getting an artificial affinity to any particular position which is why I shuffle the nodes and edges before I do the traversal. – kcraigie Oct 28 '15 at 20:30
  • 1
    Those are really important requirements, you should add them to the question. – AndyN Oct 28 '15 at 21:28
  • Sorry about that, you're right -- I clarified the title and the requirements, and added a link to the current running implementation of the app – kcraigie Oct 28 '15 at 22:41