こんにちは、
競プロのしすぎで編入試験の勉強とレポートが手についてない愚かな少女は誰でしょう。
そう、しんちゃんです。
AGC-Fを解いてみたかったので、difficultyが一番低いものを選んで解いてみました。
1600点ってなんだかやばそうですね(笑)。現在AtCoderのレートが1784(青)である私だと3時間かかりました。
atcoder.jp
以前解いていた別の赤diffもオススメです(知識が必要なく、あきらめなければ解けるはずです)。
こちらも2時間かかりました。
atcoder.jp
ということで とかで解きたくなります。
気にしておく性質として、木は二部グラフであり、高橋君のターンの初めに駒が置いてある頂点は限られています。このとき、青木君のターンの初めに駒が置いてある頂点は、高橋君と被ることがありません。
つまり、頂点ごとに誰のターンかがわかります。
まぁまずは、ある頂点からスタートしたとして、勝敗と計算量を考慮せず考えてみましょう。
その頂点( としましょう)と、その頂点と隣接してる頂点( としましょう)において…、
- すべての において、 とき、高橋君が負けることがわかります。
なぜなら、 からどこへ進もうとも、青木君が へ戻すことができるからです。
このとき、配列 を用意して、 とします。
- 次に、ある が存在して、 かつ を満たすならば、高橋君は勝ちます。
ここで、 の条件だけでいいと勘違いしてしまう人は多いと思います。
こう考えるといいです。 高橋君スタートで にいたとすると、 は 1 減り、 へ移動します。 が新たな としてスタートです。
このとき、青木君の負けが確定するためには、元の において、 が必要です。
ここで注意するのは、 は 1 減ったものであるということです。なので、 、つまり という条件が必要になります。
このとき、配列 を用意して、 とします。
- 3つ目。すべての において、 ならば です。
こちらはわかりやすいと思われます。
さらに、すべての において、 または ならば です。
さて、次の段階に移りましょう。
ならば と の間の辺を消せます。
なぜなら から へ移っても意味がない(どうせ負ける)からです。
そして辺を消したとき、頂点が 1 つの連結成分が生まれてしまうときがあります。
この場合は、その頂点 から となる へ辺を追加しても結果が変わらないことから、負けになるとわかります。
同様に、 かつ となる と の間の辺も消してあげましょう。
最後の段階です。
辺を消すのと上記の操作を繰り返せば、いつかは全ての頂点 において、 もしくは がなりたち、 となる を昇順に出力すればいいです。
辺を消せば(もしくは未操作の段階で)、連結成分内で が最小となる頂点 が少なくとも 1 つ存在し、 となります。
そのような頂点が現れたとき、その頂点に隣接する頂点 を考えると、明らかに です。 であれば、 とできるので、 の場合を考えましょう。このとき、 は連結成分内で最小ですから、 も連結成分内で最小となります。したがって、 となります。よって、 の勝敗は確実に決定できるのです。
以上より、上記の操作 1 回につき、 となる が少なくとも 1 つできます。そして と接続している辺を消せます。辺が消えればもちろん頂点の勝ち負けも決まっていきます。
辺の個数は ですから、繰り返しは 回を超えることはないとわかります。
各繰り返しで、各頂点と隣接する頂点の探索、そして辺の削除で log ですから、この問題の時間計算量は、log となり、間に合います。(logが影響しやすいグラフほどループの回数が減るので←ほんとか?)
2023/12/04追記
辺の削除回数は合計でlogであり、削除を試みる回数はであり、辺を根こそぎとられた頂点から辺を探索するのはなので、解法全体の時間計算量はとなります。
ソースコードはこちら。
atcoder.jp
#include <bits/stdc++.h> #define be(v) (v).begin(),(v).end() #define pb(q) push_back(q) #define rep(i, n) for(int i=0;i<n;i++) #define all(i, v) for(auto& i : v) typedef long long ll; using namespace std; const ll mod=1000000007, INF=(1LL<<60); #define doublecout(a) cout<<fixed<<setprecision(10)<<a<<endl; int main() { cin.tie(0); cout.tie(0); ios::sync_with_stdio(false); int n, a, b; cin >> n; vector<set<int> > v(n); // グラフ vector<int> c(n); rep(i, n) cin >> c[i]; rep(i, n - 1){ //辺の追加 cin >> a >> b; a--; b--; v[a].insert(b); v[b].insert(a); } bool lose[n], win[n]; // 負け、勝ち memset(lose, 0, sizeof(lose)); memset(win, 0, sizeof(win)); bool con = false; // ループ継続判定 do{ con = false; // 負ける頂点を増やす。 rep(i, n){ if(lose[i] || win[i]) continue; bool ok = true; all(to, v[i]) if(c[to] < c[i]) ok = false; if(ok) lose[i] = true; } // 勝てる頂点を増やす。 rep(i, n){ if(lose[i] || win[i]) continue; bool ok = false; all(to, v[i]) if(lose[to] && c[to] < c[i]) ok = true; if(ok) win[i] = true; } // 勝つ頂点の周りの辺を消す。 rep(i, n){ set<int> pos; all(to, v[i]) if(win[i] || win[to]) pos.insert(to); all(to, pos){ v[i].erase(to); v[to].erase(i); } } // lose[i] == true かつ c[i] >= c[to] となる辺を消す。 rep(i, n){ if(!lose[i]) continue; set<int> pos; all(to, v[i]) if(c[i] >= c[to]) pos.insert(to); all(to, pos){ v[i].erase(to); v[to].erase(i); } } // まだ決まってないのがあるなら続ける。 rep(i, n) if(!lose[i] && !win[i]) con = true; }while(con); rep(i, n) if(win[i]) cout << i + 1 << " "; cout << endl; return 0; }
みなさまよいお年を~~!!!!
冬休みクラスメートと会えなくて暇なので(←暇ではない。寂しいので)通話などのお誘い待ってます。