AOJ1143 Hexerpents of Hexwamp

問題リンク Hexerpents of Hexwamp

  • 解法

方針は幅優先探索なのですが、ある蛇の状態から次の遷移先を作るのが非常に厄介です。その都度遷移先を計算していたら時間が厳しくなるので、最初に一括で遷移先状態を計算しておき、幅優先探索中はキャッシュされた遷移先を参照することにします。
まず、六角座標の隣接マスに0〜5の番号を付けます。自分のソースでは真上が0、時計回りに1, 2, 3...です。
蛇の状態は、頭の位置を起点にして、節がどうつながっているかで表すことにします。例えば、節が(0, 0)(0, 1)(-1, 2)という長さ3の蛇は、頭から、真下、左下という順に繋がっているので、"34"となります。長さNの蛇はN-1文字の文字列で表現できます。
次に、蛇の状態で有効な状態を全て求めます。同じマスを節が共有していたり、節が許されない隣接の仕方をしているものを除去するのです。蛇の状態を表す文字列Sを作ったら、まず、節の座標を復元します。その結果
i番目とi+1番目の節が隣接していない
同じ座標を指す節が2つ以上ある
i番目の節に隣接している節の数を数えて
 iが端の節なのに、隣接個数が1でない
 iが中間に節なのに、隣接個数が2でない
のいずれかが成り立てばそれは有効でない蛇の状態です。
これを長さ1〜8の文字列について行います。
有効な状態を全て生成したら、それらについて次の状態を作り出します。
次の状態は「頭の位置の変位」と「遷移後の蛇の状態」の組で表します。当然遷移後の蛇の状態は有効なものです。
次の状態は、k番目の節を動かしてみて、現在の状態が有効かを調べ、有効な状態ならば、次はk+2番目の節について考えてみる、という再帰で探しました。
ここまで準備ができれば後はだいぶ楽で、幅優先探索を書くだけです。
ただ、合流が多いので訪れたノードはキャッシュしておきます。
岩の存在する座標は1つの整数でSetに突っ込んでおきます。今回は座標X, Yに10^6をプラスし、必ず正の数にしたうえで
X*10^7+Y
と変換しています。
この問題は制限時間が15秒と異例の設定なので、かなり根気が必要だと思います。

  • ソース
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

//Hexerpents of Hexwamp
public class AOJ1143 {

	int[][] dir = {{0,-1},{1,-1},{1,0},{0,1},{-1,1},{-1,0}};

	//蛇を表す。頭の位置が(x, y)。残りの節は連結している方向を文字列で覚える 
	//Eclipseの力でequals()とhashCode()を作る
	class Snake{
		int x, y;
		String s;
		public Snake(int x, int y, String s) {
			this.x = x;
			this.y = y;
			this.s = s;
		}
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + ((s == null) ? 0 : s.hashCode());
			result = prime * result + x;
			result = prime * result + y;
			return result;
		}
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Snake other = (Snake) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (s == null) {
				if (other.s != null)
					return false;
			} else if (!s.equals(other.s))
				return false;
			if (x != other.x)
				return false;
			if (y != other.y)
				return false;
			return true;
		}
		private AOJ1143 getOuterType() {
			return AOJ1143.this;
		}

	}

	//隣接状態を表す
	//頭の位置の変位がdx, dy
	//動いた結果の節の状態がs
	class S{
		int dx, dy;
		String s;
		public S(int dx, int dy, String s) {
			this.dx = dx;
			this.dy = dy;
			this.s = s;
		}
	}

	//節の状態がキー、遷移先状態の集合がバリュー
	Map<String, Set<S>> adj;
	int n;
	//座標を整数値に変換するときに使う
	long FIX = 1000000, M = 10000000;
	//岩の座標の集合
	Set<Long> rock;
	//有効な節の状態の集合
	Set<String> valid;

	//長さlenの蛇の体で有効なものを作る = validを埋める
	void make(int k, char[] s, int len){
		if(k>=1){
			int[][] p = new int[k+1][2];
			p[0][0]=p[0][1]=0;
			for(int i=1;i<=k;i++){
				int x = s[i-1]-'0';
				p[i][0] = p[i-1][0]+dir[x][0]; p[i][1] = p[i-1][1]+dir[x][1];
			}
			if(!valid(p, k))return;
		}
		if(k==len){
			String state = new String(s);
			valid.add(state);
			adj.put(state, new HashSet<S>());
			return;
		}
		for(char d='0';d<='5';d++){
			s[k] = d;
			make(k+1, s, len);
		}
	}

	//この体の構造から1ステップで辿りつける状態を探す
	void find(String state){
		int N = state.length()+1;
		int[][] p = new int[N][2];
		for(int i=0;i<N-1;i++){
			int d = state.charAt(i)-'0';
			p[i+1][0] = p[i][0] + dir[d][0]; p[i+1][1] = p[i][1] + dir[d][1];
		}
		dfs(0, N, p, state);
	}
	void dfs(int k, int N, int[][] p, String state){
		if(N<=k){
			int dx = p[0][0], dy = p[0][1];
			String s = "";
			for(int i=1;i<N;i++){
				for(int d=0;d<6;d++){
					if(p[i][0]==p[i-1][0]+dir[d][0]&&p[i][1]==p[i-1][1]+dir[d][1]){
						s += d; break;
					}
				}
			}
			if(!valid.contains(s))return;
			adj.get(state).add(new S(dx, dy, s));
			return;
		}
		dfs(k+1, N, p, state);
		int px = p[k][0], py = p[k][1];
		for(int d=0;d<6;d++){
			p[k][0]+=dir[d][0]; p[k][1]+=dir[d][1];
			if(valid(p, Math.min(k+1, N-1))){
				//k番目の節を動かしたら次に動かすかを考えるのはk+2番目以降の節
				dfs(k+2, N, p, state);
			}
			p[k][0] = px; p[k][1] = py;
		}
	}

	//p[0]〜p[len]まで決まっている。これは有効な状態か?
	boolean valid(int[][] p, int len){
		//体1つならなんだっていい
		if(len==0)return true;
		String s = "";
		for(int i=0;i<len;i++){
			boolean f = false;
			for(int d=0;d<6;d++){
				if(p[i][0]+dir[d][0]==p[i+1][0]&&p[i][1]+dir[d][1]==p[i+1][1]){
					s += d;
					f = true; break;
				}
			}
			//i番目とi+1番目の節が連結していない
			if(!f)return false;
		}
		boolean ok = true;
		for(int i=0;i<=len;i++)for(int j=i+1;j<=len;j++){
			//節i, jが同じ座標にある
			if(p[i][0]==p[j][0]&&p[i][1]==p[j][1]){
				ok = false; break;
			}
		}
		if(!ok)return false;
		int[] adj = new int[len+1];
		for(int i=0;i<=len;i++){
			for(int j=0;j<=len;j++){
				if(i==j)continue;
				for(int d=0;d<6;d++){
					if(p[i][0]+dir[d][0]==p[j][0]&&p[i][1]+dir[d][1]==p[j][1]){
						adj[i]++;
					}
				}
			}
			//端の節に隣接している節の個数は1つのはず
			if((i==0||i==len)&&adj[i]!=1)return false;
			//中間の節に隣接している節の個数は2つのはず
			if(0<i&&i<len&&adj[i]!=2)return false;
		}
		return true;
	}

	void run(){
		Scanner sc = new Scanner(System.in);
		valid = new HashSet<String>();
		adj = new HashMap<String, Set<S>>();
		//長さ8までの有効な蛇の状態を作る
		for(int N=1;N<=8;N++){
			make(0, new char[N-1], N-1);
		}
		//それぞれの状態に対して隣接状態を求める
		for(String s:valid)find(s);
		for(;;){
			n = sc.nextInt();
			if(n==0)break;
			int[][] p = new int[n][2];
			for(int i=0;i<n;i++)for(int j=0;j<2;j++)p[i][j]=sc.nextInt();
			int k = sc.nextInt();
			rock = new HashSet<Long>();
			while(k--!=0){
				long x = sc.nextInt()+FIX, y = sc.nextInt()+FIX;
				rock.add(x*M+y);
			}
			int gx = sc.nextInt(), gy = sc.nextInt();
			int x = p[0][0], y = p[0][1];
			String s = "";
			for(int i=0;i<n-1;i++){
				for(int d=0;d<6;d++){
					if(p[i][0]+dir[d][0]==p[i+1][0]&&p[i][1]+dir[d][1]==p[i+1][1]){
						s += d; break;
					}
				}
			}
			Snake start = new Snake(x, y, s);
			List<Snake> list = new ArrayList<Snake>();
			list.add(start);
			Set<Snake> used = new HashSet<Snake>();
			used.add(start);
			int step = 0;
			while(!list.isEmpty()&&step<=20){
				List<Snake> nextState = new ArrayList<Snake>();
				for(Snake v:list){
					if(v.x==gx&&v.y==gy){
						System.out.println(step);
						nextState.clear(); break;
					}
					for(S a:adj.get(v.s)){
						Snake z = new Snake(v.x+a.dx, v.y+a.dy, a.s);
						//既に訪れた状態ならノーカン
						if(used.contains(z))continue;
						int[][] pos = new int[n][2];
						pos[0][0] = z.x; pos[0][1] = z.y;
						for(int i=1;i<n;i++){
							int d = z.s.charAt(i-1)-'0';
							pos[i][0] = pos[i-1][0] + dir[d][0]; pos[i][1] = pos[i-1][1] + dir[d][1];
						}
						//節の中に岩に乗り上げているものがないかチェック
						boolean ok = true;
						for(int i=0;i<n;i++){
							if(rock.contains((pos[i][0]+FIX)*M+pos[i][1]+FIX)){
								ok = false; break;
							}
						}
						if(!ok)continue;
						used.add(z);
						nextState.add(z);
					}
				}
				list = nextState;
				step++;
			}
		}
	}

	public static void main(String[] args) {
		new AOJ1143().run();
	}
}