NIKKEI2019-2-QUAL E. Non-triangular Triplets
考察
をそれぞれとしよう。
実験をして、の右辺はすべての要素になるのではないかと思う
- もし右辺にC以外の要素があるなら、の形の式があって、1つ目の式の左辺のCと2つ目の式のnot Cを交換しても条件を満たす。 これを繰り返せばすべての式の右辺をCの要素にできる
- とりあえず必要条件を考えてみる。A,Bの要素の和がCの要素の和より大きい時は明らかに不可能。この条件を整理するととなる。
- どうせ十分条件だろうと予想して(パチンコ)実験をしてみる。この条件をギリギリ満たすもので実験するとなにか見えそうなのででやってみる
(7, 10, 17) (5, 11, 16) (3, 12, 15) (6, 8, 14) (4, 9, 13)
- Aの方を2ずつ減らして、Bの方を1ずつ増やすことでCのほうを1ずつ減らすとなんかうまくハマりそう。実際に一般のK, Nでこの構築方法をしたときにうまく行くかを不等式評価してみると なんかうまく行く。Nが偶数の場合もほぼ同様にできることがわかる。
感想
序盤の考察ルートは良かった。十分条件っぽいのを見つけてからがただのパズルにしか見えない。(なにか典型構図がある?)
パズルはパチンコなので苦手
void solve() { int n, k; cin >> n >> k; if (n < 2 * k - 1) { cout << -1 << endl; return; } vector<int> a(n), b(n), c(n); for (int i = 0; i < (n + 1) / 2; i++) { a[i] = k + n - 1 - i * 2; b[i] = k + n + n / 2 + i; c[i] = k + 3 * n - 1 - i; } for (int i = 0; i < n / 2; i++) { a[i + (n + 1) / 2] = k + n - 2 - i * 2; b[i + (n + 1) / 2] = k + n + i; c[i + (n + 1) / 2] = k + 3 * n - 1 - (n + 1) / 2 - i; } for (int i = 0; i < n; i++) { cout << a[i] << ' ' << b[i] << ' ' << c[i] << endl; } return; }
Educational Codeforces Round 96
Dashboard - Educational Codeforces Round 96 (Rated for Div. 2) - Codeforces
F. Realistic Gameplay
区間の始めに余っている弾を捨ててreloadする必要はない。(余っている分を使えばいいので)したがってreloadは区間の終わりに時間の余裕があるときのみ考えればよい。
とすると状態数、遷移のDPができる。
void solve() { int n; ll k; cin >> n >> k; vector<ll> l(n), r(n), a(n); for (int i = 0; i < n; i++) cin >> l[i] >> r[i] >> a[i]; vector<ll> dp(n + 1, LLINF); dp[0] = 0; for (int i = 0; i < n; i++) { if (dp[i] == LLINF) continue; ll rem = k; ll use = 0ll; for (int j = i; j <= n; j++) { if (j == n) { chmin(dp[n], dp[i] + use); break; } if (rem + k * (r[j] - l[j]) < a[j]) break; // いくら余るか ll tmp = rem + k * (r[j] - l[j]) - a[j]; if (tmp > 0) tmp = (tmp - 1) % k + 1; use += a[j]; dmp(i, j, rem, tmp, use); // 間で補充 if (j + 1 < n && r[j] < l[j + 1]) chmin(dp[j + 1], dp[i] + use + tmp); // 区間の最後で補充 if ((a[j] - rem + k - 1) / k < r[j] - l[j]) chmin(dp[j + 1], dp[i] + use + tmp % k); rem = tmp; } } if (dp[n] == LLINF) cout << -1 << endl; else cout << dp[n] << endl; return; }
G. Yet Another DAG Problem
頂点に割り当てる値は頂点数以下でよいことに着目する。(これ超本質)
とする。愚直にやるならば更新でを割り当てる頂点集合を全部みてとかになる。頂点集合にを割り当てられるかどうかは、 から到達可能な頂点全てに以下が割り当てられているかどうかで判定できる。
これでは間に合わないのでグラフがDAGであることを利用する。 を割り当てる頂点をトポロジカル順序で前のものから順に見ていくことにすれば、トポロジカル順序が後の点が前の点から到達可能であるのにもかかわらず 同時にが割り当てられるということはない。(前の点が追加された時点で、後の点は前の点から到達可能ならば以下が割り当てられているはず)
よってでこのDPができる。あとは復元すればOK。
void solve() { int n, m; cin >> n >> m; vector<ll> c(n, 0); Graph g(n); for (int i = 0; i < m; i++) { int a, b, w; cin >> a >> b >> w; a--; b--; c[a] += w; c[b] -= w; g.add_edge(a, b); } auto topo = topological_order(g); vector<int> reach(n, 0); for (int i = n - 1; i >= 0; i--) { int v = topo[i]; for (auto e : g[v]) { reach[v] |= (1 << e.to); reach[v] |= reach[e.to]; } } // dp[i][j] := jの頂点に[0, i]を割り当てるときの最小値 auto dp = vect(n + 1, vect(1 << n, LLINF)); auto prev = vect(n + 1, vect(1 << n, -1)); dp[0][0] = 0; for (int i = 0; i < n; i++) { // iを割り当てる頂点をトポロジカル順に追加 for (int k = 0; k < n; k++) { for (int j = 0; j < (1 << n); j++) { if (dp[i][j] == LLINF) continue; if (chmin(dp[i + 1][j], dp[i][j])) prev[i + 1][j] = j; int v = topo[k]; if ((j >> v) & 1) continue; if ((reach[v] & j) == reach[v]) { if (chmin(dp[i][j | (1 << v)], dp[i][j] + c[v] * i)) prev[i][j | (1 << v)] = prev[i][j]; } } } } vector<ll> ans(n, 0); int S = (1 << n) - 1; int X = n; while (X > 0 && S != 0) { int T = prev[X][S]; int U = S ^ T; for (int i = 0; i < n; i++) { if ((U >> i) & 1) ans[i] = X; } S = T; X--; } cout << ans << endl; return; }
Educational Codeforces Round 97
Dashboard - Educational Codeforces Round 97 (Rated for Div. 2) - Codeforces
E: Make It Increasing
列を区切ってLISすればOK
F: Emotional Fishermen
順列の数え上げだし挿入DPだろうと決め打って考えたが解けなかった。この問題はよくある挿入DPのようにソートして昇順or降順に要素を埋めながら列を構成するというよりは、前から順に好きな要素で埋めていくという方法がうまくいく。
として最大値が更新される場合とそうでない場合の遷移を考える。累積和を使って高速化すると
G Death DBMS
明らかにAho-Corasickを使ってくれという感じの問題なので使う。するとSuffix Linkからなる木上で、各頂点から根までの最大値を求めるクエリにたくさん答えたくなるのでHLDで殴った。 Editorialで賢い方法が紹介されていた。文字列集合が重複なしであるとすると、Aho-Corasick automatonのあるstateにマッチしうる文字列(suffix linkを辿っていってマッチするもの)の種類数はで抑えられる。なぜかというと各長さの文字列についてマッチしうるのは高々一つなので、となるからである。なので木をexit linkで愚直に根まで辿っていっても程度で通る。
JAG 模擬国内2020 H 辺が先か,頂点が先か (AOJ 3208 Edge or Vertex)
問題文 : Aizu Online Judge
LPの定式化から2つのアプローチができて面白かったのでそれについて説明する。
考察1
先手が初めに各辺に確率を割り振り、後手はそれに応じてを確率で選ぶとする。値 を先手は最大化、後手は最小化する時、Xの値はいくらかという問題になる。 まず後手はあるpure strategy(ある選択肢を確立1で選ぶ)が最適になる。すなわち、先手の戦略に対し、各頂点ごとにを計算し、これが最小となる頂点を確立1で選ぶのが後手の最適な戦略である。この考察を踏まえると、次のようにLPで定式化できる。
考察2
与えられるグラフに閉路が存在する場合、pは閉路の辺に均等に確率を割り振ればXの値を0にできる。また、後手はすべての頂点に均等に確立を割り振る戦略でXの値を必ず0以下にできる。 よって、グラフに閉路が存在するなら、解は0が言える。
そうでないとき、が負になる頂点が少なくとも1つは存在する。よってDAGの時、解は負の数が言える。
LP解法1
考察1での定式化では、maximizeの対象にminが入っており扱いづらい。こういう際の常套手段として新しい変数を加えminを不等式制約で表現するというのがある。それを行うと、
自分はここで詰まってしまった。双対とっても綺麗になりそうもないし(もしかしたらなるかもしれませんが)いかんせんの制約が邪魔で変形や解釈が難しかった。
ここ(Lesson 35: Game Theory and Linear Programming)のスライドを眺めてみると、Sの符号がわかっているときは(より一般にmatrix gameでできる)いい感じの変形があるということがわかった。今回の場合、考察2からDAGで解が負になる場合だけを考えればよいためこの変形が利用できる。 具体的にはとおくと、
となるが、よく見るとSが消去できることがわかる。よって問題は
となる。これは「各頂点を始点とするパスの長さの合計を最大化」するグラフの問題と解釈でき、今回グラフはDAGなので各頂点までの最長パスはDPで簡単に求まるので解けた。 (解説にはラグランジュ双対を取ると書いてあるけど和が1の制約とかが邪魔でどうやったら取れるのかわからない...誰か教えてください)
LP解法2
LPで定式化した際に現れるという式は各頂点への相対流入量を表しているので、をただの辺に割り振られた確率とみるのではなくフローとみなすことが有効かもしれないことがわかる。すなわち、「各頂点の相対流入量が以上ですべての辺の合計流量が1となるようなフローが存在するの最大値をもとめる」問題だと解釈できる。ここでそのようなフローが存在するかどうかはに関して単調であるので、二分探索をすることにすると次の問題を解きたくなる。
双対をとると
となり、実はは無関係で二分探索が必要ないことがわかる。答えはこの問題の最適解でのとなる。(合計流量を1以上にできる境目を見つけたいので) そして上の問題はいわゆる牛ゲー(差分制約による最短(長)経路問題のポテンシャルの最大(小)化)に近い形であり、各頂点からの最長路の長さの和が最適値であることがわかる。よって解けた。
提出
bool solve() { int N, M; cin >> N >> M; if (N == 0 && M == 0) return false; vector<vector<int>> g(N); for (int i = 0; i < M; i++) { int a, b; cin >> a >> b; a--; b--; g[a].push_back(b); } vector<int> vs; vector<bool> used(N, false); vector<int> ord(N); function<void(int)> dfs = [&](int v) { used[v] = true; for (int u : g[v]) { if (!used[u]) dfs(u); } vs.push_back(v); }; for (int i = 0; i < N; i++) { if (!used[i]) dfs(i); } reverse(all(vs)); for (int i = 0; i < vs.size(); i++) { ord[vs[i]] = i; } for (int i = 0; i < N; i++) { for (int j : g[i]) { if (ord[i] > ord[j]) { cout << 0 << endl; return true; } } } vector<int> dp(N, 0); for (int i = N - 1; i >= 0; i--) { int v = vs[i]; for (int u : g[v]) { chmax(dp[v], dp[u] + 1); } } cout << setprecision(30) << -1.0 / (long double)(accumulate(all(dp), 0)) << endl; return true; }
Codeforces Round #591 D. Stack Exterminable Arrays
問題
https://codeforces.com/contest/1240/problem/D
解法
解説を見た。
各indexについてそこから順にstackに入れていった時に初めて空になるindex nxt[i]が欲しくなる(これが分かればあとはdpで数えるだけ)。 しかし直接nxtを求めるには少し情報が足りないので補助的に次の配列を考える
nxtX[i][x] := a[i]...a[j]が空になって、かつa[j+1] = xであるような最小のj>i
これを必要なところだけHashMapで持って更新していけばOK。nxtXの更新はコピーが発生しないようにswapなりmoveを使うことに注意すればO(n)でnxt,nxtXを求められる。
a[0]...a[i]をスタックに入れた状態を文字列と見てハッシュにして同じ状態のペアを数える方法もあるみたい。こっちのほうが思考停止で書けるかも。
参考: Codeforces Round #591 Div2 F / Div1 D. Stack Exterminable Arrays • knshnbのブログ
void solve() { int n; cin >> n; vector<int> a(n); cin >> a; vector<int> nxt(n + 1, -1); vector<unordered_map<int, int>> nxtX(n + 1); vector<ll> cnt(n + 1, 0ll); ll ans = 0ll; for (int i = n - 1; i >= 0; i--) { if (i + 1 < n && a[i] == a[i + 1]) nxt[i] = i + 1; else if (nxtX[i + 1].find(a[i]) != nxtX[i + 1].end()) nxt[i] = nxtX[i + 1][a[i]] + 1; if (nxt[i] != -1) swap(nxtX[i], nxtX[nxt[i] + 1]); if (nxt[i] != -1 && nxt[i] + 1 < n) nxtX[i][a[nxt[i] + 1]] = nxt[i]; if (nxt[i] != -1) cnt[i] = 1 + cnt[nxt[i] + 1]; ans += cnt[i]; } cout << ans << endl; return; }