Codeforces Round #614 (Div. 1)

https://codeforces.com/contest/1292

A
点を共有するおじゃまマスが存在してはいけません。隣接している箇所の個数のみを保持すればいいです。

B
まずどっかの点に行ってから上or下に順番に取っていくのが最適です。点を全列挙する際にオーバーフロー
に注意しなければならないですが、僕はx > sx + t || y > sy + tでbreakしました。

C
ある辺に0を書き込んでから両端に1,2,3...とpathを延長していくのが最適です。するとコストは、
s(k) = (0を通るpathの本数)+(0,1を通るpathの本数)+...+(0,1,2,...,kを通るpathの本数)とすれば、s(N-1)となります。

ここでdp[u][v]:=uからvのpath上に0~dist(u,v)-1を書き込んだ時のs(dist(u,v)-1)の最大値

とします。path上にあってuと隣接している点をpとすれば、chmin(dp[u][v],dp[p][v]+cost[u][v])で更新できます。
vも同様にやります。ここでcostはuからvのpathを丸々含むpathの本数となるので、これを前計算で求めておきます。
これが結構難しかったです。根付き木でu,vのsubtreeの頂点数を足し引きするみたいな方針でやったんですが、適当にやったら
lca(u,v)=uの時に破滅しました。このときはcost[u][v]=(N-|subtree(p)|)*|subtree(v)|としなければならず、pをいちいち求めていると
3乗になってTLEします。なのでN=3000であることを活かしてまず最初にlca(u,v)=uとなるような頂点のpairについて頂点の親を順番にみていきながら
求め、残りのpairは|subtree(u)|*|subtree(v)|となることを利用して求めました。最初lcaが必要かなと思ったんですが結局いらずにO(N^2)でした。

上記の嘘で1WA、オーバーフローで1WAしました。絶妙にオーバーフローするんだよね…
あと更新の際pを見つけるのにdeg(u)=O(N)かかる場合があるからO(N^3)になりそうな気がするんですが、
あるuを固定するとdeg(u)かかるのがvの数、つまりN個かかるので、N deg(u)となり、uを全て見てもN 2*(N-1)にしかならずO(N^2)です。
これはかなり非直感的なので勉強になりました。

int N;
vi G[MAX_N];
int cost[3010][3010];
int dist[3010][3010];
int par[3010];
ll dp[3010][3010];
int sub[3010];

void pre(int v, int p) {
	sub[v] = 1;
	par[v] = p;
	rep(i, 0, sz(G[v])) {
		int n = G[v][i];
		if(n == p) continue;
		pre(n, v);
		sub[v] += sub[n];
	}
}

void qre(int root, int v, int p, int c) {
	dist[root][v] = c;
	rep(i, 0, sz(G[v])) {
		int n = G[v][i];
		if(n == p) continue;
		qre(root, n, v, c + 1);
	}
}

ll loop(int v, int u) {
	if(v == u) return 0;
	else if(v > u) return loop(u, v);
	else if(dp[v][u] != -1) return dp[v][u];
	else {
		ll res = 0;
		int d = dist[v][u];
		rep(i, 0, sz(G[v])) {
			int n = G[v][i];
			if(dist[n][u] < d) {
				MAX(res, loop(n, u) + cost[v][u]);
			}
		}
		rep(i, 0, sz(G[u])) {
			int n = G[u][i];
			if(dist[v][n] < d) {
				MAX(res, loop(v, n) + cost[v][u]);
			}
		}
		return dp[v][u] = res;
	}
}

void solve() {
	cin >> N;
	rep(i, 0, N - 1) {
		int a, b; cin >> a >> b; a--; b--;
		G[a].pb(b);
		G[b].pb(a);
	}
	pre(0, -1);
	rep(i, 0, N) {
		qre(i, i, -1, 0);
	}
	rep(i, 0, N) {
		int at = i;
		while(at != 0) {
			int nat = par[at];
			cost[i][nat] = (N - sub[at]) * sub[i];
			cost[nat][i] = cost[i][nat];
			at = nat;
		}
	}
	rep(i, 0, N) {
		rep(j, 0, N) {
			if(i == j) continue;
			if(!cost[i][j]) {
				cost[i][j] = sub[i] * sub[j];
			}
		}
	}
	memset(dp, -1, sizeof(dp));
	ll res = 0;
	rep(i, 0, N) {
		rep(j, i + 1, N) {
			MAX(res, loop(i, j));
		}
	}
	cout << res << "\n";
}

キーエンス プログラミング コンテスト 2020

https://atcoder.jp/contests/keyence2020

A
v = max(H, W)として、ceil(N/v)です。

B
区間スケジューリング。典型とは言え200はビビりました。

C
基本K個のSとN-K個のS+1で良いんですが、S=10^9の時だけN-K個の数を1にします。ギャグ。

D
個人的には解きやすかった。indexの距離の偶奇でAとBどちらを取るか決まります。あとはbitdpで前から順番に決めていけばよいです。
popcountすれば偶奇の判定も簡単です。

int N;
int dp[(1 << 18)][18];
int A[18], B[18];

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

	rep(bit, 0, (1 << 18)) {
		rep(i, 0, N) dp[bit][i] = inf;
	}

	dp[0][0] = 0;

	rep(bit, 0, (1 << N)) {
		rep(i, 0, N) {
			if(dp[bit][i] == inf) continue;
			int cnt = 0;
			rep(j, 0, N) {
				if(bit & (1 << j)) continue;
				int v = (__builtin_popcount(bit) - j + 50) % 2 == 0 ? A[j] : B[j];
				int pv = bit == 0 ? -1 : (__builtin_popcount(bit) - 1 - i + 50) % 2 == 0 ? A[i] : B[i];
				if(pv <= v) MIN(dp[bit | (1 << j)][j], dp[bit][i] + cnt);


				cnt += ((bit & (1 << j)) == 0);
			}
		}
	}
	int res = inf;
	rep(i, 0, N) MIN(res, dp[(1 << N) - 1][i]);
	if(res == inf) cout << -1 << "\n";
	else cout << res << "\n";
}

E
まず辺にどちらの頂点が大きいかで不等式をつけてみたところ、ある1点のみに小なりの符号が集中しているとできないことが
わかりました。逆に小なりの符号が集中しているのが、2点以上の等号による連結成分ならうまく行くだろうとなり、
構成法を色々考えていたんですが、コストの大きい頂点から辺を張っていくことを
ずっと考えていたせいでわかりませんでした。(小さい方からなら簡単です)でも偶然max(D_u,D_v)みたいなコストの辺で最小全域木を作る問題を
思い出したので、(https://atcoder.jp/contests/arc098/tasks/arc098_d)
それで全域木を作ってみた所、木上で2色彩色するだけになり簡単に構成できました。
1WA出したんですがこれは一箇所MとNを入れ替えていたためです…なんか木の場合を入力に入れたら都合よくバグってくれました。

KUPC2019

ちょっと気になった問題取り上げます。

https://atcoder.jp/contests/kupc2019/tasks/kupc2019_e

木ごとにgrundyするのかなと思ったんですが、ゲームが分割されているわけではないので今回では
あんまり役に立たないです。木ごとに単なる勝ち負けを求めても、最適moveではわざと負けるみたいな
ことをする可能性もあります。なので、(i)「手番が必ず変わらない」(ii)「手番が必ず変わる」(iii)「手番が変わるかを先手が選べる」
(iv)「手番が変わるか後手が選べる」の4パターンに分類すると良いです。
すると複数の木を見て回った後の勝敗も上の4パターンに分類できます。しかし(i)は無視できるので実質3パターンです。
(iii)が一つでもあるなら、残りのN-1の勝敗パターンを見てから先手が決められるので先手の勝利です。
(iii)がないなら、(iv)を先に選んでしまうと後手が残りの木の勝敗を見て勝利するように行動できるので負けです。
よって(ii)を順番に選んでいくゲームになり、(ii)が偶数個なら後手勝ち、奇数個なら先手勝ちとなります。

https://atcoder.jp/contests/kupc2019/tasks/kupc2019_f

まず区間ではモンスターを一点集中させるのが良いことを証明できます。これは簡単で、
区間でb_l,...,b_rとモンスターを配置したなら、argmin(A_i-b_i)に集中させても解は小さくなりません。
事前にどの地点にモンスターを配置するか決めてしまえば、解は(区間のうち配置地点を含むものの和)-(配置地点の勇者の数)
となります。ここで
dp(i):=0~i地点まで見てi地点にはモンスターを配置すると決めた時のコストの最大値とすれば、
dp(i)からdp(j)への遷移は(始点が(i,j]でjを含むような区間の和)-A_jとなり、これはjで平面走査すればO(NlogN)で行えます。
全てのiについて見ていっても合計でO(N^2logN)です。

https://atcoder.jp/contests/kupc2019/tasks/kupc2019_g

abca
bcab
cabc
abca

これが思いつけるかが全てです。あとは端を延長することによって達成できます。

初手累積和

数列{a_i}に対して区間addの操作をする時、
b_i=a_i-a_{i-1}(a_{-1}=0とする)と{b_i}を定義すると以下のように言い換えられます。

「{a_i}で[l, r)の範囲の値を+a」 <=> 「{b_i}でlを+a、rを-a」

こうすると、区間の操作が1点操作になって見やすい形になります。{a_i}を復元するには累積和を取ればいいです。
{a_i}を微分したものが{b_i}になっていると見ることも出来ます。よって{a_i}についてΣf(x)を区間で足す時は
{b_i}でf(x)を足し引きすると言い換えられる可能性が高いです。

imos法はまさにこれをやっているわけですが、imos法を陽に使わない場合でもこのテクニックを使う問題があります。
https://atcoder.jp/contests/cf17-final/tasks/cf17_final_e
https://atcoder.jp/contests/agc010/tasks/agc010_b ([l, r)に含まれるjにΣ_{i=1}^{j-l+1} 1 を足すと見れます)
https://atcoder.jp/contests/arc080/tasks/arc080_d

微分ではなく積分を取る問題も有ります。
https://atcoder.jp/contests/dwacon5th-final/tasks/dwacon5th_final_b

でもこのパターンは流石にXOR位しか出ないんじゃないかなと思っています。

いかにもこのパターンなのに逆に見づらくなる問題も存在します。
https://atcoder.jp/contests/jsc2019-qual/tasks/jsc2019_qual_c

微分をすると、操作を通して同じマスを2回以上選ぶことが出来ない条件がわかりにくくなります。
このような問題は区間を左端sortして順番に見て行ったり、iterationを回すことが肝となることが多いです。
実際上で挙げた
https://atcoder.jp/contests/cf17-final/tasks/cf17_final_e
はiterationを回しても解けます。

第6回 ドワンゴからの挑戦状 予選

https://atcoder.jp/contests/dwacon6th-prelims

うーん

A
後から見ていけばいいです。

B
400はすぐわかったんで実装して後はOEIS入れればわかるやろwってなったのでやったんですがsampleが全然合わない。
愚直も書いたんですがそれもバグらせて散々でした。
解法ですが、各n-1区間で掛ける係数がわかればいいです。区間[x_i,x_i+1)のうち、スライムj(<=i)由来のものは
(n-1)!/(i-j+1)と求まるので、あとは簡単な足し算で求まります。400はiを削除してnが一つ減った場合に帰着するdpです。
ちなみに係数をOEISに入れても出てきません(絶望)

C
積の期待値をtupleの数に言い換える典型です。しかしABDみた後に着手したので遅かった。

D
あきらかに条件がゆるゆるなので適当に貪欲にやって、最後の6要素だけは全探索すればいいと思ったんですが嘘でした。
800点なんだからなんか非自明な条件が存在するだろうと思えればよかったんですがね…
ある頂点に集中するのがヤバイのでそうなってないかを確認しながら先頭から決めていけばよいです。

Educational Codeforces Round 80 (Rated for Div. 2)

https://codeforces.com/contest/1288

突然ですがblog再開です。

A
凸なので解が小さくなり始めたらbreakすれば良いんですが条件を若干間違えてWA...
コンテスト後100000までやるってのを見て賢いと思いました。

B
bの桁数を固定してしまえばbは9999....のようにしかなりえません。Aは範囲内ならなんでもいいです。

C
数列内の不等式が成り立っていればあとはa_m <= b_mであればいいです。
a_mとb_mを固定して数列ごとに独立に場合の数をdpで求めました。
しかし長さ2mの数列に見立てれば一つのdpで済んだ上に、経路数に帰着してしまえばO(1)で済みます。

D
maxと見えたのでにぶたんしたら出来ました。2つの値のorを取ったら全bitが埋まっているという条件になるので前計算で
そのような値のpairを全て求めておきます。

E
最大が本質パートです。数列の先頭にn n-1 n-2 ... 1を追加すると、同じ数の間に入っている数の種類数を求めればいいことになります。
例えば5 4 3 2 1 3 5 1 4 1なら1同士の間には{3, 5}と{4}が含まれるので、max(|{3,5}|, |{4}|)+1=3を答えにすればいいです。
最初計算量を下げられる気がしなかったんですが(というか種類数を扱うので平方分割だと思った)、2次元座標で可視化したらあっさりでした。indexが小さい順から見ていって、同じ数内では一番最後に見たものだけ管理していけば良いです。これはBITで実装できます。

F
なんか線形計画問題にしてみたんですがよくわからなかったです…

SAT Solver作ってみた

この記事は
https://adventar.org/calendars/4520
の22日目の記事として書かれました。昨日はRisebbitさんのシークエント計算の自動証明を実装してみたでした。

夏休みなんかノリでSAT Solverを作ったんで、大変だったところとか書いておきます。

SATとは?


論理式充足問題と言います。例えば (b_1 \rightarrow b_2) \land(b_2 \land (\lnot b_3 \lor \lnot b_1))を充足する変数 b_1, b_2, b_3の真偽の割り当ては存在するか?みたいな感じです。この場合なら、 b_1=false, b_2=true, b_3=trueが条件を満たしますね。通常はCNF形式という形に変換してから解きます。上の例なら (\lnot b_1 \lor b_2) \land (b_2) \land (\lnot b_3 \land \lnot b_1)となります。このように \lorで結合された"節"を更に \landで結合した形で表されます。全ての論理式がCNFに変換できることは示せます。

SATが解けて何か嬉しいんか?


SATは単純ゆえ高速化の技術が進歩しています。NP完全問題にかかわらず変数の数が105オーダーの問題も解けるというから驚きです。解きたい問題がSATに帰着さえできれば解ける可能性があるということでもあります。実際以下のような問題で成功を収めているようです。

プランニング (SATPLAN, Blackbox) [Kautz+, 1992]
自動テストパターン生成 [Larrabee, 1992]
ジョブショップスケジューリング [Crawford+, 1994]
有界モデル検査 [Biere, 2009]
ソフトウェア検証 (Alloy) [Jackson, 2006]
書換えシステム (AProVE) [J¨urgen+, 2004]
インテル社の i7 プロセッサの検証 [Kaivola+, 2009]
Eclipse のコンポーネント間の依存解析 [Le Berre+, 2009] 
解集合プログラミング (clasp) [Gebser+, 2012]
Linux のパッケージマネージャである DNF の依存性解決
制約充足問題 (Sugar) [Tamura+, 2009]
 ▶ オープンショップスケジューリング問題の未解決問題の求解[Tamura+, 2009]
 ▶ パッキング配列問題の未解決問題の求解 [則武+, 2013]

まあこういうのは建前でなんかNP完全問題を解いてるってだけで興奮しませんか…?

SATの高速化技術


ベースにCDCLというアルゴリズムがあり、実装技術、経験的に良いとわかっているヒューリスティックスがたくさんあります。その中から実装したものを紹介します。詳しくは参考資料を見てください。

DPLL


簡単に言ってしまえばバックトラックアルゴリズムです。変数の真偽を決めていって、失敗したら真偽値を変えて続行という感じです。ある変数の真偽を固定した時、他の変数の真偽がどれだけ決まるか計算することを 単位伝播 と呼んだりします。SAT Solverで一番最初に出てきたアルゴリズムであり、CDCLの礎にもなっています。

CDCL



CDCLに関してはとてもわかりやすいスライドがあるのでこちらをご覧ください。



簡単に何をやっているか説明していると、DPLLの要領で順番に真偽を決め単位伝播し、矛盾したらその原因となった割り当てを特定し、その割り当ての真偽を反転させたものを新しく追加する感じです。新しく足された節を学習節と呼びます。学習節のおかげで同じような論理式の割り当てをした時に矛盾が早期の段階で起こるようになり、効率的な探索が行えるようになります。

VSIDS


Variable State Independent Decaying Sumの略です。変数に得点をつけておき、得点の高い順番から真偽割り当てを行うようにします。Decayingとあるように、定期的に全変数の得点を割合で減らします。(0.5倍とか)

学習節の削除


CDCLを回すと大量の学習節が生まれます。それらにも序列があって、論理の肝になるやつと、ただただ(速度面で)単位伝播の邪魔になるものもあります。それらを区別するために学習節にも得点をつけ、その得点が低い順から定期的に削除するようにします。得点は真面目にやるとLBD(Literal Blocks Distance)などの方式がありますが、自分の実装ではVSIDSのようなスコアを使っています。実装が楽なんでね。

リスタート


文字通り今までの変数割り当てを解除して最初からやり直すことです。すると探索範囲が変わって今までとは違うタイプの学習節が獲得できるのではないかというアイディアです。

監視リテラル


VSIDS、学習節の削除、リスタートはヒューリスティックですが、これは実装面での改善です。
実は節の中で常に見ておく必要がある変数は2つだけです。2つ真の変数を見ておき、どちらかが偽になったら真の変数を節中で探します。もし2つ見つからなかったら、もうひとつの変数は必ず真である必要があるので真の割り当てをします。2つ見つかったら、監視中ではない方を新たに監視すればいいです。これにより単位伝播の速度がだいぶ改善されます。
これは実装が辛いです。いろんなパターンがあって結構バグりました。資料はたくさん上がってるんですが全てのパターンを網羅できているものは少なかったです。

実測


Minisatという軽実装で実用的なsat solverがあるのでそれと比較しました。

CNFのタイプ 自作sat Minisat
case1 数独(変数729個,節3257個) 120.6s 2.942s
case2 変数60個,節937個のUNSAT 10.82s 7.096s
case3 変数41個,節224個のSAT 4.647s 1.376s

変数が3000個とかになると、Minisatでは一瞬、自作satでは永遠に解けないみたいな問題がたくさんありました。

1sあたりのバックトラック回数 Case1 case2 case3
自作sat  1.731 \times 10^4  2.586 \times 10^4  8.749 \times 10^4
Minisat  1.505 \times 10^5  1.345 \times 10^5  4.409 \times 10^5



ちなみにcase1の数独

9 0 0 0 0 0 1 8 0
0 0 0 4 0 0 0 0 0
7 0 0 0 0 3 0 0 0
0 4 0 1 0 0 0 0 0
0 0 0 0 8 0 0 2 0
0 3 6 0 0 0 0 0 0
0 0 0 0 2 0 0 0 0
0 0 0 6 0 0 0 0 3
0 5 0 0 0 0 4 0 0

をCNFに変換したものを使いました。0が空白マスです。一応難易度:難となっていたので難しいはず?


改善点


バックトラック回数からわかるようになんか遅い。何がボトルネックになっているのかよくわからないが特定する。制作期間中の後半ずっと考えてたんですけどわかりませんでしたね…データ構造が悪いのかな…

リスタート戦略、学習節の得点のヒューリスティックスがあんまり良くない気がするのでそこら辺を改善する。リスタート戦略に関しては実装していない可能性すらある。(!?)(4ヶ月前なので忘れてしまった)

明日はiojjooさんのゲームの話です。楽しみですね。

コード


https://github.com/omosan0627/sat_solver

参考資料


https://www.slideshare.net/sakai/satsmt
http://www-erato.ist.hokudai.ac.jp/docs/seminar/nabeshima.pdf
http://minisat.se/
超自己流! 競プロ解説記事の書き方 - Mister雑記 (この記事はここの方法で書かれました)