CODE FESTIVAL 2017 Final,H: Poor Penguin

https://cf17-final-open.contest.atcoder.jp/tasks/cf17_final_h

お気持ちはわかるし、結構惜しいところまではいけるけど…

氷山を消すと、4辺全てに氷山がある長方形領域に分断されることがわかります。なのでそれにしたがってDPすればいいです。

といえば簡単なんですが、分断されるというアイディアに至るためにはたくさん図を描く必要があると思います。
僕は特殊な場合だけうまくいく方法を思いついたんですが、もちろんWAだし、アルゴリズムの正当性も全然証明できなくて途方に暮れてしまいました。

今回みたいな特に言い換えるパートもなく、何をやっているか正しく把握できるかが勝負になる問題は、ひたすら実験するのが吉みたいですね。

int N, M;
int X, Y;
int A[50][50];
int S[50][50];
int dp[50][50][50][50];

int get_sum(int x1, int y1, int x2, int y2) {
	return S[x2 + 1][y2 + 1] - S[x2 + 1][y1] - S[x1][y2 + 1] + S[x1][y1];
}

void solve() {
	cin >> N >> M;
	rep(i, 0, N) {
		rep(j, 0, M) {
			char c; cin >> c;
			if(c == '+') A[i][j] = 0;
			else if(c == '#') A[i][j] = 1;
			else {
				A[i][j] = 0;
				X = i; Y = j;
			}
		}
	}
	rep(i, 0, N) {
		rep(j, 0, M) {
			S[i + 1][j + 1] = A[i][j] + S[i + 1][j] + S[i][j + 1] - S[i][j];
		}
	}
	rep(x1, 0, N) {
		rep(y1, 0, M) {
			rep(x2, x1, N) {
				rep(y2, y1, M) {
					dp[x1][y1][x2][y2] = N * M;
				}
			}
		}
	}
	dp[0][0][N - 1][M - 1] = 0;
	rep(x1, 0, N) {
		rep(y1, 0, M) {
			rer(x2, N, x1) {
				rer(y2, M, y1) {
					rep(x, 0, x1 + 1) {
						rep(y, 0, y1 + 1) {
							MIN(dp[x1][y1][x2][y2], 
									dp[x][y][x2][y2] + get_sum(x, y1, x1 - 1, y2) + get_sum(x1, y, x2, y1 - 1));
						}
					}

					rep(x, x2, N) {
						rep(y, 0, y1 + 1) {
							MIN(dp[x1][y1][x2][y2],
									dp[x1][y][x][y2] + get_sum(x1, y, x2, y1 - 1) + get_sum(x2 + 1, y1, x, y2));
						}
					}

					rep(x, 0, x1 + 1) {
						rep(y, y2, M) {
							MIN(dp[x1][y1][x2][y2],
									dp[x][y1][x2][y] + get_sum(x, y1, x1 - 1, y2) + get_sum(x1, y2 + 1, x2, y));
						}
					}

					rep(x, x2, N) {
						rep(y, y2, M) {
							MIN(dp[x1][y1][x2][y2],
									dp[x1][y1][x][y] + get_sum(x2 + 1, y1, x, y2) + get_sum(x1, y2 + 1, x2, y));
						}
					}
				}
			}
		}
	}
	int ans = N * M;
	rep(x1, 0, N) {
		rep(y1, 0, M) {
			rep(x2, x1, N) {
				rep(y2, y1, M) {
					if(!(x1 <= X && X <= x2 && y1 <= Y && Y <= y2)) continue;
					MIN(ans, dp[x1][y1][x2][y2] + get_sum(x1, y1, X, Y));
					MIN(ans, dp[x1][y1][x2][y2] + get_sum(X, y1, x2, Y));
					MIN(ans, dp[x1][y1][x2][y2] + get_sum(x1, Y, X, y2));
					MIN(ans, dp[x1][y1][x2][y2] + get_sum(X, Y, x2, y2));
				}
			}
		}
	}
	cout << ans << "\n";
}

CODE FESTIVAL 2016 Final,H: Tokaido

https://cf16-final-open.contest.atcoder.jp/tasks/codefestival_2016_final_h

面白いです。良問。

まずM=1の時を考えて普通にdpすると、
f(j)=max(Ai-sum(j,i)-f(i+1)|i>=j)となります。ただしsum(j,i)=Σ(k=j...i-1)Akです。

よくある添字を1つ減らすテクニックで変形すると
f(j)=max(Aj-f(j+1), max(Ai-sum(j+1,i)-f(i+1)|i>=j+1)-Aj)
=max(Aj-f(j+1),f(j+1)-Aj)=|Aj-f(j+1)|となります。後はfをO(N)で求めればいいです。

満点解法ですが、f(x)=|x-a|のような関数を複数合成した関数f^nについてf^n(x)の値がわかればいいです。
Siをf^n(x)=iを満たすようなxの集合と定義します。
たとえば、S={{0},{1},{2},{3},{4},{5}...}にf(x)=|x-2|を作用させると、
S={{2},{1,3},{0,4},{5}...}のようになります。0と4,1と3がマージされたのがわかると思います。
このように|x-a|を作用させるということはマージをa回行うということになっているので、unionfindを使えばO(Tα(T))(T=ΣAi)でf^n(x)の値を求められます。

あとはクエリに適当に答えるだけです。

int N, Q;
int A[MAX_N];
int ans[MAX_N];
int parval[MAX_N];

UF uf;

void solve() {
	cin >> N;
	rep(i, 0, N - 1) cin >> A[i];
	uf.init(2000000 + 1);

	int prev = 0;
	rer(i, N - 1, 2) {
		int nv = A[i];
		for(int j = 1; j <= nv; j++) {
			uf.unite(prev + nv - j, prev + nv + j);
		}
		prev += nv;
	}
	rep(i, 0, 1000000 + 1) {
		parval[uf.find(prev + i)] = i;
	}
	rep(i, 0, prev + 1) {
		ans[i] = parval[uf.find(i)];
	}
	cin >> Q;
	while(Q--) {
		int x; cin >> x;
		if(prev <= x) cout << A[0] - A[1] + x - prev << "\n";
		else cout << A[0] - A[1] + ans[x] << "\n";
	}
}

CODE FESTIVAL 2017 Final,I: Full Tournament

https://cf17-final-open.contest.atcoder.jp/tasks/cf17_final_i

だいぶ時間使ったけどなんとか解けました。良問です。

順位表が求められればトーナメント上の順番は簡単に求まります。

順位表上でi番目がj番目よりも小さくならなければならない条件をi->jに辺を張った有向グラフG上で考えてみます。
Auを頂点uに書き込む値とすると、任意の辺e(v,u)についてAu < AvとなるようAを決めていく問題になりました。

ある頂点uから別のある頂点vへのpath上にある頂点の値cは、Au < c < Avを満たす必要があります。
すべてのpathとその上の頂点についてこの条件を求めれば、任意の頂点uについてAuはLu <= Au <= Ruでなければならない、つまりAuは(Lu, Ru)上になければならないのように、区間の形で条件が求まります。

そうしたら後はよくある点と区間を対応させる問題を解けば良くて(MlogM)(M=2^N)で計算できます。

こういう問題はやっぱり実験が大事ですね。でもただ実験するんじゃなくて見方を変えていろいろ手を動かすと思いつきやすいようです。

int N, A[MAX_N];
int L[MAX_N], R[MAX_N];
vector<pi> query[MAX_N];
bool ok[MAX_N];

void solve() {
	cin >> N;
	rep(i, 0, (1 << N)) L[i] = -1;
	rep(i, 0, (1 << N)) R[i] = inf;
	rep(i, 0, (1 << N)) {
		cin >> A[i]; A[i]--;
		if(A[i] != -1) ok[A[i]] = true;
	}
	if(A[0] != -1 && A[0] != 0) {
		cout << "NO\n";
		return;
	}
	if(A[(1 << N) - 1] != -1 && A[(1 << N) - 1] != (1 << N) - 1) {
		cout << "NO\n";
		return;
	}

	A[0] = 0; 
	A[(1 << N) - 1] = (1 << N) - 1;
	ok[0] = true; ok[(1 << N) - 1] = true;

	rep(i, 0, (1 << N)) {
		rep(j, 0, N) {
			if(!(i & (1 << j))) continue;
			MAX(L[i], L[i ^ (1 << j)]);
		}
		if(A[i] != -1) {
			if(L[i] >= A[i]) {
				cout << "NO\n";
				return;
			}
			else L[i] = A[i];
		}
	}
	rer(i, (1 << N), 0) {
		rep(j, 0, N) {
			if(i & (1 << j)) continue;
			MIN(R[i], R[i | (1 << j)]);
		}
		if(A[i] != -1) {
			if(R[i] <= A[i]) {
				cout << "NO\n";
				return;
			}
			else R[i] = A[i];
		}
	}
	rep(i, 0, (1 << N)) {
		if(L[i] >= R[i]) continue;
		query[L[i]].pb(pi(R[i], i));
	}

	set<pi> S;
	rep(i, 0, (1 << N)) {
		vector<pi>& vec = query[i];
		rep(j, 0, sz(vec)) S.insert(vec[j]);
		if(ok[i]) continue;
		if(S.empty()) {
			cout << "NO\n";
			return;
		}
		A[S.begin()->sec] = i;
		S.erase(S.begin());
		if(!S.empty() && S.begin()->fst <= i) {
			cout << "NO\n";
			return;
		}
	}
	cout << "YES\n";
	rep(i, 0, (1 << N)) {
		int tmp = 0;
		rep(j, 0, N) {
			if(!(i & (1 << j))) continue;
			tmp |= (1 << (N - 1 - j));
		}
		cout << A[tmp] + 1 << " ";
	}
	cout << "\n";
}

AGC016F: Games on DAG

https://agc016.contest.atcoder.jp/tasks/agc016_f

やっぱりできることとできないことの差がまだわからない…。

1と2のgrundy数が一致しないようなグラフを数え上げればいいです。
全ての頂点についてgrundy数を決めてしまえば、グラフを数え上げるのは多項式でできます。
今回grundy数はたかだか3なので、これで枝切りして通している人もいますが、まともにやる方法もあります。

grundy数が0の頂点の集合、1の集合、2の集合…と順番に取っていくことを考えます。
そこまでわかれば後は前処理を適当にとっておけばよくあるO(3^N)のDPになって通ります。

連結成分内でgrundy数を数え上げてやろうという方針をとって、全ての頂点が連結しているか包除原理で求めようとしたんですが、1を含む集合、2を含む集合、どっちも含む集合で非対称性があってできませんでした。

包除原理はやはり式に本質があると思うので、式にできない時は諦めるべきでしたね。

やっぱり想定解はすっきりした考察をしているであろうという仮定のもと問題を解きにいったほうがいいですね。

int N, M;
int G[15][15];

int to[15][(1 << 15)];
ll dp[(1 << 15)];
ll pow2[1000];

void solve() {
	cin >> N >> M;
	rep(i, 0, M) {
		int a, b; cin >> a >> b; a--; b--;
		G[a][b] = 1;
	}

	pow2[0] = 1;
	rep(i, 0, 500) pow2[i + 1] = pow2[i] * 2 % mod;

	rep(i, 0, N) {
		rep(bit, 0, (1 << N)) {
			if((bit & (1 << i))) continue;
			int tmp = 0;
			rep(j, 0, N) {
				if(!(bit & (1 << j))) continue;
				tmp += G[i][j];
			}
			to[i][bit] = tmp;
		}
	}
	dp[0] = 1;
	rep(q, 0, (1 << N)) {
		for(int bit = q; bit; bit = (bit - 1) & q) {
			if((bit & (1 << 0)) && (bit & (1 << 1))) continue;
			int pbt = bit ^ q;
			int rbt = (1 << N) - 1 - q;
			ll mul = 1;
			ll cnt = 0;
			rep(i ,0, N) {
				if(!(rbt & (1 << i))) continue;
				MUL(mul, pow2[to[i][bit]] - 1);
			}
			rep(i, 0, N) {
				if(!(pbt & (1 << i))) continue;
				ADD(cnt, to[i][bit]);
			}
			// debug(bitset<3>(q), bitset<3>(pbt), bitset<3>(bit), bitset<3>(rbt), dp[pbt], mul, cnt);
			ADD(dp[q], dp[pbt] * mul % mod * pow2[cnt] % mod);
		}
		// debug(bitset<3>(q), dp[q]);
	}
	cout << dp[(1 << N) - 1] << "\n";
}

AGC013E: Placing Squares

https://agc013.contest.atcoder.jp/tasks/agc013_e

一応知ってるテクニックの組み合わせにはなるんだけど…難しいですね。

Degwerさんの数え上げテクニック集で紹介されている問題です。

とりあえず自明DPを考えると
dp(x)=Σ(y=1...x)y^2dp(x-y)となります。こういうy=1からのΣの式はy=1の場合を分離するとうまく行くことが多いです。蟻本にも載ってますね。
そのように式変形すれば、
dp(x)=dp(x-1)+Σ(y=1...x-1)(y+1)^2dp(x-y)=dp(x-1)+Σ(y=1...x-1)y^2dp(x-y)+2Σ(y=1...x-1)ydp(x-y)+Σ(y=1...x-1)dp(x-y)
=2*dp(x-1)+2dp'(x-1)+dp''(x-1)(ただしdp'(x-1)=Σydp(x-y), dp''(x-1)=Σdp(x-y))
となって、dp'(x),dp''(x)もdp(x-1),dp'(x-1),dp''(x-1)の線型結合で表せるので行列累乗が使えます。

おいてはいけない点についても掛ける行列を多少変えれば対応できてO(MlogN)です。

Editorialでは場合の数に帰着していますが思いつくわけがありませんね。でもこういう和を場合の数に帰着して計算することがあるのは勉強になりました。

int N, M, C;
int A[MAX_N];
int L[MAX_N], R[MAX_N];

void add_interval(int l, int r) {
	if(l <= r) {
		L[C] = l;
		R[C] = r;
		C++;
	}
}

mat skip(ll k) {
	mat res(3, vl(3, 0));
	res[0][0] = (k + 1) * (k + 1) + 1;
	res[0][1] = 2 * (k + 1);
	res[0][2] = (k + 1) * (k + 1);
	res[1][0] = k + 1;
	res[1][1] = 1;
	res[1][2] = k + 1;
	res[2][0] = 1;
	res[2][1] = 0;
	res[2][2] = 1;
	rep(i, 0, 3) 
		rep(j, 0, 3) res[i][j] %= mod;
	return res;
}

void solve() {
	cin >> N >> M;
	rep(i, 0, M) cin >> A[i];

	C = 0;

	mat BB = skip(0);

	if(M != 0) {
		add_interval(0, A[0] - 1);
		rep(i, 0, M - 1) {
			add_interval(A[i] + 1, A[i + 1] - 1);
		}
		add_interval(A[M - 1] + 1, N);

		mat D(3, vl(3, 0));
		D[0][0] = 1; D[1][1] = 1; D[2][2] = 1;
		rep(i, 0, C) {
			D = mat_mul(mat_pow(BB, R[i] - L[i]), D);
			if(i != C - 1) {
				D = mat_mul(skip(L[i + 1] - R[i] - 1), D);
			}
		}
		cout << D[0][2] << "\n";
	}
	else {
		cout << mat_pow(BB, N)[0][2] << "\n";
	}
}

AGC010F: Tree Game

https://agc010.contest.atcoder.jp/tasks/agc010_f

O(N)で心配になった…。

まずある頂点vに対して、隣接している全ての頂点piに対してA[v]<=A[pi]が成り立つ場合、vからゲームを始めると必ず負けます。
なぜなら高橋君がvからどこに動かしても、青木くんがvに駒を戻せばいいです。そうすればA[v]がいずれ0となり高橋君は負けてしまいます。

なので2人はそのような頂点に駒を動かさないようにするわけですが、そうするとvからA[v]<=A[pi]を満たす頂点piに動かすのは両プレーヤーにとって最適ではないことがわかります。
なぜなら次のプレーヤーはvに駒を動かせばvから駒を動かす前の状態とほとんど同じになり、A[v]とA[pi]が1小さくなっただけとなります。
ここでA[v]とA[pi]の大小関係は変わらず、A[v]<=A[pi]を満たす頂点piが新たに出来てしまう可能性があります。

なのでA[v]>A[pi]を満たす頂点にのみ移動するのが最適で、v->piに辺を張った有向グラフでdpすればO(N)で全ての頂点について勝敗がわかります。

大小関係が本質だとわかればそんな難しくはないですね。実際コンテスト中かなり解かれています。

int N;
int A[MAX_N];
vector<int> G[MAX_N];

bool used[MAX_N];
int dp[MAX_N];

void loop(int v) {
	used[v] = true;
	dp[v] = 0; 
	rep(i, 0, sz(G[v])) {
		int n = G[v][i];
		if(A[v] > A[n]) {
			loop(n);
			dp[v] |= (dp[n] ^ 1);
		}
	}
}

void solve() {
	cin >> N;
	rep(i, 0, N) cin >> A[i];
	rep(i, 0, N - 1) {
		int a, b; cin >> a >> b; a--; b--;
		G[a].pb(b);
		G[b].pb(a);
	}
	rep(i, 0, N) {
		if(!used[i]) loop(i);
	}
	vector<int> ans;
	rep(i, 0, N) {
		if(dp[i]) ans.pb(i);
	}
	rep(i, 0, sz(ans)) {
		cout << ans[i] + 1 << " ";
	}
	cout << "\n";
}

AGC010E: Rearranging

https://beta.atcoder.jp/contests/agc010/tasks/agc010_e

O(N^4)からの高速化が思いつかなかった…。

まずAiを頂点としたグラフを考えて、互いに素ではない頂点同士をつなげます。すると各連結成分で一番小さい要素をcとしてcのうち最大のものを取るのが最適だとわかります。cをとった連結成分で次に取る要素はcと連結しているものです。

これでO(N^4)になったので高速化します。

ある頂点と連結している要素で一番小さいものを取っていく操作を繰り返します。そうすることで出来たDFS木について考えましょう。
ある頂点vの子piについて最適な数列Spiが求まっているとすると、vの最適な数列Sは先頭がvで、あとはSpiの先頭の要素のうち最大のものをpvとしてApvをSに追加し、pvはSpiから削除することを繰り返すことによって得られます。

これで主要なパートはO(N^2)でできるので間に合います。

なにかを順序を決めて取っていく問題でDFSとトポロジカル順はとても重要ですね。
無向グラフは少し使えるようになってきたと思うけど、有向グラフもすぐに書けるようになりたいですね。

int N;
int A[MAX_N];
bool used[MAX_N];
vector<int> G[MAX_N];

vi loop(int v) {
	used[v] = true;
	vector<vi> E;
	vector<int> at, res, order;

	if(v != 0) res.pb(A[v]);

	rep(i, 0, sz(G[v])) {
		int n = G[v][i];
		if(used[n]) continue;
		order.pb(i);
	}
	sort(all(order), [&](int a, int b){ return A[G[v][a]] < A[G[v][b]]; });

	rep(i, 0, sz(order)) {
		int n = G[v][order[i]];
		if(!used[n]) {
			E.pb(loop(n));
			at.pb(0);
		}
	}

	int m = sz(E);
	while(true) {
		int a = -1, tat = -1;
		rep(i, 0, m) {
			if(sz(E[i]) == at[i]) continue;
			if(a < E[i][at[i]]) {
				a = E[i][at[i]]; tat = i;
			}
		}
		if(tat == -1) break;
		at[tat]++; res.pb(a);
	}
	return res;
}

void solve() {
	cin >> N;
	rep(i, 0, N) {
		cin >> A[i + 1];
	}
	rep(i, 1, N + 1) {
		rep(j, i + 1, N + 1) {
			if(gcd(A[i], A[j]) != 1) {
				G[i].pb(j);
				G[j].pb(i);
			}
		}
	}
	rep(i, 1, N + 1) {
		G[i].pb(0);
		G[0].pb(i);
	}

	vector<int> ans = loop(0);
	rep(i, 0, N) {
		cout << ans[i] << " ";
	}
	cout << "\n";
}

Editorialが簡潔ですごい。