import java.io.BufferedReader; import java.io.FileReader; import java.io.InputStreamReader; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.ArrayList; import java.util.LinkedList; import java.util.TreeMap; import java.util.HashMap; import java.util.Queue; /* DISTRIBUTION OF DICTIONARY: Read the words...88984 1 1 2 48 3 601 4 2409 5 4882 6 8205 7 11989 8 13672 9 13014 10 11297 11 8617 12 6003 13 3814 14 2173 15 1169 16 600 17 302 18 107 19 53 20 28 Elapsed time FAST: 2.8 vs 3.8 Elapsed time MEDIUM: 50.9 vs 50.5 Elapsed time SLOW: 95.9 vs 96.1 (H vs T) **/ public class WordLadder { public static List readWords( BufferedReader in ) throws IOException { String oneLine; List lst = new ArrayList<>( ); while( ( oneLine = in.readLine( ) ) != null ) lst.add( oneLine ); return lst; } // Returns true is word1 and word2 are the same length // and differ in only one character. private static boolean oneCharOff( String word1, String word2 ) { if( word1.length( ) != word2.length( ) ) return false; int diffs = 0; for( int i = 0; i < word1.length( ); i++ ) if( word1.charAt( i ) != word2.charAt( i ) ) if( ++diffs > 1 ) return false; return diffs == 1; } private static void update( Map> m, KeyType key, String value ) { List lst = m.get( key ); if( lst == null ) { lst = new ArrayList<>( ); m.put( key, lst ); } lst.add( value ); } // Computes a map in which the keys are words and values are Lists of words // that differ in only one character from the corresponding key. // Uses a quadratic algorithm (with appropriate Map). public static Map> computeAdjacentWordsSlow( List theWords ) { Map> adjWords = new HashMap<>( ); String [ ] words = new String[ theWords.size( ) ]; theWords.toArray( words ); for( int i = 0; i < words.length; i++ ) for( int j = i + 1; j < words.length; j++ ) if( oneCharOff( words[ i ], words[ j ] ) ) { update( adjWords, words[ i ], words[ j ] ); update( adjWords, words[ j ], words[ i ] ); } return adjWords; } // Computes a map in which the keys are words and values are Lists of words // that differ in only one character from the corresponding key. // Uses a quadratic algorithm (with appropriate Map), but speeds things up a little by // maintaining an additional map that groups words by their length. public static Map> computeAdjacentWordsMedium( List theWords ) { Map> adjWords = new HashMap<>( ); Map> wordsByLength = new HashMap<>( ); // Group the words by their length for( String w : theWords ) update( wordsByLength, w.length( ), w ); // Work on each group separately for( List groupsWords : wordsByLength.values( ) ) { String [ ] words = new String[ groupsWords.size( ) ]; groupsWords.toArray( words ); for( int i = 0; i < words.length; i++ ) for( int j = i + 1; j < words.length; j++ ) if( oneCharOff( words[ i ], words[ j ] ) ) { update( adjWords, words[ i ], words[ j ] ); update( adjWords, words[ j ], words[ i ] ); } } return adjWords; } // Computes a map in which the keys are words and values are Lists of words // that differ in only one character from the corresponding key. // Uses an efficient algorithm that is O(N log N) with a TreeMap, or // O(N) if a HashMap is used. public static Map> computeAdjacentWords( List words ) { Map> adjWords = new TreeMap<>( ); Map> wordsByLength = new TreeMap<>( ); // Group the words by their length for( String w : words ) update( wordsByLength, w.length( ), w ); // Work on each group separately for( Map.Entry> entry : wordsByLength.entrySet( ) ) { List groupsWords = entry.getValue( ); int groupNum = entry.getKey( ); // Work on each position in each group for( int i = 0; i < groupNum; i++ ) { // Remove one character in specified position, computing representative. // Words with same representative are adjacent, so first populate // a map Map> repToWord = new HashMap<>( ); for( String str : groupsWords ) { String rep = str.substring( 0, i ) + str.substring( i + 1 ); update( repToWord, rep, str ); } // and then look for map values with more than one string for( List wordClique : repToWord.values( ) ) if( wordClique.size( ) >= 2 ) for( String s1 : wordClique ) for( String s2 : wordClique ) if( s1 != s2 ) // must be same string; equals not needed update( adjWords, s1, s2 ); } } return adjWords; } // Find most changeable word: the word that differs in only one // character with the most words. Return a list of these words, in case of a tie. public static List findMostChangeable( Map> adjacentWords ) { List mostChangeableWords = new ArrayList<>( ); int maxNumberOfAdjacentWords = 0; for( Map.Entry> entry : adjacentWords.entrySet( ) ) { List changes = entry.getValue( ); if( changes.size( ) > maxNumberOfAdjacentWords ) { maxNumberOfAdjacentWords = changes.size( ); mostChangeableWords.clear( ); } if( changes.size( ) == maxNumberOfAdjacentWords ) mostChangeableWords.add( entry.getKey( ) ); } return mostChangeableWords; } public static void printMostChangeables( List mostChangeable, Map> adjacentWords ) { for( String word : mostChangeable ) { System.out.print( word + ":" ); List adjacents = adjacentWords.get( word ); for( String str : adjacents ) System.out.println( " " + str ); System.out.println( " (" + adjacents.size( ) + " words)" ); } } public static void printHighChangeables( Map> adjacentWords, int minWords ) { for( Map.Entry> entry : adjacentWords.entrySet( ) ) { List words = entry.getValue( ); if( words.size( ) >= minWords ) { System.out.print( entry.getKey( ) + " )" + words.size( ) + "):" ); for( String w : words ) System.out.print( " " + w ); System.out.println( ); } } } // After the shortest path calculation has run, computes the List that // contains the sequence of word changes to get from first to second. public static List getChainFromPreviousMap( Map prev, String first, String second ) { LinkedList result = new LinkedList<>( ); if( prev.get( second ) != null ) for( String str = second; str != null; str = prev.get( str ) ) result.addFirst( str ); return result; } // Runs the shortest path calculation from the adjacency map, returning a List // that contains the sequence of words changes to get from first to second. public static List findChain( Map> adjacentWords, String first, String second ) { Map previousWord = new HashMap<>( ); Queue q = new LinkedList<>( ); q.add( first ); while( !q.isEmpty( ) ) { String current = q.element( ); q.remove( ); List adj = adjacentWords.get( current ); if( adj != null ) for( String adjWord : adj ) if( previousWord.get( adjWord ) == null ) { previousWord.put( adjWord, current ); q.add( adjWord ); } } previousWord.put( first, null ); return getChainFromPreviousMap( previousWord, first, second ); } // Runs the shortest path calculation from the original list of words, returning // a List that contains the sequence of word changes to get from first to // second. Since this calls computeAdjacentWords, it is recommended that the // user instead call computeAdjacentWords once and then call other findChar for // each word pair. public static List findChain( List words, String first, String second ) { Map> adjacentWords = computeAdjacentWords( words ); return findChain( adjacentWords, first, second ); } public static void main( String [ ] args ) throws IOException { long start, end; FileReader fin = new FileReader( "dict.txt" ); BufferedReader bin = new BufferedReader( fin ); List words = readWords( bin ); System.out.println( "Read the words..." + words.size( ) ); Map> adjacentWords; start = System.currentTimeMillis( ); adjacentWords = computeAdjacentWords( words ); end = System.currentTimeMillis( ); System.out.println( "Elapsed time FAST: " + (end-start) ); start = System.currentTimeMillis( ); adjacentWords = computeAdjacentWordsMedium( words ); end = System.currentTimeMillis( ); System.out.println( "Elapsed time MEDIUM: " + (end-start) ); start = System.currentTimeMillis( ); adjacentWords = computeAdjacentWordsSlow( words ); end = System.currentTimeMillis( ); System.out.println( "Elapsed time SLOW: " + (end-start) ); // printHighChangeables( adjacentWords, 15 ); System.out.println( "Adjacents computed..." ); List mostChangeable = findMostChangeable( adjacentWords ); System.out.println( "Most changeable computed..." ); printMostChangeables( mostChangeable, adjacentWords ); BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) ); for( ; ; ) { System.out.println( "Enter two words: " ); String w1 = in.readLine( ); String w2 = in.readLine( ); List path = findChain( adjacentWords, w1, w2 ); System.out.print( path.size( ) + "..." ); for( String word : path ) System.out.print( " " + word ); System.out.println( ); } } }