AOJ2405: 姉妹港

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

解法をyosupoさんのブログとか見て理解したのでメモしておきます。

とりあえずどっかで多角形を切り開いて区間の問題にします。
普通に考えるとO(N^2)のDPになって、更新式は
[l r]に区間が張られているとき
dp(l,r)=dp(l+1,r-1)+dp(l,a)*dp(a+1,r)
[l r]に区間が張られていない時
dp(l,r)=dp(l,a)*dp(a+1,r)
ただしaはlから張られている区間のうちr未満で最大のものです。

ここから計算量を落としたいのですが、dp(区間)みたいな形で更新できないか考えてみます。
まずdp(l+1,r-1)だけに注目すると、dp(l+1, r-1)=dp(l+1,b)*dp(b+1,r-1)=dp(l+1,b)*dp(b+1,c)*dp(c+1,d)...*dp(x,r-1)
のようになって[l+1,b],[b+1,c]...[x,r-1]はすべて区間なので予め求めておけばdp(l+1,r-1)も求まります。
dp(l,a),dp(a+1,r)も同じように出来て、dp(l+1,r-1),dp(l,a),dp(a+1,r)を求める際に使われる区間にかぶりはありません。
さらに、使われる区間は[l,r]にカバーされるので、dp(l,r)を求める際の1回しか使われません。
よって区間を長さ順にsortしておけばならし計算O(M)でdp(0,n)を求めることができます。

実装の際は区間を保存するのではなく、区間の左の座標を覚えておくと実装が楽になります。

dpはまず定式化することが重要で、次に遷移をみてオーダーを減らすのが基本です。
少しadhocですが、基本に忠実にやれば解ける問題ではありました。

CODE FESTIVAL 2016 Grand Final,G: FESTIVAL

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

昇順に並んでいる数列S{1,a,b,c,...X}があって、隣り合っている数の比がa(自然数)より小さいとします。
するとX以下の自然数NはSの線形結合で表されて、係数の和はO(a*loga(N))となります。

例えばS={1,2,5,13}でa=3,N=12なら
N=5*2+2*1と表されて係数の和は3となります。

帰納法で簡単に示せます。

これを使うと今回の問題が解けます。

FESTIVAL....LFESTIVAL...L...みたいなのを考えて、
f(k):=k個FESTIVA繰り返した文字列でFESTIVAが部分列として何個あるかとすると、
k個目のあと何個Lを置くかをckとしてN=c1f(1)+c2f(2)...cmf(m)となります。
fは簡単なdpで表されて、f(8)=1716らへんから隣接した数同士の比が2未満になることがわかります。
またm=600とするとf(600)=5*10^15なので、まずNを5*10^15未満にするのに200個位Lが必要で、
5*10^15から1716未満にするのに(2-1)log2(5*10^15)=50個程度必要です。あとはf(1)..f(7)で作れば良いのですが、これも100個程度でできるので、合計600*7+200+50+100=4550個程度でできます。

別に2の累乗じゃなくてもlogっぽい数列だったらこういうことできるんですね。

ll N;
ll dp[610][8];
ll f[610];
ll ans[610];
 
void solve() {
	cin >> N;
	rep(i, 0, 7) dp[0][i] = 1;
	f[0] = 1;
	rep(i, 1, 600) {
		dp[i][6] = 1;
		rer(j, 6, 0) {
			rep(k, 0, i + 1) dp[i][j] += dp[k][j + 1];
		}
		f[i] = dp[i][0] + f[i - 1];
	}
	rer(i, 600, 0) {
		while(N - f[i] >= 0) {
			N -= f[i];
			ans[i]++;
		}
	}
	string res;
	rep(i, 0, 600) {
		res += "FESTIVA";
		rep(k, 0, ans[i]) res += 'L';
	}
	cout << res << "\n";
}

CODE FESTIVAL 2016 Grand Final,D: Dice Game

https://beta.atcoder.jp/contests/cf16-exhibition-final-open/tasks/cf16_exhibition_final_d

面白い。

petrが赤のサイコロを選ぶ確率をX,touristがiが出た時に赤という確率をxiとします。するとtouristが勝つ確率は
f(X,x1...x6)=1-X+Σ((pi+qi)X-qi)xiとなります。
touristはこの値を大きくしようとするので、(pi+qi)X-qi>=0ならxi=1,そうでないならxi=0とします。
petrはtouristがこの戦略をとった時にfが一番小さくなるようなXを選びます。
(pi+qi)X-qiの正負が変わるのはX=qi/(pi+qi)の時しかなく、fは一次式なので、X=0,1,qi/(pi+qi)だけ試せばよいです。

もうちょっとミニマックスを意識出来たらよかったかなぁ。

double P[10], Q[10];

double f(double a) {
	double ans = 1 - a;
	rep(i, 0, 6) {
		ans += max(0.0, (P[i] + Q[i]) * a - Q[i]);
	}
	return ans;
}

void solve() {
	rep(i, 0, 6) {
		int a; cin >> a;
		P[i] = a / 100.0;
	}
	rep(i, 0, 6) {
		int a; cin >> a;
		Q[i] = a / 100.0;
	}
	double ans = min(f(0), f(1));
	rep(i, 0, 6) {
		ans = min(ans, f(Q[i] / (P[i] + Q[i])));
	}
	cout << ans << "\n";
}

AGC012C: Tautonym Puzzle

https://agc012.contest.atcoder.jp/tasks/agc012_c

12345...12345...みたいに並べて2^n関連でやることは見えたけどそこからが思いつかなかった。
要素入れ替えたりを考えたけど、想定解は挿入でした。

やっぱり秩序なくやるよりも、定式化できそうな良い構造に沿ってやるのが良さそうですね。

ll N;

void solve() {
	cin >> N;
	ll a = 0, n = 1;
	while(n * 2 <= N + 1) {
		a++; n *= 2;
	}
	vector<int> ans;
	ll cn = N + 1 - n;
	rep(i, 0, a) {
		if(cn & (1LL << i)) {
			cn -= (1LL << i);
			ans.pb(99 - i);
		}
		ans.pb(i);
	}
	rep(i, 0, 100) ans.pb(i);
	cout << sz(ans) << "\n";
	rep(i, 0, sz(ans)) {
		cout << ans[i] + 1 << " ";
	}
	cout << "\n";
}

CODE FESTIVAL 2017 Exhibition,A: Awkward

https://cf17-exhibition-open.contest.atcoder.jp/tasks/cf17_exhibition_a

包除原理してpathを何本含むかでdpすれば良いということはわかりました。
ただ自信がなく解法見てやっぱり合ってたってなりました。
これ絶対debug時間かかるやつだと思いましたがあっさりサンプル1発で通ってACでした。(謎)

多分包除原理に自信が持てなかったのではと思ってます。包除原理はまず具体的な形で数式を書いて後で同じにできそうなものをまとめるという感じです。あと木dpもあやふやだったので復習しておくと、2つの木のmergeがO(l*r)(lとrは左右の木の大きさ)でできればO(N^3)っぽいけどO(N^2)なやつになります。配列の初期化に注意しなくてはなりませんがO(l+r)は許されると覚えておけばよさそうです。

int N;
vi G[MAX_N];
ll dp[2010][2010][3];
int ts[2010];

void loop(int v) {

	int n = sz(G[v]);
	ts[v] = 1;
	rep(i, 0, n) {
		int to = G[v][i]; 
		loop(to);
		ts[v] += ts[to];
	}

	int m = ts[v];

	vector<vl> tdp[2] = {vector<vl>(m + 1, vl(3, 0)), vector<vl>(m + 1, vl(3, 0))};
	tdp[0][1][0] = 1;
	int now = 0, nex = 1;
	int cnt = 1;
	rep(i, 0, n) {
		int to = G[v][i];

		rep(j, 0, cnt + ts[to] + 1) {
			rep(k, 0, 3) {
				tdp[nex][j][k] = 0;
			}
		}

		rep(j, 0, cnt + 1) {
			rep(k, 0, ts[to] + 1) {
				rep(l, 0, 3) ADD(tdp[nex][j + k][0], tdp[now][j][0] * dp[to][k][l] % mod);
				rep(l, 0, 3) ADD(tdp[nex][j + k][1], tdp[now][j][1] * dp[to][k][l] % mod);
				rep(l, 0, 3) ADD(tdp[nex][j + k][2], tdp[now][j][2] * dp[to][k][l] % mod);

				if(k < ts[to]) {
					ADD(tdp[nex][j + k][1], tdp[now][j][0] * dp[to][k + 1][0] % mod * 2 % mod);
					ADD(tdp[nex][j + k][1], tdp[now][j][0] * dp[to][k + 1][1] % mod);
					ADD(tdp[nex][j + k][2], tdp[now][j][1] * dp[to][k + 1][0] % mod);
					ADD(tdp[nex][j + k][2], tdp[now][j][1] * dp[to][k + 1][1] % mod * fiv[2] % mod);
				}
			}
		}
		cnt += ts[to];
		swap(now, nex);
	}
	rep(j, 0, m + 1) 
		rep(k, 0, 3) {
			dp[v][j][k] = tdp[now][j][k];
		}
}

void solve() {
	cin >> N;
	C_init(N);
	rep(i, 0, N - 1) {
		int a; cin >> a; a--;
		G[a].pb(i + 1);
	}
	loop(0);

	ll res = 0;
	rep(j, 0, N + 1) {
		rep(k, 0, 3) {
			if((N - j) % 2 == 0) ADD(res, dp[0][j][k] * fac[j] % mod);
			else ADD(res, mod - dp[0][j][k] * fac[j] % mod);
		}
	}
	cout << res << "\n";
}

CODE FESTIVAL 2017 Elimination Tournament Round 3,F: Unicyclic Graph Counting

https://cf17-tournament-round3-open.contest.atcoder.jp/submissions/2045180

こんなのサイクルが1つあるだけのグラフ以外に考察の余地無いだろで、DPに行けたのは良かったです。

dp[i][j][k]:=i番目まで見て、j点サイクルに使って、サイクルの次数がkの時のある値として、
dp[i][j][k]*(N-j-1)!*(k-1)!/2みたいなのを足し合わせれば答えになるだろっていうところまで行きましたが、式を間違っていたためサンプル通らず…
editorialの式見て少し直してAC。

まずWA出すと方針が間違っているのではと心配になるんですよねぇ。
自信持てるようになるまで精進しないと

int N;
ll dp[2][310][610];
int d[310];

bool is_two() {
	rep(i, 0, N) if(d[i] != 2) return false;
	return true;
}

void solve() {
	cin >> N;
	C_init(2 * N);
	rep(i, 0, N) cin >> d[i];

	if(is_two()) {
		cout << fac[N - 1] * fiv[2] % mod << "\n";
		return;
	}

	dp[0][0][0] = 1;
	int now = 0, nex = 1;
	rep(i, 0, N) {
		memset(dp[nex], 0, sizeof(dp[nex]));
		rep(j, 0, N) {
			rep(k, 0, 2 * N + 1) {
				if(!dp[now][j][k]) continue;
				if(d[i] >= 2) {
					ADD(dp[nex][j + 1][k + d[i] - 2], dp[now][j][k] * fiv[d[i] - 2]);
				}
				ADD(dp[nex][j][k], dp[now][j][k] * fiv[d[i] - 1] % mod);
			}
		}

		swap(now, nex);
	}


	ll res = 0;
	rep(j, 3, N) {
		rep(k, 1, 2 * N + 1) {
			if(!dp[now][j][k]) continue;
			ADD(res, dp[now][j][k] * k % mod * fac[N - j - 1] % mod * fac[j - 1] % mod * fiv[2] % mod);
		}
	}
	cout << res << "\n";
}

てか冒頭の式なかったら結構難しいですよねこれ。有名事実なのかなぁ。

CODE FESTIVAL 2016 Grand Final,F: Intervals

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

まず真ん中の区間を動かさないで最小を達成する方法があることが示せます。
なので右左にそれぞれN/2個の区間を割り振ればいいです。
そして貪欲が厳しそうだと思えるのでDPをしようとするんですが、ここが思いつきませんでした。

大きい区間から真ん中に置いていきます。次の区間は真ん中から他の区間を押し出すように挿入します。
あとはdp[i][j][k]:=(右i個左j個真ん中k個(0or1)おいた時の最小コスト)で解けます。

要はDPに落としこむために順番を変えて見ればよかったわけですね。

あとsort関数で>=を使ってバグらせたんですが、今まで経験なかったのが不思議。明確に>にしましょう。

int N;
pl I[5010];
ll dp[2510][2510][2];

bool compare(const pl& p1, const pl& p2) {
	return (p1.sec + p1.fst) > (p2.sec + p2.fst);
}

void solve() {
	cin >> N;
	rep(i, 0, N) {
		ll l, r;
		cin >> l >> r;
		I[i] = pl(l, r);
	}
	sort(I, I + N, compare);

	int LN = (N - 1) / 2, RN = N / 2;

	rep(i, 0, LN + 1) {
		rep(j, 0, RN + 1) {
			dp[i][j][0] = linf;
			dp[i][j][1] = linf;
		}
	}

	dp[0][0][0] = 0;

	rep(i, 0, LN + 1) {
		rep(j, 0, RN + 1) {
			rep(k, 0, 2) {
				if(dp[i][j][k] == linf) continue;
				int id = i + j + k;
				ll l = I[id].fst, r = I[id].sec;
				if(i < LN) MIN(dp[i + 1][j][k], dp[i][j][k] + (l + r) * (i + 1) - l);
				if(j < RN) MIN(dp[i][j + 1][k], dp[i][j][k] + (l + r) * (j + 1) - r);
				if(k == 0) MIN(dp[i][j][k + 1], dp[i][j][k] + l * LN + r * RN);
			}
		}
	}
	cout << dp[LN][RN][1] << "\n";
}