競プロ勉強会 第2回

今回から新たに電電時代の学友Rくんも参加してくれました! 何回も一緒に定期テスト解いてるので知っていたのですが, さすがの賢さでした… CODE FESTIVAL 2015 予選A D 壊れた電車 今までと同じようにチェック関数を用意してから二分探索を行う問題。p = max(minute - dist *2, (minute - dist)/2) + x[i] + 1;のところで詰まってました… int n, m; int x[100010]; bool isCheck(LL minute) { LL p = 0; REP(i, m) { int dist = x[i] - p; if (dist > minute) { return false; } else if (dist> 0) { p = max(minute - dist *2, (minute - dist)/2) + x[i] + 1; } else { p = max(p, x[i] + minute + 1); } } return p >= n; } int main(int argc, char *argv[]) { cin >> n >> m; REP(i, m) { cin >> x[i]; x[i]--; } LL left = 0; LL right = n * 2; LL mid; while (right - left > 1) { mid = (right + left) / 2; if (isCheck(mid)) { right = mid; } else { left = mid; } } if (isCheck(left)) { cout << left << endl; } else { cout << right << endl; } return 0; } ```cpp ### [ARC 060 C 高橋君とカード](https://atcoder.jp/contests/arc060/tasks/arc060_a) 全探索すればいけた問題。いろいろ小難しいことを考えがちだが, まず全探索や貪欲法を考えてそっからチューニングしていくべきだと思った。けど、これ一瞬で書いてるFさんには一生届かない気もしてます。 ```cpp int n, a; int x[51]; int main(int argc, const char *argv[]) { cin >> n >> a; REP(i, n) { cin >> x[i]; x[i] -= a; } map<LL, LL> m; m[0] = 1; REP(i, n) { map<LL, LL> mTmp = m; REPI(it, mTmp) { m[it->first + x[i]] += it->second; } } cout << m[0] - 1 << endl; return 0; } ```cpp

May 29, 2020 · 2 min

競プロ勉強会 第1回

先週やった第1回のことをメモっときます。 JOI 2007 本選 C ダーツ ダーツやのに4回投げる問題。ここで2回なげる結果を全て記録して, その要素の組み合わせを二分探索で考えて3回っぽくする感動的な解法。 upper_boundとlower_boundの違いを履き違えていたが, upper_boundは探索したいkeyより大きいイテレータを返し, lower_boundは探索したいkey以上のイテレータを返す。 int n, m; int p[100000002]; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> p[i]; } vector<int> v; for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { v.push_back(p[i] + p[j]); } } int ans = 0; sort(v.begin(), v.end()); int vMax = *(v.end() - 1); for (int i = 0; i < v.size(); i++) { int nokori = min(m - v[i], vMax); if(nokori < 0) break; auto it = upper_bound(v.begin(), v.end(), nokori); int sum = *(it - 1) + v[i]; ans = max(ans, sum); } cout << ans << endl; return 0; } ```cpp ### [ABC 023 D 射撃王](https://atcoder.jp/contests/abc023/tasks/abc023_d) いける高度を二分探索する。mid, left, rightを使って二分探索を書く方法でバグらせまくったので, `while(right - left) { mid = (right + left) / 2; ...`をテンプレとして使っていきたい。 ```cpp LL n; LL h[100001], s[100001]; bool isok(LL x) { VI count(n); REP(i, n) { if (h[i] > x) { return false; } else { count[min(n - 1, (x - h[i]) / s[i])]++; } } REP(j, n - 1) { count[j + 1] += count[j]; } REP(j, n) { if (count[j] > j + 1) return false; } return true; } int main() { cin >> n; for (int i = 0; i < n; i++) { cin >> h[i] >> s[i]; } LL inf = 1e18; LL right = inf; LL left = 0; LL middle; while (right - left > 1) { middle = (right + left) / 2; if (isok(middle)) { right = middle; } else { left = middle; } } if (isok(right)) { cout << right << endl; } else { cout << left << endl; } return 0; } ```cpp

May 27, 2020 · 2 min

蟻本 | 2-2 猪突猛進! "貪欲法"

硬貨の問題 const int v[6] = {1, 5, 10, 50, 100, 500}; int c[6]; int A; void solve() { int ans = 0; for (int i = 5; i >= 0; --i) { int t = min(A / v[i], c[i]); A -= t * v[i]; ans += t; } printf("%d\n", ans); } ```cpp ### 区間スケジューリング問題 ```cpp const int MAX_N = 100000; int N, s[MAX_N], t[MAX_N]; pair<int, int> itv[MAX_N]; void solve() { for (int i = 0; i < N; ++i) { itv[i].first = t[i]; itv[i].second = s[i]; } sort(itv, itv + N); int ans = 0; int t = 0; for (int i = 0; i < N; ++i) { if (t < itv[i].second) { t = itv[i].first; ans++; } } printf("%d\n", ans); } ```cpp ### Best Cow Line ```cpp int N; char s[100000]; void solve() { cin >> N; for (int i = 0; i < N; ++i) { cin >> s[i]; } int a = 0, b = N - 1; while (a <= b) { //左からと右からを比較 bool left = false; for (int i = 0; a + i <= b; ++i) { if (s[a + i] < s[b - i]) { left = true; break; } else if (s[a + i] > s[b - i]) { left = false; break; } } if (left) { putchar(s[a++]); } else { putchar(s[b--]); } } putchar('\n'); } ```cpp ### Saruman's Army ```cpp int n, r; int x[1010]; void solve() { cin >> n >> r; for (int i = 0; i < n; ++i) { cin >> x[i]; } sort(x, x + n); int i = 0, ans = 0; while (i < n) { int s = x[i++]; while (i < n && x[i] <= s + r) { i++; } int p = x[i - 1]; while (i < n && x[i] <= p + r) { i++; } ans++; } printf("%d\n", ans); } ```cpp ### Fence Repair ```cpp int n, l[50010]; void solve() { cin >> n; for (int i = 0; i < n; ++i) { cin >> l[i]; } LL ans = 0; while (n > 1) { int mii1 = 0, mii2 = 1; if (l[mii1] > l[mii2]) { swap(mii1, mii2); } for (int i = 2; i < n; ++i) { if (l[i] < l[mii1]) { mii2 = mii1; mii1 = i; } else if (l[i] < l[mii2]) { mii2 = i; } } int t = l[mii1] + l[mii2]; ans += t; if (mii1 == n - 1) { swap(mii1, mii2); } l[mii1] = t; l[mii2] = l[n - 1]; n--; } printf("%lld\n", ans); } ```cpp

May 22, 2020 · 3 min

蟻本 | 2-1 すべての基本 "全探索"

最近蟻本の初級編をやり直したのだが2-1と2-2の記事がなかったので一応貼り付けとく 部分和問題 bool dfs(int i, int sum) { if (i == n) { return sum == k; } if (dfs(i + 1, sum)) { return true; } if (dfs(i + 1, sum + a[i])) { return true; } return false; } void solve() { if (dfs(0, 0)) { cout << "Yes" << endl; } else { cout << "No" << endl; } } ```cpp ### Lake Counting ```cpp int n, m; char field[110][110]; void dfs(int x, int y) { //置き換える field[x][y] = '.'; for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { int nx = x + dx; int ny = y + dy; if (0 <= nx && nx < n && 0 <= ny && ny < m && field[nx][ny] == 'W') { dfs(nx, ny); } } } } void solve() { int res = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { if (field[i][j] == 'W') { dfs(i, j); res++; } } } printf("%d\n", res); } ```cpp ### 迷路の最短路 ```cpp char maze[MAX_N][MAX_N + 1]; int n, m; int sx, sy; int gx, gy; int d[MAX_N][MAX_N]; //移動4方向ベクトル int dx[4] = {1, 0, -1, 0}; int dy[4] = {0, 1, 0, -1}; // (sx, sy)から(gx, gy)への最短距離を求める // たどり着けないとINF int bfs() { queue<P> que; //全ての点をINFで初期化 for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { d[i][j] = INF; } } //スタート地点をqueに入れて距離を0にする que.push(P(sx, sy)); d[sx][sy] = 0; //キューが空になるまでループ while (que.size()) { //キューの先頭を取り出す P p = que.front(); que.pop(); //ゴールなら終了 if (p.first == gx && p.second == gy) { break; } //移動4方向ループ for (int i = 0; i < 4; ++i) { //移動した後の点を(nx, ny)とする int nx = p.first + dx[i]; int ny = p.second + dy[i]; //移動が可能か //すでに訪れていないか if (0 <= nx && nx < n && 0 <= ny && ny < m && maze[nx][ny] != '#' && d[nx][ny] == INF) { //移動できるならばキューに入れて、距離を代入 que.push(P(nx, ny)); d[nx][ny] = d[p.first][p.second] + 1; } } } return d[gx][gy]; } void solve() { int res = bfs(); printf("%d\n", res); } ```cpp

May 22, 2020 · 2 min

競プロ勉強会 第0回

最近atcoderをやっていることをツイートしたら, めちゃめちゃ頭のいいFさんにお声がけいただいて,つよつよエンジニア学生Aくんも誘って, 競プロの勉強会をすることにしました。 僕はそんなに頭がよくなく理解が遅いかつ実装が遅いのでなかなか恐れ多い勉強会ですが, 頑張って食らいついていこうと思っております。 僕はC++を使っていて, FさんとAくんはPythonを使ってます。 Fさんは来週からC++使うそうです。あっという間に抜かされそうです。 今週はatcoder150を解きました。 D - Semi Common Multiple 長さ N の偶数からなる正の整数列 A = a_1, a_2, …, a_N と、整数 M が与えられます。 任意の k ( 1 ≤ k ≤ N) に対して以下の条件を満たす正の整数 X を A の「半公倍数」と定義します。 X = a_k * (p + 0.5) を満たす負でない整数 p が存在する。 1 以上 M 以下の整数のうちの A の半公倍数の個数を求めてください。 半公倍数の理解に時間がかかった。Aの要素が2で割れる回数が全て等しくなければならないというところがミソ。 E - Change a Little Bit 0, 1からなる長さ N の異なる 2 つの数列 S , T に対し、関数 f (S , T) を以下のように定めます。 S に対し以下の操作を繰り返して T と等しくすることを考える。このとき行う操作のコストの和として考えられる最小の値が f(S , T) である。 S_i を ( 0 から 1 、もしくは 1 から 0 に) 変更する。この操作のコストは、変更の直前に S_j ≠ T_j ( 1 ≤j ≤N) であったような整数 j の個数を D として、 D * C_i である。 0 , 1 からなる長さ N の異なる 2 つの数列の組 (S , T) は 2^N × ( 2^N − 1) 通り考えられます。これらすべてに対する f( S , T) の和を 10^9 + 7 で割った余りを計算してください。 ...

May 16, 2020 · 1 min

蟻本 | 2-7 GCJの問題に挑戦してみよう(1)

今まで学んだことを生かして4問解いてみた. 答えと全然違う実装をしたくないので, 問題を読んで実装の流れを考えたら答えをみるという流れでやってみた. Minimum Scalar Product (2008 Round1A A) 2つのベクトル成分を自由に入れ替えて内積を最小にする問題. 最初は数の正負を考えて, 正の数で絶対値が大きいものには負の数で絶対値が大きいものと積を取るようにみたいなことを考えていたが, シンプルに2つの成分を順と逆順でソートすればよかった. const int MAX_N = 800; typedef long long ll; int n; int v1[MAX_N], v2[MAX_N]; void solve() { sort(v1, v1 + n); sort(v2, v2 + n); ll ans = 0; for (int i = 0; i < n; i++) { ans += (ll)v1[i] * (ll)v2[n - i - 1]; } printf("%lld\n", ans); } ```cpp ### Crazy Rows (2009 Round2 A) 行をバブルソートする問題. 行列が与えられて下三角行列がどうとか難しい話をしているが案外簡単で, 上から順に行を埋めていくことを考える. 動かす行は上の行にすることで, 動かす操作数を最小化する. 実装のポイントとしては`a[i]: i行目の最後に現れる1の位置-1 ~ N-1`を定義することで行列より扱いやすくしている. また, 配列の要素を入れ替える`swap`をうまく使って再帰的な処理を実現している. ```cpp void solve() { int res = 0; // 配列にする for (int i = 0; i < N; i++) { a[i] = -1; for (int j = 0; j < N; j++) { if (M[i][j] == 1) { a[i] = j; } } } for (int i = 0; i < N; i++) { int pos = -1; // i + 1行目に移動する行番号 for (int j = i; j < N; j++) { if (a[j] <= i) { pos = j; break; } } // 実際にスワップ for (int j = pos; j > i; j--) { swap(a[j], a[j - 1]); res++; } } printf("%d\n", res); } ```cpp ### Bribe the Prisoners (2009 Round 1C C) 囚人の問題. 普通に問題として面白かった. アルゴリズムのポイントとしては, 空き部屋の概念と端の概念を一緒にしていかに再帰を実現するか. 実装のポイントとしては, 動的計画法を用いるために`dp[MAX_Q + 1][MAX_Q + 2]: (i, j)を解放するのに必要な金貨`を定義して使うところ. dpがどういう動きをするのかが最初わかりにくいので具体的な例でdpテーブルを埋める作業をしてみる. 幅が小さい順に調べるモチベーションがわかる. ```cpp const int MAX_Q = 100; int P, Q, A[MAX_Q + 2]; // (i, j)を解放するのに必要な金貨 int dp[MAX_Q + 1][MAX_Q + 2]; void solve() { // 簡単のため端をAに追加する A[0] = 0; A[Q + 1] = P + 1; // 初期化 for (int q = 0; q <= Q; q++) { dp[q][q + 1] = 0; } // 幅が小さい順にdpを埋めていく for (int w = 2; w <= Q + 1; w++) { for (int i = 0; i + w <= Q + 1; i++) { int j = i + w; int t = INT_MAX; // 最初に解放する囚人を全て試し最小コストのものを探す for (int k = i + 1; k < j; k++) { t = min(t, dp[i][k] + dp[k][j]); } // 最初の解放では解放する囚人にかかわらずA[j] - A[i] - 2の金貨が必要 dp[i][j] = t + A[j] - A[i] - 2; } } printf("%d\n", dp[0][Q + 1]); } ```cpp ### Millionaire (2008 APAC local onsites C) お金の掛け方が整数に限られていないので, 全探索ができない. ここで, ラウンドが1回しかなかった場合, 金額によって0%, P%, 100%の3つの確率に分けられることに気づく. さらに2ラウンドの場合は, 5つの確率に分けられる. この考えかたを動的計画法で実装する. 実装のポイントとしては, iラウンド目の確率はi-1ラウンド目の確率をもとに計算するので, 2つの配列`prv`と`nxt`を用意してうまく使うところにある. i-1ラウンド目の状態から正解・不正解する場合を確率の独立性を生かして考える. ```cpp typedef long long ll; const int MAX_M = 15; int M, X; double P; double dp[2][(1 << MAX_M) + 1]; void solve() { // n = 2^M int n = 1 << M; double \*prv = dp[0], \*nxt = dp[1]; memset(prv, 0, sizeof(double) * (n + 1)); // 既に100万円持っていれば確率1 prv[n] = 1.0; for (int r = 0; r < M; r++) { for (int i = 0; i <= n; i++) { int jub = min(i, n - i); double t = 0.0; for (int j = 0; j <= jub; j++) { t = max(t, P * prv[i + j] + (1 - P) * prv[i - j]); } nxt[i] = t; } swap(prv, nxt); } // 全勝した場合の金額 / 100万円 int i = (ll)X * n / 1000000; printf("%.6f\n", prv[i]); } ```cpp ### 感想 アルゴリズムに落とし込むところは発想力, そのあとの実装は再帰などをどう実現するかの数学力が必要なのではないかと思った.

May 21, 2019 · 3 min

蟻本 | 2-6 数学的な問題を解くコツ

ユークリッドの互除法やエラストテネスの篩, 繰り返し二乗法の計算を勉強した. 線分上の格子点の個数 最大公約数-1がこの問題の解となる. ユークリッドの互除法を元にgcdを実装. #include <iostream> #include <vector> using namespace std; int x_1, x_2, y_1, y_2; int gcd(int a, int b) { if (b == 0) { return a; } else { return gcd(b, a % b); } } int main(int argc, char const *argv[]) { cin >> x_1 >> y_1 >> x_2 >> y_2; int x_diff = abs(x_1 - x_2); int y_diff = abs(y_1 - y_2); cout << gcd(x_diff, y_diff) - 1 << endl; return 0; } ```cpp ### 双六 ユークリッドの互除法を拡張する. `ax+by=1`を満たす整数`x, y`を探すには以下のようなスクリプトを書く. ```cpp int extgcd(int a, int b, int& x, int& y) { int d = a; if (b != 0) { d = extgcd(b, a % b, y, x); y -= (a / b) * x; } else { x = 1; y = 0; } return d; } ```cpp ### 素数判定 素数かどうかは`2 ~ √n`までを調べればよい. ```cpp // 素数判定 bool is_prime(int n) { for (int i = 2; i * i <= n; i++) { if (n % i == 0) return false; } return true; } // 約数の列挙 vector<int> divisor(int n) { vector<int> res; for (int i = 2; i * i <= n; i++) { if (n % i == 0) { res.push_back(i); if (i != n / i) { res.push_back(n / i); } } } return res; } // 素因数分解 map<int, int> prime_factor(int n) { map<int, int> res; for (int i = 2; i * i <= n; i++) { while (n % i == 0) { ++res[i]; n /= i; } } if (n != 1) { res[n] = 1; } return res; } ```cpp ### 素数の個数 エラストテネスの篩と呼ばれるアルゴリズムを使って数える. 最小の素数の倍数をどんどん省いていく. ```cpp int sieve(int n) { int p = 0; for (int i = 0; i <= n; i++) { is_prime[i] = true; } is_prime[0] = is_prime[1] = false; for (int i = 2; i <= n; i++) { if (is_prime[i]) { prime[p++] = i; for (int j = 2 * i; j <= n; j += i) { is_prime[j] = false; } } } return p; } ```cpp ### 区間内の素数の個数 篩を2つ用いることで, 特定の区間の素数の個数を調べることができる. 区間`[a, b)`を調べる場合は, `[2, √b)`と`[a, b)`の篩を別々に作っておく. ```cpp const int MAX_N = 100000000; const int MAX_SORT_B = 100000000; typedef long long ll; bool is_prime[MAX_N]; bool is_prime_small[MAX_SORT_B]; int a, b; void segment_sieve(ll a, ll b) { for (int i = 0; (ll)i * i < b; i++) { is_prime_small[i] = true; } for (int i = 0; i < b - a; i++) { is_prime[i] = true; } for (int i = 2; (ll)i * i < b; i++) { if (is_prime_small[i]) { for (int j = 2 * i; (ll)j * j < b; j += i) { is_prime_small[j] = false; } for (ll j = max(2LL, (a + i - 1) / i) * i; j < b; j += i) { is_prime[j - a] = false; } } } } ```cpp ### Carmichael_Numbers 繰り返し二乗法を用いてべき乗を計算する. 基本的な考え方としては`x^22 = x^16 * x^4 * x^2`のようにx^2^i を順次求めながら計算する. ```cpp ll mod_pow(ll x, ll n, ll mod) { ll res = 1; while (n > 0) { if (n & 1) { res = res * x % mod; } x = x * x % mod; n >>= 1; } return res; } ```cpp ## 感想 最後のビット計算をうまく使う実装は自分でできる気がしない…

May 12, 2019 · 3 min

蟻本 | 2-5 あれもこれも実は "グラフ"

グラフを使った解法を勉強した. グラフの表現方法として隣接行列と隣接リストの2つがあって, 最短経路問題を特にはベルマンフォード法, ダイクストラ法, ワーシャル-フロイド法があり, 最小全域木問題を特にはプリム法とクラスカル法がある. この一部を使った演習問題を紹介する. 二部グラフ判定 深さ優先探索を用いて, 隣接している頂点をどんどん塗っていく. グラフは隣接リストを使って表現されている. #include <iostream> #include <vector> using namespace std; const int MAX_V = 10000; vector<int> G[MAX_V]; int V, E; int color[MAX_V]; //頂点を1と-1で塗っていく bool dfs(int v, int c) { color[v] = c; for (int i = 0; i < G[v].size(); i++) { // 隣接している頂点が同じ色ならfalse if (color[G[v][i]] == c) { return false; } // 隣接している頂点がまだ塗られていないなら-cで塗る if (color[G[v][i]] == 0 && !dfs(G[v][i], -c)) { return false; } } return true; } int main(int argc, char const *argv[]) { cin >> V >> E; for (int i = 0; i < E; i++) { int s, t; cin >> s >> t; G[s].push_back(t); G[t].push_back(s); } for (int i = 0; i < V; i++) { if (color[i] == 0) { if (!dfs(i, 1)) { printf("No\n"); return 1; } } } printf("Yes\n"); return 0; } ```cpp ### Roadblocks 道を辺として, 交差点を頂点として2番目に短い経路を求める問題. 基本的にはダイクストラ法を用いるが, 2番目に短い経路も記憶しておくところがみそ. ```cpp #include <iostream> #include <queue> #include <vector> using namespace std; const int MAX_N = 1000; const int INF = 99999999; struct edge { int to, cost; }; typedef pair<int, int> P; // firstは最短距離, secondは頂点の番号 int N, R, E; vector<edge> G[MAX_N]; // グラフの隣接リスト表現 int dist[MAX_N]; int dist2[MAX_N]; void read() { cin >> N >> R >> E; for (int i = 0; i < E; i++) { int s, t, c; cin >> s >> t >> c; edge x, y; x.to = s; y.to = t; x.cost = c; y.cost = c; G[s].push_back(y); G[t].push_back(x); } } void solve() { priority_queue<P, vector<P>, greater<P> > que; fill(dist, dist + N, INF); fill(dist2, dist2 + N, INF); // 発ノード dist[0] = 0; que.push(P(0, 0)); while (!que.empty()) { P p = que.top(); que.pop(); int v = p.second; int d = p.first; if (dist2[v] < d) { // 2番目の最短路より長い continue; } // vと隣接している頂点を更新 for (int i = 0; i < G[v].size(); i++) { edge e = G[v][i]; int d2 = d + e.cost; if (dist[e.to] > d2) { // 最短より短い場合 swap(dist[e.to], d2); que.push(P(dist[e.to], e.to)); } if (dist2[e.to] > d2 && dist[e.to] < d2) { // 2番目より短い場合 dist2[e.to] = d2; que.push(P(dist2[e.to], e.to)); } } } printf("%d\n", dist2[N - 1]); } int main(int argc, char const *argv[]) { read(); solve(); return 0; } ```cpp ### Conscription 人を頂点として親密度をコスト付きの辺として, 最小全域木をもとめる問題. この時, コストの高い辺を使いたいのでコストの正負を反転させる. また, この問題は男と女が分けられているが, 特殊な構造を使わなかった. 最小全域木を求めるときはクラスカル法を用いた. クラスカル法はUnionFind木の手法を応用している. ```cpp #include <iostream> #include <queue> #include <vector> using namespace std; const int MAX_N = 50000; const int MAX_E = 50000; const int MAX_R = 50000; // union find // -------------------------------------------- int par[MAX_N]; int depth[MAX_N]; void init_union_find(int n) { for (int i = 0; i < n; i++) { par[i] = i; depth[i] = 0; } } int find(int x) { if (par[x] == x) { return x; } else { return par[x] = find(par[x]); } } void unite(int x, int y) { x = find(x); y = find(y); if (x == y) return; if (depth[x] < depth[y]) { par[x] = y; } else { par[y] = x; if (depth[x] == depth[y]) depth[x]++; } } bool same(int x, int y) { return find(x) == find(y); } struct edge { int u, v, cost; }; bool comp(const edge& e1, const edge& e2) { return e1.cost < e2.cost; } edge es[MAX_E]; int V, E; int kruskal() { sort(es, es + E, comp); init_union_find(V); int res = 0; for (int i = 0; i < E; i++) { edge e = es[i]; if (!same(e.u, e.v)) { unite(e.u, e.v); res += e.cost; } } return res; } int N, M, R; int x[MAX_R], y[MAX_R], d[MAX_R]; void read() { cin >> N >> M >> R; for (int i = 0; i < R; i++) { cin >> x[i] >> y[i] >> d[i]; } } void solve() { V = N + M; E = R; for (int i = 0; i < E; i++) { es[i] = (edge){x[i], N + y[i], -d[i]}; } printf("%d\n", 10000 * (V) + kruskal()); } int main(int argc, char const* argv[]) { read(); solve(); return 0; } ```cpp ### Layout 牛を頂点と考え, 牛の番号, 仲の良さ, 仲の悪さをコストつきの辺として最短経路を求める問題. 発想としては, 全てを制約条件に落としこんだ結果, 両辺に変数が1つずつしか現れていないことに着目する. 今回は負の辺があるのでベルマンフォード法を用いる. 負閉路が存在するかどうかは, 牛の数だけupdateを行ったあとにも更新があるかどうかで判断する. ```sql #include <iostream> #include <queue> #include <vector> using namespace std; const int MAX_ML = 10000; const int MAX_MD = 10000; const int MAX_N = 1000; const int INF = 99999999; int N, ML, MD; int AL[MAX_ML], BL[MAX_ML], DL[MAX_ML]; int AD[MAX_MD], BD[MAX_MD], DD[MAX_MD]; int d[MAX_N]; bool updated; void read() { cin >> N >> ML >> MD; for (int i = 0; i < ML; i++) { cin >> AL[i] >> BL[i] >> DL[i]; } for (int i = 0; i < MD; i++) { cin >> AD[i] >> BD[i] >> DD[i]; } } void update(int& x, int y) { if (x > y) { x = y; updated = true; } } void bellmanford() { for (int k = 0; k <= N; k++) { updated = false; // i+1からiへコスト0 for (int i = 0; i + 1 < N; i++) { if (d[i + 1] < INF) { update(d[i], d[i + 1]); } } // ALからBLへコストDL for (int i = 0; i < ML; i++) { if (d[AL[i] - 1] < INF) { update(d[BL[i] - 1], d[AL[i] - 1] + DL[i]); } } // BDからADへコスト-DD for (int i = 0; i < MD; i++) { if (d[BD[i] - 1] < INF) { update(d[AD[i] - 1], d[BD[i] - 1] - DD[i]); } } } } void solve() { // 負閉路チェック fill(d, d + N, 0); bellmanford(); if (updated) { printf("%d\n", -1); return; } fill(d, d + N, INF); d[0] = 0; bellmanford(); int res = d[N - 1]; if (res == INF) { res = -2; } printf("%d\n", res); } int main(int argc, char const* argv[]) { read(); solve(); return 0; } ```cpp ## 感想 最後の牛の並びを最短経路問題に変換する方法はむずすぎる….

May 8, 2019 · 5 min

蟻本 | 2-4 データを工夫して記憶する "データ構造"

平成最後の日も蟻本の勉強をしてみた. 二分木, 二分探索木, Union-Find木を使った問題を3問解いた. priority_queueを用いる問題 1 expedition ガソリンを補給する回数を最小限に抑える問題. 探索としては, 今あるガソリンで次のガソリンスタンドに辿り着けなかったら, 過去に通過した供給量が一番大きいガソリンスタンドで補給したことにする. ここで, ガソリンスタンドをpriority_queueに入れて, 供給量が大きいもの順に並べていた. #include #include using namespace std; const int MAX_N = 10000; int N, L, P; int A[MAX_N + 1], B[MAX_N + 1]; void read() { cin >> N >> L >> P; for (int i = 0; i < N; i++) { cin >> A[i]; } for (int i = 0; i < N; i++) { cin >> B[i]; } } void solve() { A[N] = L; B[N] = 0; priority_queue que; int ans = 0, pos = 0, tank = P; for (int i = 0; i <= N; i++) { int d = A[i] - pos; // ガソリンを補給 while (tank - d < 0) { if (que.empty()) { puts("-1"); return; } tank += que.top(); que.pop(); ans++; } tank -= d; pos = A[i]; que.push(B[i]); } printf("%d\n", ans); } int main(int argc, char const *argv[]) { read(); solve(); return 0; } ```cpp ### 2 fence repair なるべく長い板を切らせないように目的の板の集合を手に入れる問題. ナイーブに実装すると`O(N^2)`時間になってしまうが, `priority_queue`を用いれば`O(NlogN)`時間になる. ```cpp const int MAX_N = 10000; int n, l[MAX_N]; void solve() { cin >> n; for (int i = 0; i < n; ++i) { cin >> l[i]; } LL ans = 0; priority_queue, greater > que; for (int i = 0; i < n; i++) { que.push(l[i]); } while (que.size() > 1) { int l1, l2; l1 = que.top(); que.pop(); l2 = que.top(); que.pop(); ans += l1 + l2; que.push(l1 + l2); } printf("%lld\n", ans); } int main() { solve(); return 0; } ```cpp #### `priority_queue`の小さい順 ```cpp priority_queue<int, vector<int>, greater<int> > que; ```cpp このように宣言する. ### 3 食物連鎖 複数の情報から矛盾している情報の数を求める. 食物連鎖の関係に矛盾がないことを調べるのにUnion-Find木を用いる. 情報を見ると, 同じ種類に属するか捕食被食の関係にあるという情報だけで種類を特定できない. そこで, 3通りのUnion-Find木を作って1つの木が同時に実現するということにする. 以下がUnion-Find木の実装である. ```cpp // union find // -------------------------------------------- int par[MAX_N]; int depth[MAX_N]; // 初期化 void init(int n) { for (int i = 0; i < n; i++) { par[i] = i; depth[i] = 0; } } // 木の根を返す int find(int x) { if (par[x] == x) { return x; } else { return par[x] = find(par[x]); } } // 木を併合する void unite(int x, int y) { x = find(x); y = find(y); if (x == y) return; if (depth[x] < depth[y]) { par[x] = y; } else { par[y] = x; if (depth[x] == depth[y]) depth[x]++; } } bool same(int x, int y) { return find(x) == find(y); } int N, K; int T[MAX_K], X[MAX_K], Y[MAX_K]; void solve() { cin >> N >> K; for (int i = 0; i < K; i++) { cin >> T[i] >> X[i] >> Y[i]; } init(N * 3); int ans = 0; for (int i = 0; i < K; i++) { int t = T[i]; int x = X[i] - 1, y = Y[i] - 1; // 0...N-1 // 正しくない番号 if (x < 0 || x >= N || y < 0 || y >= N) { ans++; continue; } if (t == 1) { // xとyは同じ種類という情報 if (same(x, y + N) || same(x, y + N * 2)) { ans++; } else { unite(x, y); unite(x + N, y + N); unite(x + N * 2, y + N * 2); } } else { if (same(x, y) || same(x, y + N * 2)) { ans++; } else { unite(x, y + N); unite(x + N, y + N * 2); unite(x + N * 2, y); } } } printf("number of wrong info is %d\n", ans); } int main() { solve(); return 0; } ```cpp ## 感想 Union-Find木を知らなかったので, 食物連鎖の実装が綺麗でわかりやすかった. これも標準のライブラリがあればいいのに…

April 30, 2019 · 3 min

蟻本 | 2-3 値を覚えて再利用 "動的計画法"

アルゴリズム活用力は, 主にコーディングの試験の際に見られるところであり, webアプリやモバイルアプリを作るのにはあまり必要ないかもしれない. しかし, 将来転職する際にAtcoderである程度の成績を納めていたら強いのではという理由と, 単に楽しいからという理由で蟻本の中級くらいやりたいなと思っている. ってか, 今日のAtcoder全然解けへんくて萎えた… 今回は, 2_3 値を覚えて再利用"動的計画法"を勉強した. 01ナップサック問題 dpを使った動的計画法の初歩が書かれていた. 全探索を行うとO(2^n)かかってしまうところが, メモ化によりO(nW)の計算量で済む. この問題のコツは, 品物と重さを引数として価値の総和を出した時に計算結果を残しておくという発想から. const int MAX_N = 100; int n, W; int w[MAX_N], v[MAX_N]; // dp[i+1][j]: i番目までの品物から重さの総和がj以下となるように選んだ時の, // 価値の総和の最大値 int dp[MAX_N + 1][MAX_N + 1]; int main() { cin >> n; for (int i = 0; i < n; i++) { cin >> w[i] >> v[i]; } cin >> W; for (int i = 0; i < n; i++) { for (int j = 0; j <= W; j++) { if (j < w[i]) { dp[i + 1][j] = dp[i][j]; } else { dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]); } } } printf("max sum value is %d\n", dp[n][W]); return 0; } ```cpp #### 参考: メモ化テーブルを初期化する方法 ```cpp memset(dp, -1, sizeof(dp)); ```cpp ってな感じで, 0と-1ならば初期化できる. 1はできない. ### 最長共通部分列問題 この問題のコツは, 2つも文字列を引数とした計算結果をメモ化するところである. 最初のうちはDPテーブルをかくと漸化式が浮かびやすいかなと思う. ```cpp const int MAX_N = 1000; int n, m; char s[MAX_N], t[MAX_N]; int dp[MAX_N][MAX_N]; int main() { cin >> n >> m >> s >> t; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (s[i] == t[j]) { dp[i + 1][j + 1] = dp[i][j] + 1; } else { dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]); } } } cout << "max value is " << dp[n][m] << endl; return 0; } ```cpp ### 個数制限なしのナップサック問題 個数制限がなくなったことで, 愚直に計算するとforループがもう一つ増えてしまう. そこで無駄に計算を行なっている箇所を探して, 漸化式を簡単にする. コツとしては, DPテーブルをみて漸化式の動きをみて, 簡単になる箇所がないかを探すのみだと思う. #### 参考: 偶奇を考えた配列の再利用 ```cpp for (int i = 0; i < n; i++) { for (int j = 0; j <= W; j++) { if (j < w[i]) { dp[(i + 1) & 1][j] = dp[i & 1][j]; } else { dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[(i + 1) & 1][j - w[i]] + v[i]); } } } ```cpp `(i + 1) & 1`の使い方がめちゃうまい気がする… こうして, 2行分のデータを確保できる. ### 01ナップサック問題その2 重さの制約条件が緩くなって, Wが10^9まで飛んでしまった時は, Wでforループを回したくなくなる. そんなときは, メモ化けする対象を入れ替えることでうまくいく. メモ化の値に大きくなるものを入れるのがコツな気がする. ```cpp const int MAX_N = 100; const int MAX_V = 100; const int INF = 99999999; int n, W; int w[MAX_N], v[MAX_N]; // dp[i+1][j]: i番目までの品物から価値の総和がjとなるよう選んだときの, // 重さの総和の最小値 そのような解が存在しない場合は十分大きな値INF int dp[MAX_N + 1][MAX_N * MAX_V + 1]; int main() { // 入力値の読み込み cin >> n; for (int i = 0; i < n; i++) { cin >> w[i] >> v[i]; } cin >> W; // 解く fill(dp[0], dp[0] + MAX_N * MAX_V + 1, INF); dp[0][0] = 0; for (int i = 0; i < n; i++) { for (int j = 0; j <= MAX_N * MAX_V; j++) { if (j < v[i]) { dp[i + 1][j] = dp[i][j]; } else { dp[i + 1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]); } } } int res = 0; for (int i = 0; i <= MAX_N * MAX_V; i++) { if (dp[n][i] <= W) { res = i; } } printf("max sum value is %d\n", res); return 0; } ```cpp ### 個数制限付き部分和 bool値をメモ化けするのは無駄がある. そこで大きな値である残りの`a_i`を入れることで漸化式が改善されて計算量を落とすことができる. ```cpp const int MAX_N = 100; const int MAX_K = 100000; int n; int K; int a[MAX_N]; int m[MAX_N]; // dp[i+1][j]: i番目まででjを作る際に余る最大のi番目の個数(作れない場合は-1) int dp[MAX_K]; int main() { cin >> n; for (int i = 0; i < n; i++) { cin >> a[i] >> m[i]; } cin >> K; memset(dp, -1, sizeof(dp)); dp[0] = 0; for (int i = 0; i < n; i++) { for (int j = 0; j <= K; j++) { if (dp[j] >= 0) { dp[j] = m[i]; } else if (j < a[i] || dp[j - a[i]] < 0) { dp[j] = -1; } else { dp[j] = dp[j - a[i]] - 1; } } } if (dp[K] > 0) { printf("answer is YES\n"); } else { printf("answer is NO\n"); } return 0; } ```cpp ### 最長増加部分列問題 案外奥が深い問題. 部分文字列の候補があがってくるのでそれをメモ化すると`O(n^2)`で解くことができるが, 候補よりも, 最終要素がなにであるかに着目すると`O(n \log n)`で解くことができる. ここらへんは地頭の良さ. #### 参考: lower_bound ```cpp fill(dp, dp + n, INF); for (int i = 0; i < n; i++) { * lower_bound(dp, dp + n, a[i]) = a[i]; } printf("%ld\n", lower_bound(dp, dp + n, INF) - dp); ```cpp ってな感じで, ソートされた列aから二分探索で`a_i <= k`となりような最小の`a_i`のポインタを求めることができる. ### 分割数 最適化だけでなく場合の数にも応用. ```cpp const int MAX_N = 1000; const int MAX_M = 1000; int n, m; int M; int dp[MAX_M + 1][MAX_N + 1]; int main(int argc, char const *argv[]) { cin >> n >> m; cin >> M; dp[0][0] = 1; for (int i = 1; i <= m; i++) { for (int j = 0; j <= n; j++) { if (j >= i) { dp[i][j] = dp[i - 1][j] + dp[i][j - i]; } else { dp[i][j] = dp[i - 1][j]; } } } printf("%d\n", dp[m][n] % M); return 0; } ```cpp ### 重複組み合わせ これはめちゃめちゃ難しかった. これも, - 何をメモ化するか - 漸化式が作れるように - 計算量が多くならないように - 1つ前の値を意識する - DPテーブルから漸化式を導き出す みたいなところが大事である. ```cpp const int MAX_N = 1000; int n, m; int a[MAX_N + 1]; int M; int dp[MAX_N + 1][MAX_N + 1]; int main(int argc, char const *argv[]) { cin >> n >> m; for (int i = 0; i < n; i++) { cin >> a[i]; } cin >> M; // 1つも選ばない方法は常に1通り for (int i = 0; i <= n; i++) { dp[i][0] = 1; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= m; j++) { if (j - 1 - a[i] >= 0) { dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j] - dp[i][j - 1 - a[i]] + M) % M; } else { dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j]) % M; } } } printf("%d\n", dp[n][m]); return 0; } ```cpp ## 感想 蟻本なかなか重たい. 結構な練習を積まないと身につかない. やっぱり初級編だけ終わらせようかなと思ってきた…

April 27, 2019 · 5 min