Skip to content

Instantly share code, notes, and snippets.

@komiya-atsushi
Created December 24, 2013 02:51
Show Gist options
  • Save komiya-atsushi/8108168 to your computer and use it in GitHub Desktop.
Save komiya-atsushi/8108168 to your computer and use it in GitHub Desktop.
「213個の単語からパングラムを作ろう」 https://codeiq.jp/ace/stakemura/q594 の回答です。整数計画法とかは一切使わず枝刈りをひたすらに頑張るアプローチです。
package codeiq;
import java.util.*;
/**
* 深さ優先探索を基本戦略とする SQL92 予約語パングラムのソルバーです。
* <p>
* 出題は https://codeiq.jp/ace/stakemura/q594 こちら。
* </p>
* <pre>
* ## 戦術
*
* 単純な深さ優先探索では計算時間がかかってしまうので、以下の手段によって計算量を減らしています。
*
* - 探索に利用する予約語を事前に削減する。
* - ESCAPE → SPACE など、同じ文字集合でより短い表現ができる
* 予約語を除去する
* - ADD に対する AND など、一方の文字集合の部分集合となるような
* 予約語を除去する
* - GOTO に対する [GO, AT] など、複数予約語の文字集合の
* 部分集合となるような予約語を除去する
*
* - 「ある予約語(もしくは予約語の組み合わせ)を構成している文字集合」を、
* 26 ビットの整数値で表現する。
* - 予約語ごとにこの整数値を求めておくことで、複数の予約語からなる文字集合を
* ビット演算 (OR 演算) で算出することができる
* - また、文字集合のサイズは popcount {@link Integer#bitCount(int)} で算出できる
*
* - 文字集合ごとに、その文字集合を達成するのに必要な予約語の総文字列長 (最小値) を
* 保持しておく (2^26 の空間計算量が必要となる)
* - 探索時の枝刈り (探索打ち切りの判定) に利用する
*
* ## 実行方法
*
* java -Xmx1024m codeiq.PangramSolver
*
* ## 実行結果
*
* UPDATE FETCH ZONE WORK JOIN SQL MAX AVG BY
* 約 3.8 秒 (Core i5-3450)
* </pre>
*
* @author KOMIYA Atsushi
*/
public class PangramSolver {
private static final List<String> WORDS = Arrays.asList("ABSOLUTE", "ACTION", "ADD", "ALL", "ALLOCATE", "ALTER", "AND", "ANY", "ARE", "AS", "ASC", "ASSERTION", "AT", "AUTHORIZATION", "AVG", "BEGIN", "BETWEEN", "BIT", "BOTH", "BY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHECK", "CLOSE", "COALESCE", "COLLATE", "COLLATION", "COLUMN", "COMMIT", "CONNECT", "CONNECTION", "CONSTRAINT", "CONSTRAINTS", "CONTINUE", "CONVERT", "CORRESPONDING", "CREATE", "CROSS", "CURRENT", "CURSOR", "DATE", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DESCRIBE", "DESCRIPTOR", "DIAGNOSTICS", "DISCONNECT", "DISTINCT", "DOMAIN", "DOUBLE", "DROP", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCEPTION", "EXEC", "EXECUTE", "EXISTS", "EXTERNAL", "EXTRACT", "FALSE", "FETCH", "FIRST", "FLOAT", "FOR", "FOREIGN", "FOUND", "FROM", "FULL", "GET", "GLOBAL", "GO", "GOTO", "GRANT", "GROUP", "HAVING", "HOUR", "IDENTITY", "IMMEDIATE", "IN", "INDICATOR", "INITIALLY", "INNER", "INPUT", "INSENSITIVE", "INSERT", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "IS", "ISOLATION", "JOIN", "KEY", "LANGUAGE", "LAST", "LEADING", "LEFT", "LEVEL", "LIKE", "LOCAL", "LOWER", "MATCH", "MAX", "MIN", "MINUTE", "MODULE", "MONTH", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NEXT", "NO", "NOT", "NULL", "NULLIF", "NUMERIC", "OF", "ON", "ONLY", "OPEN", "OPTION", "OR", "ORDER", "OUTER", "OUTPUT", "OVERLAPS", "PAD", "PARTIAL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVILEGES", "PROCEDURE", "PUBLIC", "READ", "REAL", "REFERENCES", "RELATIVE", "RESTRICT", "REVOKE", "RIGHT", "ROLLBACK", "ROWS", "SCHEMA", "SCROLL", "SECOND", "SECTION", "SELECT", "SESSION", "SET", "SIZE", "SMALLINT", "SOME", "SPACE", "SQL", "SQLCODE", "SQLERROR", "SQLSTATE", "SUBSTRING", "SUM", "TABLE", "TEMPORARY", "THEN", "TIME", "TIMESTAMP", "TO", "TRAILING", "TRANSACTION", "TRANSLATE", "TRANSLATION", "TRIM", "TRUE", "UNION", "UNIQUE", "UNKNOWN", "UPDATE", "UPPER", "USAGE", "USER", "USING", "VALUE", "VALUES", "VARCHAR", "VARYING", "VIEW", "WHEN", "WHENEVER", "WHERE", "WITH", "WORK", "WRITE", "YEAR", "ZONE");
private static boolean verbose = false;
/** 探索に利用する SQL92 の予約語一覧です */
private Word[] wordTable;
/** 探索処理において、現在評価中の予約語列を表します */
private Stack<Word> wordSequence;
/** 文字集合を表現するのに必要な最低文字列長を保持・管理します */
private short[] minimumLengthTable;
/** 探索現時点における最小パングラムの文字列長を保持します */
private int currentBestLength = Integer.MAX_VALUE;
/** 最小のパングラムを表します */
private List<Word> bestResult;
/**
* エントリポイント。
*
* @param args
*/
public static void main(String[] args) {
long begin = System.currentTimeMillis();
String result = new PangramSolver().solve(WORDS);
long end = System.currentTimeMillis();
printMessage("処理時間 : %,d [ms]", end - begin);
System.out.println(result);
}
/**
* 指定された単語からなる最小のパングラムを求め、
* スペース区切りの 1 つの文字列に結合した状態で返却します。
*
* @param words 単語列
* @return words の単語列から構成される最小のパングラム (スペース区切り)
*/
String solve(List<String> words) {
minimumLengthTable = new short[1 << 27];
Arrays.fill(minimumLengthTable, Short.MAX_VALUE);
wordTable = Word.build(words);
wordSequence = new Stack<>();
solveRecursive(0, 0, 0, (short) 0);
StringBuilder sb = new StringBuilder();
for (Word word : bestResult) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(word);
}
return sb.toString();
}
/**
* 最小パングラム算出のための深さ優先探索をします。
*
* @param nextIndex
* @param key
* @param numUsedChars
* @param length
*/
void solveRecursive(int nextIndex, int key, int numUsedChars, short length) {
for (int i = nextIndex; i < wordTable.length; i++) {
Word word = wordTable[i];
int newKey = key | word.key;
int newNumUsedChars = Integer.bitCount(newKey);
short newLength = (short) (length + word.length());
if (newNumUsedChars == numUsedChars
|| newLength >= minimumLengthTable[newKey]
|| newLength + (26 - newNumUsedChars) >= currentBestLength) {
// 以下の場合は、予約語 word の追加を却下する
// - 予約語 word を加えても文字集合に変化が生じない場合
// - 現在の文字集合を実現するのに必要となる、過去に探索したときの最小文字列長と等しいもしくはそれを超える場合
// - パングラムが見つかっている場合、その見つかっているパングラムと同じもしくはそれを超える長さの場合
continue;
}
// 現在の文字集合を実現する最小文字列長を記録しておく
minimumLengthTable[newKey] = newLength;
wordSequence.push(word);
try {
if (newNumUsedChars == 26) {
printMessage("%d : %s", newLength, wordSequence.toString());
bestResult = new ArrayList<>(wordSequence);
currentBestLength = newLength;
continue;
}
solveRecursive(i + 1, newKey, newNumUsedChars, newLength);
} finally {
wordSequence.pop();
}
}
}
/**
* 予約語を表します。
*
* @author KOMIYA Atsushi
*/
static class Word {
/** 予約語そのものを表します */
public final String word;
/** 予約語で使われている文字種別に対してビットフラグ ('A' が MSB で 'Z' が LSB) を立てた値を保持します */
public final int key;
Word(String word) {
this.word = word;
int key = 0;
for (char ch = 'A'; ch <= 'Z'; ch++) {
if (word.contains(Character.toString(ch))) {
key |= 1;
}
key <<= 1;
}
this.key = key;
}
short length() {
return (short) word.length();
}
@Override
public String toString() {
return word;
}
static Word[] build(List<String> _words) {
// 文字列長の短い順に予約語を並び替える
Collections.sort(_words, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.length() < o2.length()) {
return -1;
} else if (o1.length() > o2.length()) {
return 1;
}
return o1.compareTo(o2);
}
});
List<Word> words = new ArrayList<>();
for (String w : _words) {
words.add(new Word(w));
}
// 不要な予約語を順次消去する
words = removeRedundantWords(words);
words = removeWordsReplaceableWithAnotherWord(words);
words = removeWordsReplaceableWithWordCombinations(words);
// 文字列長の長いものから順に列挙する
Collections.reverse(words);
Word[] result = new Word[words.size()];
result = words.toArray(result);
return result;
}
/**
* 同じ文字構成で、かつより短い別の単語 v が存在する、冗長な単語 w を除去します。
*
* @param words 単語リスト。単語長が短い順に格納されていることを前提とします。
* @return 冗長な単語を除去した後の単語リスト
*/
private static List<Word> removeRedundantWords(List<Word> words) {
Map<Integer, Word> keys = new HashMap<>();
List<Word> result = new ArrayList<>();
for (Word word : words) {
Word shortWord = keys.get(word.key);
if (shortWord == null) {
keys.put(word.key, word);
result.add(word);
} else {
printMessage("%s を構成する文字種別は %s と等しいです。", word, shortWord);
}
}
return result;
}
/**
* 別の 1 つの単語 v で代替表現できる、単語 w を除去します。
* <p>
* 下記の条件すべてを満たすとき、「単語 w は単語 v で代替表現できる」と言います。
* <ul>
* <li>単語 v の文字列長と比べて、単語 w のそれが長いか等しいこと</li>
* <li>単語 v を構成する文字種別数と比べて、単語 w のそれが少ないこと</li>
* </ul>
* </p>
*
* @param words 単語リスト
* @return 代替表現可能な単語を除去した単語リスト
*/
private static List<Word> removeWordsReplaceableWithAnotherWord(List<Word> words) {
List<Word> result = new ArrayList<>();
outer:
for (int i = 0; i < words.size(); i++) {
Word wordI = words.get(i);
for (int j = 0; j < words.size(); j++) {
if (i == j) {
continue;
}
Word wordJ = words.get(j);
if ((wordI.key & wordJ.key) == wordI.key
&& wordI.length() >= wordJ.length()) {
printMessage("%s は %s で代替表現できます。", wordI, wordJ);
continue outer;
}
}
result.add(wordI);
}
return result;
}
/**
* 他の単語の組み合わせで代替できる単語を除去します。
* <p>
* 下記の条件すべてを満たすとき、「単語 w は単語集合 V = [v1, v2, ..., vn] で代替表現できる」と言います。
* <ul>
* <li>単語集合 V に属するすべての単語の文字列長を合算したものと比べて、単語 w のそれが長いか等しいこと</li>
* <li>単語集合 V の全単語を構成する文字種別の数と比べて、単語 w のそれが少ないこと</li>
* </ul>
* </p>
*
* @param words 単語リスト
* @return 複数の単語で代替表現可能な単語を除去した単語リスト
*/
private static List<Word> removeWordsReplaceableWithWordCombinations(List<Word> words) {
List<Word> result = new ArrayList<>();
for (Iterator<Word> i = new ArrayList<>(words).iterator();
i.hasNext(); ) { // Word word : words) {
Word word = i.next();
if (!new CombinationContext(word, words).canReplaceable()) {
result.add(word);
} else {
i.remove();
}
}
return result;
}
/**
* {@link #removeWordsReplaceableWithWordCombinations(java.util.List)} で使われる、
* 複数の単語で代替可能かどうかを判定する機能を提供します。
*
* @author KOMIYA Atsushi
*/
static class CombinationContext {
Word targetWord;
List<Word> words;
Stack<Word> combination = new Stack<>();
CombinationContext(Word targetWord, List<Word> words) {
this.targetWord = targetWord;
this.words = words;
}
boolean canReplaceable() {
return canReplaceableRecursive(0, (short) 0, 0);
}
boolean canReplaceableRecursive(int nextIndex, short length, int key) {
for (int i = nextIndex; i < words.size(); i++) {
Word word = words.get(i);
if (word.equals(targetWord)) {
continue;
}
short newLength = (short) (length + word.length());
if (newLength > targetWord.length()) {
continue;
}
combination.push(word);
int newKey = key | word.key;
if ((targetWord.key & newKey) == targetWord.key) {
printMessage("%s は複数単語の組み合わせ %s で代替表現できます。", targetWord, combination);
return true;
}
if (canReplaceableRecursive(i + 1, newLength, newKey)) {
return true;
}
combination.pop();
}
return false;
}
}
}
static void printMessage(String format, Object... args) {
if (verbose) {
System.err.printf(format, args);
System.err.println();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment