yukicoder:No.701 ひとりしりとり
スペシャルジャッジ問題は面白いなぁ
問題概要
ちょうど 個の単語でしりとりをせよ。ただし次のルールを守る必要がある。
- 小文字アルファベットのみからなる ~ 文字の単語のみ使用できる。
- 単語の末尾と次の文字を同じにする必要がある。
- 最後以外の単語の末尾は
n
以外、最後の単語の末尾はn
である必要がある。 - はじめの単語には上記ルールに従う任意の単語を用いてよい。
- 同じ単語は用いてはならない。
制約
解法
結論から述べると、 ~ 個目の単語として
a
+ (かぶらない 文字の単語) + a
を用いて 個目に an
を用いて終わらせばよい。
文字の単語には aaaaa
~ zzzzz
の 通りのバリエーションがあるので 通りは十分表現可能。
24 1 3 4 5
25 1 3 4 5
0 2 3 4 5
という風に配列で繰り上がりを表現して 通り作ればよい。
コード
#include <bits/stdc++.h> using namespace std; using ll = long long; #define mod 1000000007ll #define loop(i, n) for (int i = 0; i < n; i++) #define all(v) v.begin(), v.end() #define putout(a) cout << a << '\n' #define Sum(v) accumulate(all(v), 0ll) int main() { ll N; cin >> N; vector<ll> num(5); loop(i, N - 1) { string now; num[0]++; ll j = 0; while (num[j] == 26) { num[j] = 0; num[j + 1]++; j++; } loop(j, 5) now += (char)('a' + num[j]); cout << "a" << now << "a" << endl; } putout("an"); return 0; }
yukicoder:No.1103 Directed Length Sum
めちゃくちゃ教育的で面白い問題だった!
問題概要
頂点の有向木がある。
この有向木は根以外の各頂点の親からその頂点へ 本の有向辺が伸びた構造をしている。
を「頂点 から頂点 に移動するとき経由する辺の数」とするとき、
を求めよ。
但し , ( から へのパスが存在しないとき)とする。
制約
解法
が大きいのでまず愚直 は通らない。
この有向木は根以外の各頂点の親からその頂点へ 本の有向辺が伸びた構造をしている。(意訳)という文言が重要そうである。
なぜなら から へのパスが存在しないとき なので、根からとある葉へのパスに異なる頂点 と が両方とも存在していて が根に近いときにのみ が でなくなるという言い換えができるからである。
この言い換えをすることによって何が嬉しいのかというと、前回解説した問題 yukicoder:No.1096 Range Sums - kacho65535の競プロメモ でも用いた考察典型である各要素が何回足されるかを考える問題に帰着できるからである。 しかも、足される回数の求め方も前回の問題と本質的に同じである。
以上の考察から次のことが分かる。
ある有向辺 が足される回数は、根から までのパスに含まれる頂点の個数(根や も含む)を 、この有向辺を取り除いたときにできる を含む部分木の頂点数を として、 回である。
よってすべての有向辺に対する の総和が答えになる。(辺のコストが なので)
これを求めるには、各 を根からのBFSで求めた後、木dpで部分木のサイズを記録していけばよい。
コード
#include <bits/stdc++.h> using namespace std; using ll = long long; #define mod 1000000007ll #define loop(i, n) for (int i = 0; i < n; i++) #define all(v) v.begin(), v.end() #define putout(a) cout << a << '\n' #define Sum(v) accumulate(all(v), 0ll) void bfs(vector<vector<ll>> &G, vector<ll> &dist, ll s) { queue<ll> que; dist[s] = 0; que.push(s); while (!que.empty()) { ll v = que.front(); que.pop(); for (auto next_v : G[v]) { if (dist[next_v] != -1) continue; dist[next_v] = dist[v] + 1; que.push(next_v); } } } vector<ll> sz; void dfs(vector<vector<ll>> &G, ll v, ll p) { for (auto nv : G[v]) { if (nv == p) continue; dfs(G, nv, v); } sz[v] = 1; for (auto nv : G[v]) { if (nv == p) continue; sz[v] += sz[nv]; } } int main() { ll N; cin >> N; vector<vector<ll>> G(N); ll root = 0; //bとして1度も出現しないのが根 sz.resize(N); vector<ll> A(N - 1), B(N - 1), x(N - 1), y(N - 1), used(N, false); loop(i, N - 1) { cin >> A[i] >> B[i]; A[i]--; B[i]--; used[B[i]] = true; G[A[i]].emplace_back(B[i]); } loop(i, N) if (!used[i]) root = i; vector<ll> dist(N, -1); bfs(G, dist, root); dfs(G, root, -1); loop(i, N - 1) x[i] = dist[B[i]]; //dist[A[i]]+1でもよい loop(i, N - 1) y[i] = sz[B[i]]; ll ans = 0; loop(i, N - 1) { ans += x[i] * y[i] % mod; ans %= mod; } putout(ans); return 0; }
yukicoder:No.1096 Range Sums
一部空のコードブロックがありますが、texの数式を正しく表示させるための応急処置なので許してください...
問題概要
個の要素 からなる数列がある。
を求めよ。
制約
- 答えは 以下に収まる
解法
まず、初期状態では範囲が広い が つ付いていて、愚直にやると計算に かかる。
累積和を用いれば各 が で計算できるようになるが、左側に が つ残っているので普通にやると間に合わない。
こういった問題でよく用いられる典型考察として、各要素が何回足されるかを考えるというものがある。
この考察が役立つAtcoder上の問題(ネタバレ防止のため隠してあります)
AGC005 B - Minimum Sum Aiが答えに加算される回数を考えるところが本質です。 [https://atcoder.jp/contests/agc005/tasks/agc005_b] ABC140 E - Second Sum Minimum Sumの完全上位互換です。まだ解けてません(←?) [https://atcoder.jp/contests/abc140/tasks/abc140_e] 第6回 ドワンゴからの挑戦状 予選 B - Fusing Slimes スライムがxi,x(i+1)間を移動する確率をそれぞれ求めるのが本質という点で類似しています。 [https://atcoder.jp/contests/dwacon6th-prelims/tasks/dwacon6th_prelims_b]
は、 が 以上 以下かつ が 以上 以下のときにのみ足されるので、合計で 回足されることになる。
実装上では -indexedで計算する必要があることに注意しながら足し合わせていけばよい。
計算量は 。
コード
#include <bits/stdc++.h> using namespace std; using ll = long long; #define mod 1000000007ll #define loop(i, n) for (int i = 0; i < n; i++) #define all(v) v.begin(), v.end() #define putout(a) cout << a << '\n' #define Sum(v) accumulate(all(v), 0ll) int main() { ll N, a, ans = 0; cin >> N; loop(i, N) { cin >> a; ans += a * (1 + i) * (N - i); } putout(ans); return 0; }
課題残ってるけど毎日 問位なんか解説書いたほうがいいかなと思って、投稿...
い、息抜き代わりということで...
yukicoder:No.944 煎っぞ!
計算量解析を大分さぼったけど投げたらACできた...(あまりよろしくない)
問題概要
個の要素からなる数列を、順序を維持したまま 等分( 個の連続部分列の要素の和が等しくなるように分ける)するときの の最大値を求めよ。
制約
解法
まず、 個の要素の和 の最大値が と小さめであることに着目する。
等分できるとき、 は の約数であることを考慮すると、約数の個数は 個を超えないため(実際には大分少ない)、 の候補として の約数を全て調べても 回のチェックが でできるので制限時間に間に合う。
枝狩りをしたところ でACできた。
コード
#include <bits/stdc++.h> using namespace std; using ll = long long; #define mod 1000000007ll #define loop(i, n) for (int i = 0; i < n; i++) #define all(v) v.begin(), v.end() #define putout(a) cout << a << '\n' #define Sum(v) accumulate(all(v), 0ll) template <typename T> bool chmax(T &a, const T &b) { if (a < b) { a = b; // aをbで更新 return true; } return false; } int main() { ll N; cin >> N; vector<ll> A(N); ll maxx = 0; loop(i, N) { cin >> A[i]; chmax(maxx, A[i]); } ll S = Sum(A); for (int i = N; i >= 1; i--) { if (S % i != 0 || (S / i) < maxx) continue; ll now = 0; bool ng = false; ll div = S / i; //各豆のおいしさ loop(j, N) { now += A[j]; if (now > div) { ng = true; break; } if (now == div) now = 0; } if (ng || now != 0) continue; putout(i); return 0; } return 0; }
あ
あいさつ
お久しぶりです。kacho65535です。
競技プログラミングにはまっていたときにその場の勢いではてなブログを始めたものの全く更新していなかったので、その反省も兼ねて来年はちょこちょこ更新していきたいと思います。
なぜ更新をさぼっていたのかというと、単純に怠けていたのもありますが、大学が始まって忙しくなったりAtcoder Problemsで今埋めている最中の青diff中盤が自分の実力にあわず解けずに放置していたり、初見で分からなかった問題の解説を書く気が失せてしまったからです。
なんやかんやAtcoderを始めてから1年が過ぎようやく水色コーダーになれましたが、あまり競プロのモチベーションは上がっていないので改善していくためにatcoderではなく、あえてyukicoderの問題を簡単なところから埋めていきつつ解説記事を書いていけたらいいなと思っています。
AtCoder Beginner Contest 164 E - Two Currencies
解説が自分には難しかったので備忘録として書きます
問題
https://atcoder.jp/contests/abc164/tasks/abc164_e
解法
頂点間の距離に加えて、金貨銀貨の設定が加わっているため一気に複雑になります。幸い頂点数と銀貨の運賃の制約が、, と抑え目になっているため、ここを生かすことを考えます。
制約からグラフは連結であることがわかるので、任意の2頂点間を移動するとき、最悪の条件でも運賃が銀貨枚である辺を本移動移動すればたどり着けることがわかります。つまり、任意の頂点にいる時のコインの枚数は枚の範囲しか持つ必要がないことがわかります。(もっと言うと運賃の最大値をとして枚の範囲)しかし、先ほどの制約からここは思い切って枚として考えてしまっても問題ないことがわかります。さて、一つの頂点上でコインを持つ状態を複数重ねるという実装をするときは、イメージ的には頂点の真上にエッジを張るようにして遷移させればよいです。要するに、頂点は頂点で銀貨を枚持つ状態を表す、として個の頂点を持つグラフを構築したのちに普通のダイクストラ法で単一始点最短路を求めればよいです。
ここからは、具体的なグラフの構築方法のまとめです。
頂点上で金貨枚を分かけて銀貨に交換するという操作を表すために、まずは頂点について、銀貨を枚持っている状態から銀貨を持っている状態へ、コストの有向辺を張るという操作をまで繰り返します。ここで注意したいのが、先ほど決めた添え字のルール上、どの頂点についても、銀貨を枚以上持つという状態を考えることができないので、がを超える場合は、辺を問答無用で銀貨枚持っている状態に繋ぎます。(ここが公式の解説で"持ちきれない銀貨は捨てる"と表現されている部分)
次に頂点,を結ぶ作業に入ります。まず、頂点の値が1-indexedだと扱いづらいので、どちらもデクリメントしておきます。次に、頂点から頂点に有向辺を結ぶ操作を考えます。移動には必ず正の枚数の銀貨を運賃として払う必要があるので、頂点で銀貨を2500枚持っている状態をはじめとして段々と枚数を下げていけばよいです。運賃は銀貨枚なので、頂点で銀貨を枚持っているとして、実際の実装では頂点から頂点にコスト(移動にかかる時間)の有向辺を張ります。頂点で銀貨の枚数が未満ならば辺は張れないことに注意しましょう。これと全く同様の操作を頂点から頂点に対しても行えば、いよいよ目的のグラフの完成です!あとは始点を決めてダイクストラ法を実行すればよいのですが、銀貨の初期値も2501以上は切り捨てるよう注意しましょう。あとはそれぞれの頂点への最短経路を求めて出力すれば終了です!お疲れ様でしたー!
コード
#include <bits/stdc++.h> #define _GLIBCXX_DEBUG using namespace std; struct edge { long long to; long long cost; }; typedef pair<long long, long long> P; //P.first:最短距離,P.second:頂点の番号 //頂点V個の負閉路なし重み付きグラフGにおける始点sからの最短距離を求める long long VV; vector<vector<edge>> G; void dijkstra(vector<long long> &dist, long long s) { //greater<P>によってfirstが小さい順に取り出される priority_queue<P, vector<P>, greater<P>> que; for (long long i = 0; i < VV; i++) dist[i] = 1e18; dist[s] = 0; que.push(P(0, s)); //queが空になるまで繰り返す while (!que.empty()) { P p = que.top(); que.pop(); long long v = p.second; if (dist[v] < p.first) continue; //すでに最短距離ならスルー for (long long i = 0; i < G[v].size(); i++) { edge e = G[v][i]; //更新作業 if (dist[e.to] > dist[v] + e.cost) { dist[e.to] = dist[v] + e.cost; que.push(P(dist[e.to], e.to)); } } } } int main() { cout << fixed << setprecision(30); long long N, M, S; cin >> N >> M >> S; VV = N * 2501; //とりあえず2500枚あれば安泰(0~2500の遷移) G.resize(VV); //グラフを確保 //入力受け取り vector<long long> U(M), V(M), A(M), B(M); vector<long long> C(N), D(N); for (long long i = 0; i < M; i++) { cin >> U[i] >> V[i] >> A[i] >> B[i]; U[i]--; V[i]--; } for (long long i = 0; i < N; i++) cin >> C[i] >> D[i]; //まずは同じ頂点の上部(銀貨が多い方向)へ向かう有向辺を結ぶ for (long long i = 0; i < N; i++) { for (long long j = 0; j < 2500; j++) { edge e; if (j + C[i] >= 2501) e.to = i * 2501 + 2500; //行きすぎたら切り捨てる(max2500枚) else e.to = i * 2501 + (j + C[i]); e.cost = D[i]; G[i * 2501 + j].push_back(e); } } //次に違う頂点間を渡るとき銀貨の必要枚数だけ下るような有向辺を結ぶ(頂点の最下部(銀貨0)より下にいったら結ばない) for (long long i = 0; i < M; i++) { //uからvへの有向辺 for (long long j = 2500; j >= 0; j--) { //j=2500~>0,jはuの現在のコインの枚数 long long nextcoin = j - A[i]; //nextcoin:vの現在のコインの枚数 if (nextcoin < 0) break; edge e; e.to = V[i] * 2501 + nextcoin; e.cost = B[i]; G[U[i] * 2501 + j].push_back(e); } //vからuへの有向辺 for (long long j = 2500; j >= 0; j--) { //j=2500~>0,jはvの現在のコインの枚数 long long nextcoin = j - A[i]; //nextcoin:uの現在のコインの枚数 if (nextcoin < 0) break; edge e; e.to = U[i] * 2501 + nextcoin; e.cost = B[i]; G[V[i] * 2501 + j].push_back(e); } } vector<long long> dist(VV); dijkstra(dist, min(S, 2500ll)); //頂点0でコインをmin(S,2500)持っている状態からスタート //都市t(t=1~>N-1)について、各コインの状態を全探索(一応) for (long long i = 1; i < N; i++) { //i=1~>N-1 long long ans = 1e18; for (long long j = 0; j <= 2500; j++) ans = min(dist[i * 2501 + j], ans); cout << ans << endl; } return 0; }