AtCoder Beginner Contest ABC025 C - 双子と○×ゲーム
C - 双子と○×ゲーム
解説
まず、置く順番は通りありますが、すべて試していいだろうとわかります。順列の列挙は、簡単に行う場合はnext_permutationを使いますが、今回は順列の生成とともにゲーム木を構成したいので、自前で実装します。基本的なDFSです。
木が構成されたとき、偶奇によって先攻と後攻どちらに意思決定権があるかが変わります。ゲーム木が葉までいったとき、順番が決まるわけですが、その順番で置いたときの先攻と後攻の得点を計算します。そして、意思決定権のある方の得点が最大になるような手のみを親に伝播させていくことで、最初にどちらの手を打つべきかがわかり、このゲームにおける最適な行動がわかるというわけです。
より簡単に考えると、2人の得点の和は一定なので、両方を保持する必要はなく、先攻の得点のみを記録し、先攻が決める場合は最大、後攻が決める場合は最小のように実装しても同様の結果になります。
以上のような実装で、計算回数は回ほどで終わり、間に合います。
実装
int b[10][10], c[10][10]; int d[10][10]; bool visited[10]; // 順列が与えられるので、計算する i_i calc(vector<int> &v){ int sz = v.size();// = 9 rep(i,sz){ int x = v[i] % 3; int y = v[i] / 3; d[x][y] = i&1; } int l = 0, r = 0; rep(i,2)rep(j,3){ if(d[i][j] == d[i+1][j])l += b[i][j]; else r += b[i][j]; } rep(i,3)rep(j,2){ if(d[i][j] == d[i][j+1])l += c[i][j]; else r += c[i][j]; } return make_pair(l,r); } i_i dfs(vector<int> &v){ if(v.size() == 9)return calc(v); int sz = v.size(); int maxx = -1; i_i res; rep(i,9){ if(visited[i])continue; v.emplace_back(i); visited[i] = true; auto p = dfs(v); int l = p.first; int r = p.second; v.pop_back(); visited[i] = false; if(sz&1){ // lをmaxにする手を選ぶ if(chmax(maxx, r))res = p; } else{ // rをmaxにする手を選ぶ if(chmax(maxx,l))res = p; } } return res; } int main() { rep(i,2)rep(j,3)cin >> b[i][j]; rep(i,3)rep(j,2)cin >> c[i][j]; rep(i,10)visited[i] = false; vector<int> v; auto ans = dfs(v); cout << ans.first << endl; cout << ans.second << endl; }
AtCoder Grand Contest AGC005 B - Minimum Sum
B - Minimum Sum
解説
区間がたくさん与えられるので、その中のある値の和を求めよという問題は多くありますが、基本的に区間をすべて列挙して解くことはできません。今回も例によってそのパターンで、ある値が、いくつの区間の値になるかを求める問題だと捉えることでソリューションを見出します。いわゆる寄与ゲー、主客転倒と言われる部類の問題です。
ある値が最小値になる区間は、左右に広がっているので、その境界を二分探索することでうまくいきます。左右の境界が求まったら、左右の境界の組み合わせが寄与する個数になるので、で解くことができました。
実装
template<typename T> class SegTree { public: int N;//葉の数 vector<T> data;//配列 T unit;//単位元 function<T(T,T)> op;//区間クエリで使う処理 function<T(T,T)> update;//点更新で使う処理 T _query(int a, int b, int k, int l, int r) { if(r <= a || b <= l)return unit; if(a <= l && r <= b){ return data[k]; } else{ T c1 = _query(a, b, 2 * k + 1, l, (l + r) / 2); //左の子 T c2 = _query(a, b, 2 * k + 2, (l + r) / 2, r); //左の子 return op(c1, c2); } } //コンストラクター //_N: 必要サイズ, _unit: 初期値かつ単位元, _op: クエリ関数, _update: 更新関数 SegTree(int _N, T _unit, function<T(T, T)> _op, function<T(T, T)> _update) :unit(_unit), op(_op), update(_update){ N = 1; while(N < _N)N *= 2; data.assign(2 * N - 1, unit); } //i(0-indexed)の値にupdate関数を適用 void change(int i, T x){ i += N - 1; data[i] = update(data[i], x); while(i > 0){ i = (i - 1) / 2; data[i] = op(data[i * 2 + 1], data[i * 2 + 2]); } } //[a, b)の区間クエリの実行 T query(int a, int b){ return _query(a, b, 0, 0, N); } //添字でアクセス T operator[](int i) { return data[i + N - 1]; } }; int main(){ int N; cin >> N; long long inf = (1LL << 31) - 1; auto f = [&](long long a, long long b){ return min(a, b); }; auto g = [&](long long a, long long b){ return b; }; SegTree<long long> Tree(N,inf,f,g); vector<ll> A(N); rep(i, N)cin >> A[i]; rep(i,N)Tree.change(i, A[i]); auto checkl = [&](int l, int r) -> bool{ return Tree.query(l,r) == A[r-1]; }; auto checkr = [&](int l, int r) -> bool{ return Tree.query(l,r) == A[l]; }; ll ans = 0; rep(i,N){ // calc 左端と右端を計算する O(logN) ll l, r; { int ng = -1; int ok = i; while(ok - ng > 1){ int mid = (ok + ng) >> 1; if(checkl(mid, i+1))ok = mid; else ng = mid; } l = i+1 - ok; } { int ok = i+1; int ng = N+1; while(ng - ok > 1){ int mid = (ok + ng) >> 1; if(checkr(i,mid))ok = mid; else ng = mid; } r = ok - i; } ll cnt = l * r; ans += cnt * A[i]; } cout << ans << endl; }
AtCoder Beginner Contest 174 ABC174 参加記
Eまでノーペナ30分だったのにF既出を検索できずにしょっぱいパフォを取ってしまいました。もったいなかったです。
jjjjjjjtgpptmjjさんのAtCoder Beginner Contest 174での成績:1161位
— naga@精進 (@longeqe) 2020年8月2日
パフォーマンス:1476相当
レーティング:1307→1325 (+18) :)#AtCoder #ABC174 https://t.co/94Y1w9iGl9
目次
A - Air Conditioner
解説
if文を使います。C++では以下のような書き方でifを使わずに書くことができます。
実装
int main() { int N; cin >> N; cout << ((N >= 30) ? "Yes" :"No") << endl; }
B - Distance
解説
ルートは外して整数で考えましょう。intだとオーバーフローするので、long longを使いましょう。
実装
int main() { ll N, D; cin >> N >> D; ll ans = 0; rep(i,N){ ll x, y; cin >> x >> y; ans += (x*x+y*y) <= D*D; } cout << ans << endl; }
C - Repsept
解説
modは、基本的に桁ごとにみていいです。 これ説明難しいです、エスパーして大きめに回すと解を得ます。
実装
int main() { ll K; cin >> K; vector<ll> v(10*K); v[0] = 0; rep(i,10*K){ v[i+1] = (v[i]*10 % K) + 7; v[i+1] %= K; if(v[i+1] == 0){ cout << i+1 << endl; return 0; } } cout << -1 << endl; }
D - Alter Altar
解説
Rを左に寄せれば良いので、まず既存のRの個数を計算し、先頭から長さの中にあると右にあるをswapすることを考えれば良いです。これが操作回数の最小値です。
実装
int main() { int N; cin >> N; string S; cin >> S; ll cntr = 0; rep(i,N)cntr += S[i] == 'R'; ll ans = 0; rep(i,cntr)ans += S[i] == 'W'; cout << ans << endl; }
E - Logs
解説
最大値の最小化問題なので、二分探索をしたくなります。いわゆる決めうち二分探索です。全てを以下にするために必要な操作回数をで求めることができれば、全体の計算量がになります。
長さを以下の長さに分けるのに必要な回数は、(切り上げ)です。
実装
int main() { ll N, K; cin >> N >> K; vector<ll> A(N); rep(i, N)cin >> A[i]; sort(all(A)); auto check = [&](ll X) -> bool{ ll cnt = 0; rep(i,N)cnt += (A[i] + X - 1) / X; return cnt-N <= K; }; ll ng = 0; ll ok = A[N-1]; while(ok - ng > 1){ ll mid = (ok + ng) >> 1; if(check(mid))ok = mid; else ng = mid; } cout << ok << endl; }
F - Range Set Query
感想
既出臭がプンプンするのでぐぐると出てくるようです。私は英語でググっていたので全くヒットしませんでした。
AtCoder Beginner Contest ABC023 C - 収集王
C - 収集王
解説
行にある点の個数が個のとき、列が個のとき和がになりそうなところから考察を進める。
ここで、列が個の列の個数を追加すれば良さそうだが、ここに罠がある。その列に点があるときはその列は、点がないときはであることが条件であり、そのように単純に数えることができない。
よって、まず個の点を持つ列の個数を追加した後、実際の頂点を見て、その列が個だったら削除し、だったら追加するというアルゴリズムでうまく数えることができる。
計算量は。
実装
#include <bits/stdc++.h> using namespace std; #define rep(i,N) for(int i=0;i<int(N);++i) int H, W, K, N; int x, y; int mpy[100050]; vector<pair<int, int>> v[100050], vv[100050]; int main() { cin.tie(0); ios::sync_with_stdio(false); cin >> H >> W >> K; cin >> N; rep(i,N){ cin >> x >> y; x--; y--; v[x].emplace_back(x,y); vv[y].emplace_back(x,y); } rep(j,W)mpy[vv[j].size()]++; long long ans = 0; rep(i,H){ int r = v[i].size(); int add = mpy[K-r]; for(auto p: v[i]){ int cnty = vv[p.second].size(); if(cnty == K-r)add--; if(cnty == K-r+1)add++; } ans += add; } cout << ans << endl; }
AtCoder Grand Contest AGC025 B - RGB Coloring
B - RGB Coloring
解説
各マスの状態が4種類ある
- 無
デフォルトを無だと考えて、そこに得点を割り振っていくと考えると、にするかしないかを独立に決めて、重なったらにすると考えればよいことがわかる。
よって、の個数をに決めれば、和がになるの個数は一意に定まり、
になる。ただしが条件。
の個数がそれぞれになるような塗り方はである。これをすべてのについて足し合わせればよい。
計算量は。
実装
struct mint { }; struct combination { }C(300010); int main() { ll N, A, B, K; cin >> N >> A >> B >> K; mint ans = 0; for(ll i = 0; i <= N; i++){ ll rem = K - A * i; if(rem < 0)continue; if(rem % B > 0)continue; ll j = rem / B; ans += C.Comb(N,i) * C.Comb(N,j); } cout << ans << endl; }
AtCoder Beginner Contest ABC008 C - コイン
解説
公式解説とは異なる方針でACしたので記録を残しておきます。
まず、いわゆる主客転倒(寄与ゲー)と呼ばれる問題だと捉えます。 今回求めたいのは、の並びのうち、表のコインの数の期待値ですが、 これを期待値ではなく数え上げの問題として認識します。 つまり、表のコインの枚数を数え上げ、最後にで割れば期待値の計算ができます。
これを、あるコインが表になる場合の数を足し合わせることで計算します。
あるコインが表になるのは、そのコインより左のコインの中に の約数が偶数個ある場合です。
まず、コイン以外での約数の個数をとします。
コインが位置にあって、その左に個コインがある場合を数え上げればよく(は偶数)、これはcombinationとpermutation、階乗の計算を前計算すれば解くことができます。
- の約数個から左に置くコイン個を選び、
- 選んだ約数個を左への並べ方が通り(場所を選んで並べるイメージ)、
- 残りの約数個を右への並べ方が通り(場所を選んで並べるイメージ)、
- 約数以外の並べ方が(場所は上で確定しているので、並べるだけ)通り。
計算量はです。
実装
ところでこの階乗の計算で誤差ってどこまで許容されるんですかね(わかっていない顔)。
#include <bits/stdc++.h> using namespace std; #define rep(i,N) for(int i=0;i<int(N);++i) int main() { cout << fixed << setprecision(20); int N; cin >> N; vector<int> C(N); rep(i, N)cin >> C[i]; vector<int> cnt(N,0); rep(i,N)rep(j,N){ if(i == j)continue; if(C[j]%C[i] == 0)cnt[j]++; } long double kai = 1; for(int i = 1; i <= N; i++)kai /= i; vector<long double> fact(N+1); fact[0] = kai; rep(i,N)fact[i+1] = fact[i] * (i+1); auto comb = [&](int a, int b) -> long double{ if(a == 0 || b == 0)return 1; if(b > a)return 0; return fact[a] / fact[a-b] / fact[b] * kai; }; auto perm = [&](int a, int b) -> long double{ if(a == 0 || b == 0)return 1; if(b > a)return 0; return fact[a] / fact[a - b]; }; long double ans = 0; // あるコインiが場所jにいて // 自分より左にk個、右にcnt[i] - k個おくときの場合の数 // kは偶数のみ rep(i,N)rep(j,N){ for(int k = 0; k <= j && k <= cnt[i]; k+=2){ int S = cnt[i]; if(k > j)continue; if(S-k > N-1-j)continue; long double tmp = comb(S,k) * perm(j,k) * perm(N-1-j,S-k) * fact[N-1-S]; ans += tmp; } } cout << ans << endl; }
公式解説
公式の解説では、もっと簡単に問題を捉えています。 まず、期待値の線形性を考えれば、コインごとの期待値の足し合わせが答えになることがわかります。
コインについて考えると、期待値を計算するのであれば、とても簡潔に考えることができます。 コインの位置について、約数の並べの中でどの位置にあるかを考えます。この位置に関して、共通して現れる計算として、約数の並べ方と約数以外の並べ方があります。これらはコインが約数の並べの中でどの位置になるかに依存しません。まず約数を並べて、位置を選び、その後約数以外を挿入すると考えるとわかりやすいでしょう。よって、約数の個数のみを考慮すれば良いことになります。
考える順番を変えるだけでかなり楽に計算することができました。
Codeforces Round #658 (Div. 2) 参加記
B - Sequential Nim
最初にに到達した方が、常に先に山に手をつけることができる。これは、次がなら、その山をだけ残して相手に渡し、次がの場合は山を全て取ることで実現できる。よって、到達するまでの回数を数えればよい。注意点として、最後の山は数えない。
void solve(){ int N; cin >> N; vector<int> a(N); rep(i,N)cin >> a[i]; int cnt = 0; rep(i,N-1){ if(a[i] == 1)cnt++; else break; } cout << ((cnt%2 == 0)? "First": "Second") << endl; }
C - Prefix Flip
Easy Version
右から揃えていく。が揃っていない場合、反転させるためには、であることが条件である。この条件を満たしていない場合は、先頭だけの処理を挟めばよい。
である。
void solve(){ int N; cin >> N; string a, b; cin >> a >> b; auto f = [&](int x){ //cerr << x << ":" << endl << a << endl; rep(i,x+1){ a[i] = char(((a[i] - '0') ^ 1) + '0'); } reverse(a.begin(), a.begin()+x+1); //cerr << a << endl; }; vector<int> ans; for(int i = N-1; i >= 0; i--){ if(a[i] == b[i])continue; if(a[0] == a[i]){ f(i); ans.push_back(i+1); } else{ f(0); ans.push_back(1); f(i); ans.push_back(i+1); } //dump(a); } cout << ans.size() << " "; for(auto v: ans){ cout << v << " "; } cout << endl; }
Hard Version
右から揃えていくのは変わらないが、reverseを実装するとTLEになる。 ここで、reverseする代わりに、左右端のindex を持ち、reverseされることをswap(l,r)に言い換える。また、間が何回反転されたかも持つ。間がどこにあるかを持つ必要はない。先頭だけの処理もあるので、個別の反転回数も持って、合計何回反転されたかをもてば偶奇でわかるようになる。
。
void solve(){ int N; cin >> N; string a, b; cin >> a >> b; vector<int> x(N), y(N); rep(i,N){ x[i] = a[i] - '0'; y[i] = b[i] - '0'; } int l = 0; int r = N-1; int cnt_all = 0; vector<int> cnt(N,0); vector<int> ans; for(int i = N-1; i >= 0; i--){ int xr = x[r] + cnt_all + cnt[r]; int xl = x[l] + cnt_all + cnt[l]; xr %= 2; xl %= 2; if(xr == y[i]){ if(l < r)r--; else r++; continue; } if(xl != xr){ xl ^= 1; cnt[l]++; ans.push_back(1); } swap(l,r); ans.push_back(i+1); cnt_all++; if(l < r)r--; else r++; } cout << ans.size() << " "; for(auto v: ans){ cout << v << " "; } cout << endl; }