AOJ 1181

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1181&lang=jp

ものを回転させる問題は他にも
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2169
がありますが、置換で回転をあらわす時の注意事項。
例えば、
(0, 1, 2, 3, 4, 5)
(4, 0, 2, 3, 5, 1)
だとしたら、位置0にあった要素を4に移す置換と考えます。つまり、
f={4, 0, 2, 3, 5, 1}、
変換前の値をvとすれば、変換後のv'は
v'[f[i]]=v[i]を満たします。
値iがjに変わるような置換としがちですが、これではうまく行きません。場所を移すと考えましょう。

set<vi> S;
map<vi, vector<vi>> M;


int F[4][6] = { //1, 2, 3, 4の方向に転がる
	{1, 5, 2, 3, 0, 4}, //a
	{2, 1, 5, 0, 4, 3}, //b
	{3, 1, 0, 5, 4, 2},  //ib
	{4, 0, 2, 3, 5, 1}  //ia
};

int dx[5] = {0, 1, 0, 0, -1};
int dy[5] = {0, 0, 1, -1, 0};

void solve() {
	queue<vi> que;
	que.push(vi{0, 1, 2, 3, 4, 5});
	S.insert(vi{0, 1, 2, 3, 4, 5});
	while(!que.empty()) {
		vi vec = que.front(); que.pop();
		rep(i, 0, 4) {
			vi tmp(6);
			rep(j, 0, 6) {
				tmp[F[i][j]] = vec[j];
			}
			if(!S.count(tmp)) {
				S.insert(tmp); que.push(tmp);
			}
		}
	}
	for(auto vec : S) {
		rep(i, 0, 4) {
			vi tmp(6);
			rep(j, 0, 6) {
				tmp[F[i][j]] = vec[j];
			}
			M[vec].pb(tmp);
		}
		// debug(vec, vector<vi>(all(M[vec])));
	}
	while(true) {
		int N; cin >> N;
		if(N == 0) break;
		map<vi, int> D;
		while(N--) {
			int a, b; cin >> a >> b; a--; b--;
			vi dice;
			for(auto vec : S) {
				if(vec[0] == a && vec[1] == b) dice = vec;
			}
			vi d({100, 0, 0});
			while(true) {
				if(d[0] == 0) break;
				d[0]--;
				if(D.count(d)) {
					d[0]++;
					pi p(-1, -1); //num and dir
					for(int i = 1; i <= 4; i++) {
						vi tmp = d;
						tmp[0]--; tmp[1] += dx[i]; tmp[2] += dy[i];
						if(!D.count(tmp) && dice[i] >= 3) {
							MAX(p, pi(dice[i], i));
						}
					}
					if(p.fst == -1) break;
					else {
						d[1] += dx[p.sec];
						d[2] += dy[p.sec];
						dice = M[dice][p.sec - 1];
					}
				}
			}
			// debug(d, dice[0]);
			D[d] = dice[0];
		}
		vi ans(6, 0);
		for(pair<vi, int> d : D) {
			d.fst[0]++;
			if(!D.count(d.fst)) ans[d.sec]++;
		}
		rep(i, 0, 6) {
			cout << ans[i] << (i == 5 ? '\n' : ' ');
		}
	}
}

面倒of面倒

JOI埋め(難易度9)

AOJ/AtCoder-JOI

9はそんなに難しくはないですね。
実装が多めなのを考慮してもatcoderで言えば600-700くらい?

Typhoon 平面探査
Stamps pairでdp
Abduction xy独立です。
Ski にぶたん
Chopsticks うわぁこれは反省。区間dpということは両端の値が本質的になるということなのに、片方ばっかりみてた…一応ACしましたが、まずオーダーが意味不明だし、やばいケースだと落ちかねないです。
Regions にぶたんしてからpairでdp。正確にはループ回すだけでいいです。またこのタイプか。
Hide-and-seek 平面走査やるだけなんですが、infの値が小さく設定しすぎて結構詰まった…
Lake 直線に落とし込んでDP。全部値が異なることが重要。

CODE FESTIVAL 2018 qual A+オマケ

https://beta.atcoder.jp/contests/code-festival-2018-quala

通過です…

A
sortしたけど、要素が3つでなんかオーバーキルな気がしてしまう
B
愚直にやった。sortもしてません
C
まあdpだよね。0にならないとKを減らせないので注意。
D
まあdpだよね。BIT使ったけどよく考えると累積和で十分。
E
最小値固定して、最大値を決めるんだけど、不等式の条件を整理したら二次元座標である領域から点を見つける問題になって、O(ZlogZ)(Z=X+Y)になったが?あんまり同じ解き方している人いませんね…みんな答えを二分探索してるんだけど、結局最大値最小値どっちも移動してあんまいい方針に見えなかった…

オマケ
ABC110C
これ難しくないですか…。S[i]->T[i]にグラフ張って考察したらこんがらがってしまいました…。普通に元の操作で考えればよかったですね。
解法ですがYesの時、グラフで枝分かれができちゃだめなので、単ループor直線になります。

AGC003F

https://agc003.contest.atcoder.jp/tasks/agc003_f

非連結だと勘違いしてハマってた…

まず縦に並べても横に並べても繋がる場合は答えが1です。
どっちもつながらない場合は#の個数をPとしてP^(K-1)です。
問題は片方だけ繋がる場合です。今回は横に繋がるとします。
#が横に2つ連続で繋がるものの個数をen、連結成分の数をfnとします。
また、盤面を横に並べた時#が繋がる個数をDとします。
するとen+1とfn+1はPとDの線形和で表されます。後は行列累乗をO(logK)でやれば答えが求まります。

ただH=1とW=1の時は場合分けが発生するので注意しましょう。

void solve() {
	cin >> H >> W >> K;
	ll p = 0;
	rep(i, 0, H) {
		string str;
		cin >> str;
		rep(j, 0, W) {
			if(str[j] == '#') B[i][j] = 1;
			p += B[i][j];
		}
	}
	int bh = 0, bw = 0;
	rep(j, 0, W) {
		if(B[0][j] == 1 && B[H - 1][j] == 1) bh++;
	}
	rep(i, 0, H) {
		if(B[i][0] == 1 && B[i][W - 1] == 1) bw++;
	}
	if(H == 1) {
		if(bw) cout << 1 << "\n";
		else cout << mod_pow(p, K - 1) << "\n";
	}
	else if(W == 1) {
		if(bh) cout << 1 << "\n";
		else cout << mod_pow(p, K - 1) << "\n";
	}
	else {
		if(bw && bh) {
			cout << 1 << "\n";
		}
		else if(!bw && !bh) {
			cout << mod_pow(p, K - 1) << "\n";
		}
		else {
			int e = 0;
			int f;
			if(bh) {
				rep(i, 0, H - 1) {
					rep(j, 0, W) {
						if(B[i][j] == 1 && B[i + 1][j] == 1) e++;
					}
				}
				f = bh;
			}
			else {
				rep(j, 0, W - 1) {
					rep(i, 0, H) {
						if(B[i][j] == 1 && B[i][j + 1] == 1) e++;
					}
				}
				f = bw;
			}
			// debug(bh, bw, p, e);
			mat A(2, vl(2, 0));
			A[0][0] = p;
			A[0][1] = mod - 1;
			A[1][0] = 0;
			A[1][1] = f;
			mat B = mat_pow(A, K - 1);
			cout << (B[0][0] + B[0][1] * e % mod) % mod << "\n";
		}
	}
}

なんか割と非連結でもいけそうな雰囲気が漂っていたから誤読に全然気づかなかった…。
多分問題数もっと解けばできるできないの判断が鋭くなるんだろうな…

AGC001F

https://agc001.contest.atcoder.jp/tasks/agc001_f

わりと自然な考察で解けるからそんな難しくはないけど…でもおもしろいのは確か。

解説にあるとおりトポロジカル順を求めれば良いんですが以下はその具体的な方法です。
(i,A[i])のようにplotします。
[i,i+k)でyvに辺を張ります。こうしてできたグラフをGとします。
[i,v)でy<=A[i]を満たす点の個数をB[i]として、C[i]:=(B[i]+ΣD[child of i])として、CをG上で求めます。
あとは簡単です。bool値の配列Dを用意します。
i番目について、
Dを0から順番に見ていって、C[i]番目のfalseが入っているidxを出力する。
D[idx]をtrueにする。
を繰り返せば答えが得られます。配列DはBITで代用できて、C[i]番目を見つけるのもBIT特有の二分探索で合計O(NlogN)でできます。

コード中のBITは普通のBIT(0-origin)
BITBは二分探索機能付きBIT(1-origin)です。ややこしい。

int N, K;
int G[MAX_N];
int A[MAX_N];
int D[MAX_N], F[MAX_N];
pi Q[MAX_N];

int loop(int v) {
	if(F[v] != -1) return F[v];
	int res = D[v];
	if(G[v] != v) {
		res += loop(G[v]);
	}
	return F[v] = res;
}

void solve() {
	cin >> N >> K;
	rep(i, 0, N) {
		cin >> A[i];
		A[i]--;
	}
	set<pi> S;
	rep(i, 0, K) S.insert(pi(-A[i], i));
	rep(i, 0, N) {
		auto it = S.upper_bound(pi(-A[i], i));
		if(it == S.end()) {
			G[i] = i;
			Q[A[i]] = pi(i, i + 1);
		}
		else {
			G[i] = (*it).sec;
			Q[A[i]] = pi(i, G[i]);
		}
		S.erase(pi(-A[i], i));
		if(i + K < N) S.insert(pi(-A[i + K], i + K));
	}
	BIT bit(N);
	rep(i, 0, N) {
		bit.update(Q[i].fst, Q[i].fst + 1, 1);
		D[Q[i].fst] = bit.get(Q[i].fst, Q[i].sec);
	}
	memset(F, -1, sizeof(F));
	rep(i, 0, N) loop(i);
	BITB bb(N);
	rep(i, 0, N) bb.update(i + 1, 1);
	rep(i, 0, N) {
		int a = bb.get_index(F[i]);
		cout << a << "\n";
		bb.update(a, -1);
	}
}

AOJ 2382

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2382

証明がむずい。地雷。絶対450じゃない。数オリメダリストの力を借りました。

まず隣接してる頂点同士を結んで木を作ります。この木の辺の数をfとしましょう。
f=N-1の時、全ての頂点が直接的or間接的につながっていることになります。
その時、適当な点を根として葉からスライムを合成していけば操作はN-1回となります。
1回の操作でスライムの数はたかだか1個しか減らないのでこれが最小です。

f≠N-1としましょう。
最小回数は壁に頂点がある時2N-f-2、ない時2N-f-1であることを示しましょう。

まずN=2です。
壁にスライムがあったら、スライムがある壁にもうひとつのスライムを移動させてから合成すればいいので2回、
壁にスライムがなかったら、2つのスライムを同じ壁に追いやってから合成させればいいので3回です。
主張は成立してます。

今度はあるN(>=2)の時、上の主張が成立するとして、N+1について考えましょう。
あるスライムを動かして頂点の数がN+1からNに減少したとします。
このときf->f-1、N+1->Nとなり、Nの場合に帰着できます。よって、操作の回数は
1+2*N-(f-1)-(1or2)=2*(N+1)-f-(1or2)となり成立です。
今度は頂点の数がN+1からN+1となった時、つまり合成が起こらなかった時を考えます。
ちょっとややこしいですが、考えるとfはせいぜい1しか増えないことがわかります。
1回操作したのに、(-f)での減少効果も1しか得られないので、N+1→N+1にする操作をしても操作の回数が2(N+1)-f-(1or2)よりも
小さくならないことが言えます。

アー結局ここまで書いたのに雑になってしまいました…fが1しか増えないことを示すのも意外にややこしい。図を書けばなんとなく
わかるんですが。
あとN+1->Nに合成する時スライムが壁から離れないようにしないといけませんが、これもf=0で場合分けがあってめんどくさい。

だれか証明完成させてください…おねがいします…

int N, H, W;
vector<int> G[300010];
bool used[300010];

void loop(int v) {
	used[v] = true;
	rep(i, G[v].size()) {
		int n = G[v][i];
		if(!used[n]) loop(n);
	}
}

int main() {
	cin >> N >> H >> W;
	bool wall = false;
	rep(i, N) {
		int a, b; cin >> a >> b; a--; b--;
		if(a == 0 || a == H - 1 || b == 0 || b == W - 1) wall = true;
		G[i].pb(a + N);
		G[a + N].pb(i);
		G[i].pb(b + N + 100000);
		G[b + N + 100000].pb(i);
	}
	int K = 0;
	rep(i, N) {
		if(!used[i]) {
			loop(i); K++;
		}
	}
	if(K == 1) cout << N - 1 << "\n";
	else cout << N + K - (wall ? 2 : 1) << "\n";
}

オマケでshortest code達成しておきました。

ARC102

https://beta.atcoder.jp/contests/arc102/tasks

C
K/2が肝とわかれば。
D
2^nの辺を張って、適当に付け加えれば良さそうと思って実際そうでした。添字がめんどい。

int L;
vector<pi> G[20];
 
void solve() {
	cin >> L;
	int N = 1;
	while(L >= (1 << N)) N++;
	rep(i, 0, N - 1) {
		G[i].pb(pi(i + 1, (1 << (N - 2 - i))));
		G[i].pb(pi(i + 1, 0));
	}
	L -= (1 << (N - 1));
	int now = (1 << (N - 1));
	rer(i, N - 1, 0) {
		if(L & (1 << i)) {
			G[0].pb(pi(N - 1 - i, now));
			now += (1 << i);
		}
	}
	int M = 0;
	rep(i, 0, N) {
		M += sz(G[i]);
	}
	cout << N << " " << M << "\n";
	rep(i, 0, N) {
		rep(j, 0, sz(G[i])) {
			cout << i + 1 << " " << G[i][j].fst + 1 << " " << G[i][j].sec << "\n";
		}
	}
}

E
奇偶に分けてじっと見たら対称性があることに気づいたのでDPしました。でもよく考えると二項係数だけで行ける。
最初包除かなと思ったんですが、こんがらがってしまいました。
包除なんですが、f(集合)というよりは、f(条件)とすれば、
f(S1∨S2∨S3)=(f(S1)+f(S2)+f(S3))-(f(S1∧S2)+f(S2∧S3)+f(S3∧S1))+f(S1∧S2∧S3)
となってわかりやすくないですか?僕はわかりやすいです。

int K, N;
ll ans[4040];
ll dp[4040][4040];
 
void solve() {
	cin >> K >> N;
	C_init(4040);
	dp[0][0] = 1;
	for(int i = 0; i <= K / 2; i++) {
		rep(j, 0, K + 1) {
			if(!dp[i][j]) continue;
			ADD(dp[i + 1][j + 1], dp[i][j] * 2 % mod);
			ADD(dp[i + 1][j], dp[i][j]);
		}
	}
	for(int i = 1; i <= K / 2 + 1; i++) {
		for(int j = 0; j <= N; j++) {
			if(!dp[i][j]) continue;
			int a = N - j;
			int b = K - (i * 2) + j;
			// debug(i * 2 + 1, j, a, b, dp[i][j] * C(a + b - 1, a));
			ADD(ans[i * 2 + 1], dp[i][j] * C(a + b - 1, a) % mod);
		}
	}
	for(int i = 1; i <= K / 2 + 1; i++) {
		for(int j = 0; j <= N; j++) {
			if(!dp[i][j]) continue;
			int a = N - j;
			int b = K - ((i - 1) * 2 + 1) + j;
			// debug(i * 2, j, a, b);
			ADD(ans[i * 2], dp[i - 1][j] * C(a + b - 1, a) % mod);
			a = N - j - 1;
			b = K - ((i - 1) * 2 + 1) + j;
			// debug(i * 2, j, a, b);
			ADD(ans[i * 2], dp[i - 1][j] * C(a + b - 1, a) % mod);
		}
	}
	// rep(i, 0, 2 * K + 1) {
	// 	debug(i, ans[i]);
	// }
	for(int i = K + 1; i <= 2 * K; i++) {
		ans[i] = ans[2 * K + 2 - i];
	}
	for(int i = 2; i <= K * 2; i++) {
		cout << ans[i] << "\n";
	}
}

F
pi-1 < pi < pi+1となればpiはこれ以降動けないのでpi=iが成立しないといけないことを中心に考察を進めました。
こういうのは適当にiterationを決めて実験をダラダラしてたら解けることはわかっていたので、時間かけたら解法が降ってきました。
でも速く解こうと思ったらやっぱり性質が他にないか確かめるべきですね。もし数が動き始めたら左右どちらかにしか移動しないことに気づいてなかったのは良くなかったです。問題たくさん解けばこういう性質にもう少し敏感になれるかなぁ。

int N;
int A[MAX_N];
bool used[MAX_N];

void solve() {
	cin >> N;
	memset(A, -1, sizeof(A));
	rep(i, 0, N) {
		cin >> A[i];
		A[i]--;
	}
	int lv = -1, uv = -1;
	for(int i = 0; i < N; i++) {
		if(uv == -1) {
			if(A[i] != i) {
				uv = A[i]; lv = i;
				if((uv - lv) % 2) {
					cout << "No\n";
					return;
				}
			}
		}
		else if((uv - i) % 2) {
			if(A[i] != i) {
				cout << "No\n";
				return;
			}
		}
		else {
			if(A[i] > uv) {
				used[uv] = true;
				uv = A[i];
			}
			else if(lv == A[i]) {
				used[lv] = true;
				while(used[lv]) lv += 2;
			}
			else {
				cout << "No\n";
				return;
			}
			if(lv == uv) {
				lv = -1, uv = -1;
			}
		}
	}
	if(lv != -1) cout << "No\n";
	else cout << "Yes\n";
}

re_shaさんすごい。めちゃくちゃかしこいですね…。