Saturday, August 31, 2013

A General Algorithm for Creating a Tree Structure from a String with Nested Parentheses


Suppose there's a tree structure MyTree, which contains three fields: Integer id, String value, and List<MyTree> children. When constructing a MyTree object with constructor "MyTree(Integer id, String value)", the id parameter should not be null or negative, and the value parameter should contain only English letters or numbers. The toString() public method of MyTree returns a String that contains all available information about the MyTree object and has a pattern like this:


(1:AA(..child 1..)(..child 2..)(..child 3..)...(..child n..))

For example, 

"(1:AA(2:BB)(3:CC(31:wef(52:huEf)(35:weDf(554:Q)))(43:f34w(42:wefw)))(4:ge(43:werw))(5:zer(99:f5)(100:f5)))"

is the tree string of a MyTree object (the return value of the "public String toString()" method in class MyTree) with id 1, value AA, and four MyTree children with id 2, 3, 4, 5 respectively.


The problem is: how to create a tree structure from a string with nested parentheses? In other words, how to convert this kind of tree strings back to objects with MyTree data structure?

In addition, there may be spaces inserted between parentheses, values, ids, and colons that do not damage the validity of the tree string. For example, "(1:AA(65:wete))" and "(  1  : AA(65 :  wete  )  )" are converted to the same MyTree object.


This problem seems quite difficult when first encountered, since the tree structure can't be constructed with just a few regular expressions. The following is my implementation of an algorithm for creating the tree structure from a tree string with nested parentheses. You can download the codes from http://sites.google.com/site/moderntone/MyStringParser.zip






MyTree.java


package com.treeparse;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MyTree {

 private Integer id;
 private String value;
 private List<MyTree> children;
 
 public MyTree(Integer id, String value){
  if (id == null || id < 0)
   new Exception("Error: The id should be non-negative.").printStackTrace();
  Pattern pattern = Pattern.compile("[^a-zA-Z0-9]");
  Matcher matcher = pattern.matcher(value);
  if (matcher.find())
   new Exception("Error: The value should contain only English letters or numbers.").printStackTrace();
  this.id = id;
  this.value = value;
  children = new ArrayList<MyTree>();
 }

 public String getValue() {
  return value;
 }
 public void setValue(String value){
  this.value = value;
 }
 
 public int getId(){
  return id;
 }
 public void setId(int id){
  this.id = id;
 }
 
 public List<MyTree> getChildren() {
  return children;
 }

 public MyTree getChild(int i){
  return children.get(i);
 }
 
 public boolean isLeaf() {
  return children.size() == 0;
 }
 
 public static MyTree valueOf(String treeString){
  MyParser mp = new MyParser();
  return mp.parse(treeString);
 }
 
 @Override
 public String toString(){
  StringBuilder builder = new StringBuilder("(" + id + ":" + value);
  for (MyTree child : children)
   builder.append(child);
  return builder.append(")").toString();
 }
}


MyTokenizer.java

package com.treeparse;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MyTokenizer {
 
 private String treeString;
 protected boolean isInvalid;
 
 protected String value;
 protected Integer id; 
 protected List<String> childUnits;
 
 public MyTokenizer(String treeString) {
  this.treeString = treeString.trim();
  tokenize();
 }
 
 private void tokenize(){
  try {
   setId();
   setValue();
   setChildUnits();
  } catch (Exception e) {
   System.out.println("MyTokenizer.tokenize() Exception e = " + e);
   isInvalid = true;
   return;
  }
 }
 
 private void setId(){
  try {
   Pattern pattern = Pattern.compile("(?<=\\() *\\d+ *(?=:)"); 
   Matcher matcher = pattern.matcher(treeString);
   if (matcher.find()){
    id = Integer.parseInt(matcher.group().trim());
    return;
   }
  } catch (Exception e) {
   System.out.println("MyTokenizer.setId() Exception e = " + e);
  }
  isInvalid = true;
 }
 
 private int tempIndex;
 private void setValue(){
  try {
   Pattern pattern = Pattern.compile("(?<=:) *[a-zA-Z0-9]+ *[\\(\\)]"); 
   Matcher matcher = pattern.matcher(treeString);
   if (matcher.find()){
    tempIndex = treeString.indexOf(matcher.group()) + matcher.group().length() - 1;
    value = matcher.group().substring(0, matcher.group().length() - 1).trim();
    return;
   }
  } catch (Exception e) {
   System.out.println("MyTokenizer.setValue() Exception e = " + e);
  }
  isInvalid = true;
 }
 

 /**
  * Examine treeString's parentheses from leftParenthesis to treeString.length() - 1
  * @param leftParenthesis
  */
 private void setChildUnits(){
  this.childUnits = new ArrayList<String>();
  int cv = 0;
  Integer tempStart = null;
  for (int i = tempIndex; i < treeString.length() - 1; i ++){
   switch (treeString.charAt(i)) {
   case '(':
    if (++cv == 1)
     tempStart = i;
    break;
   case ')':
    if (--cv == 0){
     childUnits.add(treeString.substring(tempStart, i+1));
     tempStart = null;
    }
    break;
   }
  }
  if (tempStart != null)
   isInvalid = true;
 }
}


MyParser.java

package com.treeparse;

import java.util.List;

public class MyParser {

 public MyParser() {
  
 }

 public MyTree parse(String treeString){
  MyTokenizer tokenizer = new MyTokenizer(treeString);
  if (tokenizer.isInvalid){
   System.err.println("warning: the input is not a valid MyTree string.");
   return null;
  }
  List<String> childUnits = tokenizer.childUnits;
  MyTree myTree = new MyTree(tokenizer.id, tokenizer.value);
  List<MyTree> children = myTree.getChildren();
  for (String childUnit : childUnits)
   children.add(parse(childUnit));
  return myTree;
 }
 
 public static void main(String[] args) {
  String testString = "(1 : AA (2 : BB) (3: wef (56:3t4) (6:4g3))  )";
  testString = "(1:AA(2:BB)(3:CC(31:wef(52:huEf)(35:weDf(554:Q)))(43:f34w(42:wefw)))(4:ge(43:werw))(5:zer(99:f5)(100:f5)))";
  MyTree tree = MyTree.valueOf(testString);
  System.out.println(tree);

  for (MyTree myTree : tree.getChildren()){
   System.out.println(myTree.getId() + " with tree string: " + myTree);
  }
 }
 
}


Key point of the algorithm: 
Identify the children tree strings of the root, and parse them recursively to create the whole tree structure



A  key point of the codes below is in setChildUnits() method in MyTokenizer.java. The method determines tree string's sub-strings that represent the children of the root, and the MyParser parses them recursively until all tree strings as leaves are parsed and the whole tree are constructed. 


The idea behind the method is not complex. The program first identifies the start and end indexes of the sub-string about the part of all children tree strings and then keeps track of the parentheses with the value of (number of left parentheses) - (number of right parentheses), stored in local variable cv. 

If cv is 1 when a left parenthesis is checked, it is the start parenthesis of a child tree string; if cv is 0 when a right parenthesis is checked, it is the end parenthesis of a child tree string. This claim can be easily proved. Suppose the tree string is a valid one, then left parentheses and right parentheses must occur  pairwise.Thus, the cv value of the start parenthesis of any child is 1 + n - n = 1, and cv of the end parenthesis of any child is obviously constantly zero. For all left parentheses beside the start parentheses of children, the cv value is always larger than 1, since 1 + n - m > 1 and m < n; for all right parentheses beside the end parentheses of children, cv is m + 0 with m = 1 or more and thus always larger than 0. With this idea, the start and end parentheses of all children tree strings are identified.


The concept of this implementation can similarly be applied to all kinds of tree structures with multiple fields and tree strings with different patterns. There are three major classes: a tree class, and tokenizer class, and a parser class. The tree class represents the tree structure; the tokenizer classes examines the tree string to obtain information about all fields and the first-generation children; the parser class recursively parses the tree strings until the tree structure is constructed.


Monday, May 20, 2013

Implementation of the AVL Tree in Java


In computer science, an AVL tree is a self-balancing binary search tree. In an AVL tree, the heights of the two child sub-trees of any node differ by at most one; if at any time they differ by more than one, rebalancing is done to restore this property.

The following is my implementation of an AVL tree Java library, which supports methods for insertion, deletion, and look-up. The data structure is simple. AVLNode has the following fields:

protected AVLNode left, right, parent;
protected Integer value, height;
private Object object; //An additional field for data to be stored in an AVLNode

And an AVLTree object represents an AVL tree composed of AVLNode objects.

You can download the Java library from http://sites.google.com/site/moderntone/AVLTree.zip




AVLTree.java

package com.avltree;

public class AVLTree {

 private AVLNode root;

 public AVLTree(int... values) {
  for (int value : values)
   insert(value);
 }

 public AVLNode getRoot() {
  return root;
 }

 public void insert(AVLNode node){
  if (root == null)
   root = node;
  else {
   root.insertToLeaf(node);
   AVLNode keyNode = updateHeightsAndDetectKeyNode(node);
   if (keyNode != null) // rotate to adjust the tree
    adjustTreeByRotation(keyNode);
  }
 }
 
 private void insert(int value){
  if (root == null)
   root = new AVLNode(value);
  else {
   AVLNode newNode = new AVLNode(value);
   root.insertToLeaf(newNode);
   AVLNode keyNode = updateHeightsAndDetectKeyNode(newNode);
   if (keyNode != null) // rotate to adjust the tree
    adjustTreeByRotation(keyNode);
  }
 }

 public void delete(AVLNode node){
  int value = node.value;
  delete(value);
 }
 
 public void delete(int value) {
  AVLNode parentOfDeletedLeaf = deleteBeforeAdjustment(value);
  if (parentOfDeletedLeaf != null) {
   AVLNode keyNode = detectKeyNode(parentOfDeletedLeaf);
   if (keyNode != null){
    AVLNode newkeyNode = adjustTreeByRotation(keyNode);
    updateHeights(newkeyNode.parent);
   }
  } else {
   System.out.println("The AVLTree doesn't contain " + value);
  }
 }

 private AVLNode detectKeyNode(AVLNode parentOfDeletedLeaf){
  AVLNode currentNode = parentOfDeletedLeaf;
  while (currentNode != null) {
   int bf = currentNode.getBalanceFactor();
   if (bf == 2 || bf == -2)
    return currentNode;
   else 
    currentNode = currentNode.parent;
  }
  return null;
 }
 
 private AVLNode deleteBeforeAdjustment(int value) {
  AVLNode currentNode = root;
  while (currentNode != null) {
   if (currentNode.value == value)
    break;
   else
    currentNode = value < currentNode.value ? 
     currentNode.left : currentNode.right;
  }

  if (currentNode != null) {
   while (!currentNode.isLeaf()) {
    AVLNode replacement = currentNode.getBalanceFactor() < 0 ? 
     currentNode.right.getLeftmost() : currentNode.left.getRightmost();
    currentNode.value = replacement.value;
    currentNode = replacement;
   }

   AVLNode parent = currentNode.getParent();
   if (parent == null) root = null;
   else if (currentNode == parent.left) 
    parent.setLeft(null);
   else 
    parent.setRight(null);
   updateHeights(parent);
   return parent;
  }
  return null;
 }

 private void updateHeights(AVLNode fromParentOfDeletedLeaf){
  AVLNode currentNode = fromParentOfDeletedLeaf;
  currentNode.adjustHeight();
  while (currentNode != null){
   currentNode.adjustHeight();
   currentNode = currentNode.parent;
  }
 }
 
 /**
  * called by insert(int) keyNode: the node closest to the newly inserted
  * node where |BF|>1
  * @param newNode : newly added leaf AVLNode
  * @return keyNode
  */
 private AVLNode updateHeightsAndDetectKeyNode(AVLNode newNode) {
  AVLNode keyNode = null;
  while (newNode.parent != null) {
   if (newNode.getParent().height - newNode.height != 1) {
    if (keyNode == null) {
     int bf_parent = newNode.getParent().getBalanceFactor();
     if (bf_parent > 1 || bf_parent < -1) {
      keyNode = newNode.getParent();
      break;
     }
    }
    newNode.getParent().height++;
    newNode = newNode.getParent();
   } else
    break;
  }
  return keyNode;
 }

 public AVLNode lookup(int value) {
  AVLNode currentNode = root;
  while (currentNode != null) {
   if (currentNode.value == value)
    return currentNode;
   else
    currentNode = value < currentNode.value ? 
     currentNode.left : currentNode.right;
  }
  System.out.println("The AVLTree doesn't contain " + value);
  return null;
 }

 /**
  * LL or LR type if balance factor == 2; rotateRight for keyNode if bf of
  * keyNode.left == -1, it's LR type; rotateLeft for keyNode.left first RR or
  * RL type if balance factor == -2; rotateLeft for keyNode if bf of
  * keyNode.right == 1, it's RL type; rotateRight for keyNode.right first
  * 
  * @param keyNode
  */
 private AVLNode adjustTreeByRotation(AVLNode keyNode) {
  AVLNode newKeyNode = null;
  int bf_keyNode = keyNode.getBalanceFactor();
  if (bf_keyNode == 2) {
   if (keyNode.left.getBalanceFactor() == -1) // LR
    keyNode.setLeft(keyNode.left.rotateLeft());
   newKeyNode = keyNode.rotateRight();
  } else if (bf_keyNode == -2) {
   if (keyNode.right.getBalanceFactor() == 1) // RL
    keyNode.setRight(keyNode.right.rotateRight());
   newKeyNode = keyNode.rotateLeft();
  } else {
   new Exception("There are some bugs").printStackTrace();
  }

  if (keyNode.parent == null) {
   root = newKeyNode;
   root.parent = null;
  }
  else {
   if (keyNode == keyNode.parent.left)
    keyNode.parent.setLeft(newKeyNode);
   else
    keyNode.parent.setRight(newKeyNode);
   newKeyNode.parent.adjustHeight();
  }
  return newKeyNode;
 }

 public void print(Order order) {
  switch (order) {
  case PREORDER:
   root.print_preorder();
   break;
  case INORDER:
   root.print_inorder();
   break;
  case POSTORDER:
   root.print_postorder();
   break;
  }
 }
}


AVLNode.java

package com.avltree;

public class AVLNode {

 protected AVLNode left, right, parent;
 protected Integer value, height;
 private Object object; //enable the AVLTree to store additional info
 
 public AVLNode(int value){
  this.value = value;
  this.height = 0;
 }
 
 public AVLNode(int value, Object object){
  this.value = value;
  this.height = 0;
  this.object = object;
 }
 
 public AVLNode(AVLNode node){
  this.value = node.value;
  this.height = node.height;
  this.left = node.left;
  this.right = node.right;
 }
 
 public Object getObject() {
  return object;
 }

 public void setObject(Object object) {
  this.object = object;
 }
 
 public int getValue(){
  return value;
 }
 
 public AVLNode getParent() {
  return parent;
 }
 
 public AVLNode getLeft() {
  return left;
 }
 
 protected void setLeft(AVLNode left){
  this.left = left;
  if (left != null)
   left.parent = this;
 }
 
 public AVLNode getRight() {
  return right;
 }
 
 protected void setRight(AVLNode right){
  this.right = right;
  if (right != null)
   right.parent = this;
 }
 
 public int getHeight() {
  return height;
 }
 
 public int getLevel(){
  int level = 0;
  AVLNode currentNode = this;
  while ((currentNode = currentNode.parent) != null)
   level++;
  return level;
 }
 
 protected int getBalanceFactor(){
  int leftHeight = getLeftHeight();
  int rightHeight = getRightHeight();
  return leftHeight - rightHeight;
 }
 
 protected void insertToLeaf(AVLNode node){
  if (node.value == value){
   System.out.println("Duplicate node " + value);
   return;
  }
  else {
   if (node.value < value){
    if (left == null)   setLeft(node);
    else left.insertToLeaf(node);
   }
   else {
    if (right == null) setRight(node);
    else right.insertToLeaf(node);
   }
  }
 }
 
 
 /**rotate right
  * change of height should be added;
  * applies to the LL type situation 
  */
 protected AVLNode rotateRight(){
  AVLNode newRight = new AVLNode(this);
  newRight.height = getRightHeight() + 1;
  newRight.setLeft(left.right);
  left.setRight(newRight);
  left.adjustHeight();
  return left;
 }

 /**
  * rotate left
  * change of height should be added;
  * applies to the LL type situation 
  */
 protected AVLNode rotateLeft(){
  AVLNode newLeft = new AVLNode(this);
  newLeft.height = getLeftHeight() + 1;
  newLeft.setRight(right.left);
  right.setLeft(newLeft);
  right.adjustHeight();
  return right;
 }
 
 protected void adjustHeight(){
  int leftHeight = getLeftHeight();
  int rightHeight = getRightHeight();
  height = (leftHeight > rightHeight) ? leftHeight + 1 : rightHeight + 1;
 } 
 
 protected int getLeftHeight(){
  return left == null ? -1 : left.height; 
 }
 
 protected int getRightHeight(){
  return right == null ? -1 : right.height;
 }
 
 protected boolean isLeaf(){
  return left == null && right == null;
 }
 
 protected AVLNode getLeftmost(){
  AVLNode leftmost = this;
  while (leftmost.left != null)
   leftmost = leftmost.left;
  return leftmost;
 }
 
 protected AVLNode getRightmost(){
  AVLNode rightmost = this;
  while (rightmost.right != null)
   rightmost = rightmost.right;
  return rightmost;
 }
 
 
 //////////
 protected void print_preorder(){
  System.out.print(value + " ");
  if (left != null) left.print_preorder();
  if (right != null) right.print_preorder();
 }

 protected void print_inorder(){
  if (left != null) left.print_inorder();
  System.out.print(value + " ");
  if (right != null) right.print_inorder();
 }
 
 protected void print_postorder(){
  if (left != null) left.print_postorder();
  if (right != null) right.print_postorder();
  System.out.print(value + " ");
 }
}

Order.java

package com.avltree;

public enum Order{
 PREORDER, INORDER, POSTORDER
}


Test.java

package com.test;

import com.avltree.*;

public class Test {

 public static void main(String[] args) {

//  int[] values = new int[] {23, 18, 12, 8, 14, 20, 44, 52 };
  
  AVLTree tree = new AVLTree(23,18,44,12,20,52,4,14,16); //LR
//  AVLTree tree = new AVLTree(18,12,44,23,52,20,20); //RL 
//  AVLTree tree = new AVLTree(18,20,12,14,8,4); //LL
//  AVLTree tree = new AVLTree(14,12,20,18,23,44); //RR
//  AVLTree tree = new AVLTree(23,18,12);   //simple LL
  
//  AVLTree tree = new AVLTree(50, 20, 80, 10, 30, 60, 90, 70); //test delete
  
  AVLNode root = tree.getRoot();
  System.out.println(root.getValue() + ", with height " + root.getHeight());
  System.out.println(root.getLeft().getValue() + ", with height " + root.getLeft().getHeight());
  System.out.println(root.getRight().getValue() + ", with height " + root.getRight().getHeight());

  System.out.println(root.getLeft().getLeft().getValue() + ", with height " + root.getLeft().getLeft().getHeight());
  System.out.println(root.getRight().getRight().getValue() + ", with height " + root.getRight().getRight().getHeight());

  int toBeDeleted = 90;
  System.out.println("After deleting " + toBeDeleted);
  tree.delete(toBeDeleted);
  System.out.println(root.getValue() + ", with height " + root.getHeight());
  System.out.println(root.getRight().getValue() + ", with height " + root.getRight().getHeight());
//  System.out.println(root.getRight().getRight().getValue() + ", with height " + root.getRight().getRight().getHeight());
//  System.out.println(root.getRight().getLeft().getValue() + ", with height " + root.getRight().getLeft().getHeight());
//  System.out.println(root.getLeft().getRight() == null);
  
  tree.print(Order.PREORDER);

 }
}

Saturday, May 11, 2013

An Implementation of the Tic-Tac-Toe Artificial Intelligence in Java


This is my Java implementation of the Tic-Tac-Toe artificial intelligence. The first player, O, is the user, and the second player, X, is the computer.

The algorithm is simple and effective but may not be entirely perfect. The heuristic rules are simply grounded on those basic strategies I put together for playing Tic-Tac-Toe games and presented in the private int computeHeuristicScoreAt(int position) method in ArtificialIntelligence.java below.

You can download the Java source codes in http://sites.google.com/site/moderntone/TicTacToe.zip and the executable jar file along with the two images in http://sites.google.com/site/moderntone/TicTacToeExecutableJar.zip









ArtificialIntelligence.java

package com.tictactoe;

import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;

public class ArtificialIntelligence extends MouseAdapter{
 
 /** Indexes of the nine positions:
  * 0 1 2
  * 3 4 5
  * 6 7 8 
  */
 private int[] situation; // length = 9; 0: empty; 1: O; 2: X private JTable table;
 private Integer[] heuristicScoresAndPositions;
 
 public ArtificialIntelligence(JTable table){
  this.table = table;
  situation = new int[9];
  
 }
 
 private static final HashMap<Integer, List<Integer>> linesMap;
 
 static {
  linesMap = new HashMap<Integer, List<Integer>>();
  
  List<Integer> lines = new ArrayList<Integer>();
  lines.add((1 << 4) + 2); lines.add((3 << 4) + 6);
  lines.add((4 << 4) + 8); 
linesMap.put(0, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 2); lines.add((4 << 4) + 7);
  linesMap.put(1, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 1); lines.add((4 << 4) + 6);
  lines.add((5 << 4) + 8);
  linesMap.put(2, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 6); lines.add((4 << 4) + 5);
  linesMap.put(3, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 8); lines.add((2 << 4) + 6);
  lines.add((1 << 4) + 7); lines.add((3 << 4) + 5);
  linesMap.put(4, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((2 << 4) + 8); lines.add((3 << 4) + 4);
  linesMap.put(5, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 3); lines.add((2 << 4) + 4);
  lines.add((7 << 4) + 8);
  linesMap.put(6, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((1 << 4) + 4); lines.add((6 << 4) + 8);
  linesMap.put(7, lines);
  
  lines = new ArrayList<Integer>();
  lines.add((0 << 4) + 4); lines.add((2 << 4) + 5);
  lines.add((6 << 4) + 7);
  linesMap.put(8, lines);
 }
 
 private boolean checkWhetherTheCurrentPlayerWins(int position, boolean byUser){
  List<Integer> possibleLines = linesMap.get(position);
  for (Integer anotherTwoPositions : possibleLines){
   int p1 = anotherTwoPositions >> 4, p2 = anotherTwoPositions & 0xf;
   if (byUser){
    if (situation[p1] * situation[p2] == 1) 
     return true;
   }
   else 
    if (situation[p1] + situation[p2] == 4) 
    return true;
  }
  return false;
 }
 
 @Override
 public void mousePressed(MouseEvent e) {
  boolean notEnded = playByUser(e);
  try {
   Thread.sleep(100);
  } catch (InterruptedException e1) {
   e1.printStackTrace();
  }
  if (notEnded)
   playByComputer();
 }

 private void computeHeuristicScores(){
  heuristicScoresAndPositions = new Integer[9]; //score << 8 + row << 4 + column 
  for (int i = 0 ; i < 9 ; i++){
   heuristicScoresAndPositions[i] = (situation[i] > 0) ? 
     i : computeHeuristicScoreAt(i) + i; 
  }
 }
 
 private int computeHeuristicScoreAt(int position){
  List<Integer> possibleLines = linesMap.get(position);
  
  int h = 0;
  for (Integer line : possibleLines){
   int p1 = line >> 4, p2 = line & 0xf;
   int zeroCount = 0, oneCount = 0, twoCount = 0;
   
   switch (situation[p1]) {
   case 0:  zeroCount++; break;
   case 1:  oneCount++; break;
   default: twoCount++; break;
   }
   switch (situation[p2]) {
   case 0:  zeroCount++; break;
   case 1:  oneCount++; break;
   default: twoCount++; break;
   }
   
   if (twoCount == 2)
    return 1 << 20;
   else if (oneCount == 2)
    h += 1 << 16;
   else {
    if (zeroCount == 1 && twoCount == 1)
     h += 1 << 12;
    else if ( zeroCount * oneCount == 1)
     h += 1 << 10;
    else if (zeroCount == 2)
     h += 1 << 9;
    else {
     h += 1 << 6;
    }
   }
  }
  return h;
 }
 
 private void playByComputer(){
  computeHeuristicScores();
  Arrays.sort(heuristicScoresAndPositions);
  int thePosition = heuristicScoresAndPositions[8] & 0xf;
  if (situation[thePosition] > 0){
   JOptionPane.showMessageDialog(null, "This game is ended in a draw.");
   restart();
   return;
  }
  situation[thePosition] = 2;
  ImageIcon OorXIcon = new ImageIcon("images/x.png");
  Image img = OorXIcon.getImage();
  Image newImg = img.getScaledInstance(90, 90,
    java.awt.Image.SCALE_SMOOTH);
  DefaultTableModel tableModel = (DefaultTableModel) table.getModel();
  tableModel.setValueAt(new ImageIcon(newImg), thePosition / 3, thePosition % 3);
  if (checkWhetherTheCurrentPlayerWins(thePosition, false)){
   JOptionPane.showMessageDialog(null, "Congratuations! Player X Wins.");
   restart();
   return;
  }
 }
 
 private boolean playByUser(MouseEvent e){
  int column = table.columnAtPoint(e.getPoint());
  int row = table.rowAtPoint(e.getPoint());
  if (table.getValueAt(row, column) != null)
   return false;
  int position = row * 3 + column;
  situation[position] = 1;
  ImageIcon OorXIcon = new ImageIcon("images/o.png");
  Image img = OorXIcon.getImage();
  Image newImg = img.getScaledInstance(90, 90,
    java.awt.Image.SCALE_SMOOTH);
  DefaultTableModel tableModel = (DefaultTableModel) table.getModel();
  tableModel.setValueAt(new ImageIcon(newImg), row, column);
  if (checkWhetherTheCurrentPlayerWins(position, true)){
   JOptionPane.showMessageDialog(null, "Congratuations! Player O Wins.");
   restart();
   return false;
  }
  return true;
 }
 
 public void restart(){
  for (int i = 0; i < 3; i++) {
   for (int j = 0; j < 3; j++)
    table.setValueAt(null, i, j);
  }
  situation = new int[9];
 }
}



ImageRenderer.java


package com.tictactoe;

import java.awt.Component;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;

public class ImageRenderer extends DefaultTableCellRenderer {
 
 private static final long serialVersionUID = 1L;
 JLabel lbl = new JLabel();
 public Component getTableCellRendererComponent(JTable table,
   Object value, boolean isSelected, boolean hasFocus, int row,
   int column) {
  lbl.setIcon((ImageIcon) value);
  return lbl;
 }
}

TictacToe.java

package com.tictactoe;

import javax.swing.*;
import java.awt.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.table.DefaultTableModel;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class TicTacToe extends JFrame {

 private static final long serialVersionUID = 1L;
 private JPanel contentPane;
 private static JTable table;
 private ArtificialIntelligence ai;
 
 public static void main(String[] args) {
  EventQueue.invokeLater(new Runnable() {
   public void run() {
    try {
     TicTacToe frame = new TicTacToe();
     frame.setVisible(true);
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  });
 }

 private void setTable() {
  final Object[][] tableItems = new Object[][] { { null, null, null },
    { null, null, null }, { null, null, null }, };
  table = new JTable();
  table.setGridColor(new Color(255, 0, 0));

  ai = new ArtificialIntelligence(table);
  table.addMouseListener(ai);
  table.setModel(new DefaultTableModel(tableItems, new String[] { "0",
    "1", "2" }) {
   private static final long serialVersionUID = 1L;
   @Override
   public boolean isCellEditable(int row, int column) {
    return false;
   }
  });

  table.setBackground(new Color(153, 255, 255));
  table.setBorder(new EtchedBorder(EtchedBorder.RAISED, new Color(107,
    142, 35), null));
  table.setBounds(89, 70, 270, 270);
  for (int i = 0; i < 3; i++) {
   table.setRowHeight(i, 90);
   table.getColumnModel().getColumn(i)
     .setCellRenderer(new ImageRenderer());
  }
 }

 public TicTacToe() {
  setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  setBounds(100, 100, 450, 470);
  setTitle("Tic-Tac-Toe");
  setResizable(false);
  contentPane = new JPanel();
  contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
  contentPane.setLayout(null);
  setContentPane(contentPane);

  setTable();
  contentPane.add(table);

  JButton btnNewButton = new JButton("Restart");
  btnNewButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent arg0) {
    ai.restart();
   }
  });
  btnNewButton.setFont(new Font("SansSerif", Font.BOLD, 16));
  btnNewButton.setBounds(180, 380, 90, 25);
  contentPane.add(btnNewButton);
 }
}

Sunday, April 28, 2013

A Java Implementation of the K-means Clustering Algorithm for 3D points


In data mining, k-means clustering is a method of cluster analysis that aims to partition n points into k clusters where each point belongs to the cluster with the nearest mean. This results in a partitioning of the data space into Voronoi cells.

The following codes are my implementation of the K-means algorithm for 3D points. The Java project can be downloaded from https://sites.google.com/site/moderntone/K-Means.zip




Cluster.java

package kmeans;
import java.util.*;

public class Cluster {

 private final List<point> points;
 private Point centroid;
 
 public Cluster(Point firstPoint) {
  points = new ArrayList<point>();
  centroid = firstPoint;
 }
 
 public Point getCentroid(){
  return centroid;
 }
 
 public void updateCentroid(){
  double newx = 0d, newy = 0d, newz = 0d;
  for (Point point : points){
   newx += point.x; newy += point.y; newz += point.z;
  }
  centroid = new Point(newx / points.size(), newy / points.size(), newz / points.size());
 }
 
 public List<point> getPoints() {
  return points;
 }
 
 public String toString(){
  StringBuilder builder = new StringBuilder("This cluster contains the following points:\n");
  for (Point point : points)
   builder.append(point.toString() + ",\n");
  return builder.deleteCharAt(builder.length() - 2).toString(); 
 }
}



Clusters.java

package kmeans;

import java.util.*;

public class Clusters extends ArrayList<cluster> {

 private static final long serialVersionUID = 1L;
 private final List<point> allPoints;
 private boolean isChanged;
 
 public Clusters(List<point> allPoints){
  this.allPoints = allPoints;
 }
 
 /**@param point
  * @return the index of the Cluster nearest to the point
  */
 public Integer getNearestCluster(Point point){
  double minSquareOfDistance = Double.MAX_VALUE;
  int itsIndex = -1;
  for (int i = 0 ; i < size(); i++){
   double squareOfDistance = point.getSquareOfDistance(get(i).getCentroid());
   if (squareOfDistance < minSquareOfDistance){
    minSquareOfDistance = squareOfDistance;
    itsIndex = i;
   }
  }
  return itsIndex;
 }

 public boolean updateClusters(){
  for (Cluster cluster : this){
   cluster.updateCentroid();
   cluster.getPoints().clear();
  }
  isChanged = false;
  assignPointsToClusters();
  return isChanged;
 }
 
 public void assignPointsToClusters(){
  for (Point point : allPoints){
   int previousIndex = point.getIndex();
   int newIndex = getNearestCluster(point);
   if (previousIndex != newIndex)
    isChanged = true;
   Cluster target = get(newIndex);
   point.setIndex(newIndex);
   target.getPoints().add(point);
  }
 }
}

Point.java

package kmeans;

public class Point {
 
 private int index = -1; //denotes which Cluster it belongs to
 public double x, y, z;
 
 public Point(double x, double y, double z) {
  this.x = x;
  this.y = y;
  this.z = z;
 }
 
 public Double getSquareOfDistance(Point anotherPoint){
  return  (x - anotherPoint.x) * (x - anotherPoint.x)
    + (y - anotherPoint.y) *  (y - anotherPoint.y) 
    + (z - anotherPoint.z) *  (z - anotherPoint.z);
 }

 public int getIndex() {
  return index;
 }

 public void setIndex(int index) {
  this.index = index;
 }
 
 public String toString(){
  return "(" + x + "," + y + "," + z + ")";
 } 
}

KMeans.java

package kmeans;

import java.io.*;
import java.util.*;

public class KMeans {

 private static final Random random = new Random();
 public final List<point> allPoints;
 public final int k;
 private Clusters pointClusters; //the k Clusters

 /**@param pointsFile : the csv file for input points
  * @param k : number of clusters
  */
 public KMeans(String pointsFile, int k) {
  if (k < 2)
   new Exception("The value of k should be 2 or more.").printStackTrace();
  this.k = k;
  List<point> points = new ArrayList<point>();
  try {
   InputStreamReader read = new InputStreamReader(
     new FileInputStream(pointsFile), "UTF-8");
   BufferedReader reader = new BufferedReader(read);
   String line;
   while ((line = reader.readLine()) != null) 
    points.add(getPointByLine(line));
   reader.close();
   
  } catch (IOException e) {
   e.printStackTrace();
  }
  this.allPoints = Collections.unmodifiableList(points);
 }

 private Point getPointByLine(String line) {
  String[] xyz = line.split(",");
  return new Point(Double.parseDouble(xyz[0]),
    Double.parseDouble(xyz[1]), Double.parseDouble(xyz[2]));
 }

 /**step 1: get random seeds as initial centroids of the k clusters
  */
 private void getInitialKRandomSeeds(){
  pointClusters = new Clusters(allPoints);
  List<point> kRandomPoints = getKRandomPoints();
  for (int i = 0; i < k; i++){
   kRandomPoints.get(i).setIndex(i);
   pointClusters.add(new Cluster(kRandomPoints.get(i)));
  } 
 }
 
 private List<point> getKRandomPoints() {
  List<point> kRandomPoints = new ArrayList<point>();
  boolean[] alreadyChosen = new boolean[allPoints.size()];
  int size = allPoints.size();
  for (int i = 0; i < k; i++) {
   int index = -1, r = random.nextInt(size--) + 1;
   for (int j = 0; j < r; j++) {
    index++;
    while (alreadyChosen[index])
     index++;
   }
   kRandomPoints.add(allPoints.get(index));
   alreadyChosen[index] = true;
  }
  return kRandomPoints;
 }
 
 /**step 2: assign points to initial Clusters
  */
 private void getInitialClusters(){
  pointClusters.assignPointsToClusters();
 }
 
 /** step 3: update the k Clusters until no changes in their members occur
  */
 private void updateClustersUntilNoChange(){
  boolean isChanged = pointClusters.updateClusters();
  while (isChanged)
   isChanged = pointClusters.updateClusters();
 }
 
 /**do K-means clustering with this method
  */
 public List<cluster> getPointsClusters() {
  if (pointClusters == null) {
   getInitialKRandomSeeds();
   getInitialClusters();
   updateClustersUntilNoChange();
  }
  return pointClusters;
 }
 
 public static void main(String[] args) {
  String pointsFilePath = "files/randomPoints.csv";
  KMeans kMeans = new KMeans(pointsFilePath, 6);
  List<cluster> pointsClusters = kMeans.getPointsClusters();
  for (int i = 0 ; i < kMeans.k; i++)
   System.out.println("Cluster " + i + ": " + pointsClusters.get(i));
 }
}