SoundHound Inc. Programming Contest 2018 -Masters Tournament-_D_Saving Snuuk
青色の問題.最小コストは求められる.そっからどうするか.グラフの問題は難しい.
問題の説明
ある国には円とスヌークという2つの通貨が存在する.個の都市は本の電車で繋がっており,どの都市からどの都市へも行ける事が保証されている.
本の電車には運賃が設定されており,番目の電車の運賃を円で払う場合は円,スヌークで払う場合はスヌークを払う必要がある.().
移動に際し,個の都市のどこかカ所で円をスヌークに両替したいと考えています(移動始めと終わりの都市で両替してもよい).両替をする際は持っている円をすべてスヌークに変えなければなりません. 最初,両替所はすべての都市で使用可能であるが,番目の両替所は年後に閉鎖され,年後から使用出来なくなります.
始めに円を持ち,できるだけ多くのスヌークを残した状態で都市からへ向かいます.年のそれぞれについて移動した際のスヌークの最大量を出力しなさい.ただし,移動中に年を跨ぐ事はないとします.
制約
考えた事
問題が込み入っていて難しい.とりあえず年数を経る毎に徐々に選択肢が狭まって行く事はわかるだろう. となれば考えたくなるのは選択肢が狭い方であろう.最終年(年)から考えてみよう.
年目の時,都市の両替所は閉鎖されており,使えるのは都市の両替所のみである.つまり,実質的にを経由し,両替する事が必須の条件となる.
という事は,年目では(まで円を用いて移動する時の最小コスト)+(までスヌークを用いて移動する時の最小コスト)とすればよい.
の移動の最小コストを求める問題は「単一始点最短路問題」として知られており,ここではDijkstra
法を用いて解く.1
Dijkstra
法の実装は最後に貼るコードを参照のこと.計算量はであり,現実的である.
間も同様に行えばいいだろうか.そうとはいかない. 次に年目を考えてみよう.問題文から両替所として使えるのは,のいずれかに絞られる.に関してはさきほど考えたので考えなくてよい.
に関してはにおいて用いたDijkstra
を利用すればよい.しかし,を移動する際の最小コストの計算はどうしようか.
もちろんを始点としたDijkstra
法で求められる.しかし,それを年数が減る度に一々求め直していたらムダが多いのは明らかである.実際計算量もとなりTLEする.
これを解決するためにはどうしようか.なるべくDijkstra
法を使う回数を減らしたい.ここでのポイントは終点がで固定されている点だ.そして,都市個を繋ぐ鉄道には向きがない,つまりこれは無向グラフである点もポイントである.
つまり,ここでの答えは,と見るのではなく,,と見る事だ.そうする事で始点でのDijkstra
法を使い回す事が出来,計算量も間に合う.2
まとめよう.この問題はまずを始点,コストを円として,Dijkstra
法を行い,次にを始点,コストをスヌークとしてDijkstra
法を行えばよい.以降,両替所がであるときの最小コストを便宜的に前者を,後者をと表記する.
最後に各年の移動コストの最小値を出して終わりにしよう.ここでも下(年)から見る考え方が活きる.
まず年の時の最小コストはもちろんである.両替所はしか使えないからだ.
次に年を考える.両替所の候補はの二カ所のみなので,最小コストはである.
ここでが再度参照されている事に気付くだろう.勘の良い方はもうピンときたはずだ.
ある年での移動にかかる最小コストは,となる.これは簡単なDP
で実装ができる.
repd(i, n - 1) { CHMIN(dif.at(i), dif.at(i + 1)); }
として後ろから決めていけばいい.以上で年から年まで決めて行く事でこの問題は解ける.
この問題で難しかった点は計算量である.雑多な方針が(Dijkstra
法を知っていれば)すぐに思いつく.しかしそれを考え無しに実装するだけではTLEとなる.始点と終点を入れ替える,逆の年数から決めていくといった工夫が必要だ.
大事なのは選択肢が多いものを後回しにして,選択肢が少ない方から決定し,それを活用出来ないかを考える事である.これはDP
を始めとして競技プログラミングで頻出のテクニックであり,さまざまな場面でみかけるものだ.青色レベルの高度な問題でもこういった基盤の部分をきっちり付いてくる点は恐ろしく,その点において非常に教育的な問題であった.この問題を解いた経験を忘れずに,今後の競技プログラミングの進捗を増やしていきたい.
本日もお読みくださりありがとうございました.
プログラム
#include <bits/stdc++.h> using namespace std; using ll = long long; using vll = vector<long long>; const long long INF = 1e18; #define rep(i, n) for (ll i = (0); i < (ll)(n); i++) #define repd(i, n) for (ll i = n - 1; i >= 0; i--) template <class T> inline T CHMIN(T& a, const T b) { return a = (a > b) ? b : a; } struct edge { ll to; ll cost; }; vll dijkstra(ll start, ll choten, vector<vector<edge>>& G) { // 開始地点と頂点数, グラフ priority_queue<pll, vpll, greater<pll>> que; // firstが最短距離,secondが頂点 vll dist(choten, INF); // startからの最短距離の配列,最初はINF dist.at(start) = 0; // startだけで0で置いとく que.push(pll(0, start)); while (!que.empty()) { pll p = que.top(); que.pop(); ll v = p.second; if (dist.at(v) < p.first) { continue; } rep(i, G.at(v).size()) { edge e = G.at(v).at(i); if (dist.at(e.to) > dist.at(v) + e.cost) { dist.at(e.to) = dist.at(v) + e.cost; que.push(pll(dist.at(e.to), e.to)); } } } return dist; } int main() { ll n, m, s, t; cin >> n >> m >> s >> t; s--; t--; vll ui(m), vi(m), ai(m), bi(m); vector<vector<edge>> G(100010);// グラフの情報を格納 rep(i, m) { ll u, v, a, b; cin >> u >> v >> a >> b; u--; v--; edge p = {v, a};// s→iの最小コストを先に決める edge q = {u, a}; G.at(u).push_back(p); G.at(v).push_back(q); ui.at(i) = u; vi.at(i) = v; ai.at(i) = a; bi.at(i) = b; } vll dista = dijkstra(s, n, G); rep(i, 100010) { G.at(i).clear(); } rep(i, m) { edge p = {vi.at(i), bi.at(i)};// i←tの最小コスト edge q = {ui.at(i), bi.at(i)}; G.at(ui.at(i)).push_back(p); G.at(vi.at(i)).push_back(q); } vll distb = dijkstra(t, n, G); vll dif; rep(i, n) { dif.push_back(dista.at(i) + distb.at(i)); }// s→i→tの最小コストを格納 repd(i, n - 1) { CHMIN(dif.at(i), dif.at(i + 1)); } // 後ろと比較 rep(i, n) { ll ans = 1e15 - dif.at(i); cout << ans << endl; } }