しがない元高専生の競プロ日記

AtCoderとかいろいろ解いてく

AtCoder 水色になりました

f:id:honehaniwa:20200308232842p:plain

何をしたか

早解きをしました.

もう緑には戻らないぞ...

第6回 ドワンゴからの挑戦状 予選 B - Fusing Slimes 解説

問題

URLはこちら↓ atcoder.jp

スライムが移動した距離の期待値に(N-1)!をかける問題。

問題を解いても実装面で罠があったりと大変だったので記事にしました。

解説

まず、問題の言い替えが発生します。

サンプルのケースはわかりにくいので、N=4,A={1, 2, 3, 4}のケースを考えます。

この時、スライムの移動する順番は(N-1)!通りあって、以下のようになります。

スライムを左から1,2,3,4とすると、
123
132
213
231
312
321
の6通り

ここから、上記の各移動順序での移動距離の和を計算してみると、以下のようになります。

123 -> 移動距離 3
132 -> 移動距離 4
213 -> 移動距離 4
231 -> 移動距離 5
312 -> 移動距離 4
321 -> 移動距離 6
  • 試しに2個目の移動方法をシミュレーションすると、1->2で距離1、3->4で距離2、2->4で距離4となります。

ここから各移動距離の期待値を求めます。各移動パターンの期待値の求め方は、

移動距離/(N-1)!で求まります。

よって全てのパターンの移動距離の期待値はΣ(各移動距離 / (N-1)!)になります。


しかし、ここであることに気付きます。

Σ(各移動距離 / (N-1)!) = 移動距離の総和 / (N-1)!と言い替えることができます。

で、この問題は最後に(N-1)!をかけたものを答えとして出力する問題です。

つまり、この問題は各移動距離の総和を求める問題に変換できたことが分かります。


それでは、移動距離の総和を求めてみましょう。

まず、i番目のスライムがj番目のスライムに合体する数を数えてみましょう。

上記の例の1番目のスライムを例に考えてみると、

123 -> 1番目のスライムは2と合体
132 -> 1番目のスライムは2と合体
213 -> 1番目のスライムは3と合体
231 -> 1番目のスライムは4と合体
312 -> 1番目のスライムは2と合体
321 -> 1番目のスライムは4と合体

{i,j} = {1,2}の時3個、
{i,j} = {1,3}の時1個、
{i,j} = {1,4}の時2個となる。

では次に2番目のスライムを例にすると、

123 -> 2番目のスライムは3と合体
132 -> 2番目のスライムは4と合体
213 -> 2番目のスライムは3と合体
231 -> 2番目のスライムは3と合体
312 -> 2番目のスライムは4と合体
321 -> 2番目のスライムは4と合体

{i,j} = {2,3}の時3個、
{i,j} = {2,4}の時3個となる。

3番目のスライムはどこにいても4番目に移動するので6個になります。

これを図に表すとこうなります。

f:id:honehaniwa:20200113191930p:plain

この図を見てみると、1->31->4の時、1->2を通っていることに気づきませんか?

つまり、1->3は、1->2 + 2->3 と変形することができます。

これを図に表すと、以下のようになります。

f:id:honehaniwa:20200113194530p:plain [追記]数値が間違ってたので修正しました

なんと、各iからjへの移動距離は(N-1)! / (これまで通った区間の数)になっていることが分かります。

よってこれらの数×各区間の距離%modを求めれば解けそうです!

実装パート

さて、ようやく実装パートに入りました。

実装においても大きな落とし穴が存在しますので、気を引き締めていきましょう。

※今回はPythonで実装を行います。一応私のメイン言語はC++なので分かりやすく書いてるつもりなのですが最後にC++でのACソースコードを貼りましたのでそちらを参考にしていただけると幸いです。

では実装していきましょう。

n=int(input())
A=list(map(int,input().split()))
mod = 10**9 + 7
# (N-1)!を求める
fac=1
for i in range(1,n):
  fac = fac*i%mod
# 個数テーブルを準備する
cnt=[0]*n
for i in range(1,n):
  cnt[i] = fac // i % mod
  cnt[i] = (cnt[i]+cnt[i-1]) % mod
# 計算
ans=0
for i in range(n-1):
  ans += (A[i+1]-A[i]) * cnt[i+1]
  ans %= mod

print(ans)

出来ました!提出します!

Submission #9496276 - Dwango Programming Contest 6th

f:id:honehaniwa:20200113195244p:plain
...

f:id:honehaniwa:20200113195435p:plain

こうなりました。

方針が間違っているのでしょうか?

実は違います。正体はmodにおける割り算の方法に問題があるからです!

qiita.com

詳しい説明はこちらの記事にて紹介されています。(けんちょんさんいつもありがとうございます)

ここでは簡単な説明のみとさせていただきますが、

  1. modの割り算ではどんな数字も割り切れる。
    -> 9/4 (mod 13) = 12(なぜなら4×12=48 ≡ 9(mod13))
  2. ではmod付きの正しい割り算の結果を知りたい。
  3. 求めるには逆元なるものが必要で、割りたい数×(割る数の逆元)で計算できるらしい!
  4. 逆元はフェルマーの小定理で計算できるらしい!
  5. デキタヤッター!

フェルマーの小定理の説明も詳しくはこっちで解説されてます。

qiita.com

では、逆元の計算を実装して完成です!

mod=10**9+7
# mod inverse(逆元)
# 今回はfermatの小定理で求める
def mod_inv(x):
  return pow(x,mod-2,mod)

n=int(input())
A=list(map(int,input().split()))
# (N-1)!を求める
fac=1
for i in range(1,n):
  fac = fac*i%mod
# 各区間の個数テーブルを先に求める
cnt=[0]*n
for i in range(1,n):
  cnt[i] = fac * mod_inv(i) % mod
  cnt[i] = (cnt[i]+cnt[i-1]) % mod
# 計算
ans=0
for i in range(n-1):
  ans += (A[i+1]-A[i]) * cnt[i+1]
  ans %= mod

print(ans)

AC結果はこちらです。

atcoder.jp

感想

考察は450くらいでした(水になれて冷えた人がコンテストほぼギリギリまで考えて考察できるレベル)が、実装で罠が張って合ってやっぱり600だなぁと感じました。

今回でmodの割り算ができるようになったので今後のコンテストで差をつけていきたいです!

ABC149 - E Handshake 解説

[追記]スマホで見るとMathJaxが死んでる可能性が高いです。PCで見ることを推奨します。

解説でもヒントが少なく、人によってソースコードが大きく違ったりと苦戦したので残しておこうと思いました。

今回は具体的な解法の説明を重視しています(思考過程等の解説は無し、実装方針の説明に寄ってます)。

コンテスト自体はunratedなので途中で諦めた人も多い気がします。 atcoder.jp

問題の意訳

配列 \(A\) をタテ、ヨコにした100マス計算みたいな感じの表を作った時、できた数の上位 \(M\) 個の総和を求める。

簡単にするとこんな感じ(入出力例1の例)

\begin{array}{|c|c|c|c|c|c|} \hline + & 10 & 14 & 19 & 33 & 34 \\ \hline 10 & 20 & 24 & 29 & 43 & 44 \\ \hline 14 & 24 & 28 & 33 & 47 & 48 \\ \hline 19 & 29 & 33 & 38 & 52 & 53 \\ \hline 33 & 43 & 47 & 52 & 66 & 67 \\ \hline 34 & 44 & 48 & 53 & 67 & 68 \\ \hline \end{array}

\(68 + 67 + 67 = 202\) で \(202\) が答えとなる。

簡単な解法の説明

今回の問題は2つのパートに分かれていて、

  1. 増える幸福度が上位M個に入る最小の和を二分探索する。
  2. 累積和を用いて総和を求める。

という2パートに分かれています。

ここで大きな罠があって、実は1,2番で具体的に書かれていない二分探索の判定パートと総和の求め方が分かりにく過ぎる(というか解説PDFで具体的な解説無し)ので大きく苦戦します(しました)。

という訳で具体的な実装までできるだけ丁寧に説明しようと思います。分かりにくかったらごめんなさい。

(heno239さんのソースコードがクソ分かりやすくて助かりました。ありがとうございました)

にぶたんパート

前提として、二分探索を行うには単調性があることが条件です。

今回は、\(X\)が上位\(M\)個未満に入れるかという判定を行います。上位\(M\)個未満というのは図のように単調性がある(ある位置で〇になったらその先も全部〇になる)といえます。

\begin{array}{|c|ccc|cc|} sum & 0 & \ldots & X-1 & X & \ldots & max(A)+max(A) \\ \hline / & × & × & × & 〇 & 〇 & 〇 \\ \end{array}


それでは遂に二分探索最大の山場、探索範囲の縮小のための判定です。

判定を行うためにはlower_boundを使う必要があります。

そのため事前に配列\(A\)をソートしておきましょう。

判定方法は以下のアルゴリズムで実装できます。

  1. まずa[i]を左手に持っているとする。
  2. a[i]を持っているときに和がX未満となる右手に持っている数字はX - a[i]未満の数字である。
  3. 2を lower_bound を使って判定、X - a[i]未満の個数を求める。
  4. 3で求めた数をNから引くことでX - a[i]以上の個数を判定できる。
  5. 1~4を繰り返し、すべてのX - a[i]以上の個数の和がM未満かを判定する。

これをC++で記述するとこうなります。

//判定
bool chk(long long x) {
    long long cnt = 0;
    for(int i=0; i<n; i++) {
        long long pos = lower_bound(a.begin(), a.end(), x - a[i]) - a.begin();
        cnt += (n - pos);
    }
    return cnt < m;//X以上の和がM個未満かどうか
}

int main() {
    cin >> n >> m;
    a.resize(n);
    for(int i=0; i<n; i++)cin >> a[i];
    sort(a.begin(), a.end());
    //二分探索
    long long ng = 0, ok = M;
    while (abs(ok - ng) > 1) {
        long long mid = (ok + ng) / 2;
        if (chk(mid)) ok = mid;
        else ng = mid;
    }
    //終了時にngにX-1,okにXが入っている。
    ...

和を求めるパート

続いて、答えとなる総和を求めるパートに入ります。
ここで、ある区間の総和を高速に求める必要があるので累積和を求めておきます。

累積和が分からない人はコチラ

ここからは以下のようなアルゴリズムで総和を求めていきます。
1. upper_boundを使い、左手にa[i]があると仮定した場合のX-a[i]以下の数の個数を判定
2.1で求めた数をNから引いてa[i]を左手に持った時のX以上の数を計算。
3.ans2で求めた数*a[i]+右手に持つ幸福度の総和を足す。
4.M2で求めた数を引く。
5.1~4繰り返し

ここで、`M`を見てみると、数が余っています。何故でしょう?
何故なら、今回の`X`とは、上位`M`個に確実に入る数となっているからです。
具体例としては、`{35,35,35,35,38,39}`、`M = 3`とします。
こうすると、`X`には`35`は入らず、`36`が答えとなります。
何故なら、`35`は複数存在するため、確実にすべての`35`は上位`M`個に入らなくなり、上位`M`個に確実に入ることができる数は`36`になります。
よって、最後に余った`M`にはすべて入るか不確定だった \(X-1\) 、つまり `ng` を余りの `M` 個足し合わせることで総和を求めることができます。

具体的なソースコードはコチラになります。

 ...
    vector<long long> wa(n + 1); //Aの累積和
    for(int i=0; i<n; i++) wa[i + 1] = wa[i] + a[i];
    for(int i=0; i<n; i++) {
        long long pos = upper_bound(a.begin(), a.end() , ng - a[i]) - a.begin();
        long long cnt = n - pos;
        ans += cnt * a[i] + (wa[n] - wa[pos]);
        m -= cnt;
    }
    ans += m * ng;
    cout << ans << endl;
}

総括

二分探索は結構得意な部類だと思っていたのですがコレとその前のAGCで完全に自信を無くしました。

判定パートが499点分あると死にます。助けてください...

ついでに同じような類題を1つ紹介しておきます(割と天才向けです)。

atcoder.jp

最後に最終的なソースコードと提出結果を貼って終わろうと思います。

ここまで見ていただきありがとうございました。

Submission #9301825 - AtCoder Beginner Contest 149

#include <bits/stdc++.h>
using namespace std;


long long n, m, ans;
vector<long long> a;

//判定
bool chk(long long x) {
    long long cnt = 0;
    for(int i=0; i<n; i++) {
        long long pos = lower_bound(a.begin(), a.end(), x - a[i]) - a.begin();
        cnt += (n - pos);
    }
    return cnt < m;//X以上の和がM個未満かどうか
}

int main() {
    cin >> n >> m;
    a.resize(n);
    for(int i=0; i<n; i++)cin >> a[i];
    sort(a.begin(), a.end());
    //二分探索
    long long ng = 0, ok = LLONG_MAX;
    while (abs(ok - ng) > 1) {
        long long mid = (ok + ng) / 2;
        if (chk(mid)) ok = mid;
        else ng = mid;
    }
    //終了時にngにX-1,okにXが入っている。


    vector<long long> wa(n + 1); //Aの累積和
    for(int i=0; i<n; i++) wa[i + 1] = wa[i] + a[i];
    for(int i=0; i<n; i++) {
        long long pos = upper_bound(a.begin(), a.end() , ng - a[i]) - a.begin();
        long long cnt = n - pos;
        ans += cnt * a[i] + (wa[n] - wa[pos]);
        m -= cnt;
    }
    ans += m * ng;
    cout << ans << endl;
}

Cafecoder Tea Break 003 感想

Tea Break 003に参加しました。

f:id:honehaniwa:20191225230255p:plain
全完21位

全完はしたものの考察がガバでWA_TLEして踏んだり蹴ったり。

A(Green) Hop Step Jump!

1,1,2,1,1,2...と移動してマスを踏むかの問題。4で割って3余るか調べればいいですね。

提出結果

n=int(input())
print("No"if n%4==3else"Yes")

B(Ceylon) Sound!

\(K\) から1づつ左or右に拡張していって指定した位置すべてに移動できるかの問題。

音階の最小値、最大値と \(K\) の差を足し合わせればOK。

最小値が \(K\) の右( \(K \lt min(A)\) )にあったり、最大値が \(K\) の左( \(K \gt max(A)\) )だったりしたときは0にするのを忘れずに!

提出結果

n,k=map(int,input().split())
l=sorted(list(map(int,input().split())))
cnt=max(0,l[-1]-k)+max(0,k-l[0])
print(cnt)

C(Dimbula) Good Triangle

3点が面積が整数になる3角形を作れるかの問題。面積が0は作れないことに注意して3重ループで頂点を全探索して公式に当てはめれば大丈夫です。

間違えないように変数を問題文と同じにするとバグが減りそうですね。

提出結果

n=int(input())
p=[]
for i in range(n):
  p.append(list(map(int,input().split())))
cnt=0
for i in range(n):
  for j in range(i+1,n):
    for k in range(j+1,n):
      a,b=p[i]
      c,d=p[j]
      e,f=p[k]
      if abs((c-a)*(f-b)-(d-b)*(e-a))%2==0 and abs((c-a)*(f-b)-(d-b)*(e-a))!=0:
        cnt+=1
print(cnt)

D(Darjeeling) Breaker

円形のテーブルを最も効率よく1周できる方法を求める問題。 スタート地点を全探索して実際に試して最小回数を出力します。

提出結果

n=int(input())
a=list(map(int,input().split()))
ans=10**25
for i in range(n):
  pos = i;cnt=0;flag=False
  while True:
    #print(pos)
    pos+=a[pos]
    cnt+=1
    if pos>=n:
      if flag:
        break
      pos-=n;flag=True
    if pos>=i and flag:
      break
  ans=min(ans,cnt)
print(ans)

E(Earlgray) Cafe Road

ここら辺から一筋縄ではいかなさそうな問題が出てきますね。

原点を通る直線を引いて一番 \(x_i,y_i\) が被るの時の個数はいくつという問題。

ありがたいことに原点を通ってくれるので各点 \(x\) , \(y\) を通る傾きを求めて同じ傾きが一番多い個数を出力します。

ここでx=0の時ゼロ除算が発生してREになるので注意です(自分が引っ掛かりました)

提出結果

import collections as C #重複要素を数えてくれるやつが欲しい
n=int(input())
pos=[list(map(int,input().split()))for i in range(n)]
l=[]
for a,b in pos:
  if a!=0: l.append(b/a)
  else: l.append(1e20)
c=C.Counter(l) #ここで傾きの個数を数えてdict型にしてくれる
print(int(max(c.values())))

F(Keemun) '0' or '1'

必ず左か下は現在地以下でなければいけないグリッドの総数を求める問題。

逆に言えば右or上に自分未満の数字が来ないことが分かる。

これは、実験したらわかるのですが、(0,0)から(n,m)へ上or右へしか行けないときの経路の総数と同じになります。(うまく言葉で説明できないので公式解説に期待...) DPっぽく数え上げてACです。

後、この解法ではPythonで通りません... 諦めてC++を使いました。

提出結果

一応Python

n=int(input())
m=int(input())
table=[[0]*(m+1)]*(n+1)
for i in range(n+1): table[i][0]=1
for i in range(m+1): table[0][i]=1
for i in range(1,n+1):
  for j in range(1,m+1):
    table[i][j]=(table[i][j-1]+table[i-1][j])%(1e9+7)
print(int(table[n][m]))

f:id:honehaniwa:20191225234321p:plain
実行時間厳しい

Pythonの高速化まわりでアドバイス募集してます...

後かふぇこでnumpyが使えないみたいな話もあったけどどうなんだろう...

※追記 どうやら数学でO(1)が存在するらしい?ので興味があったら是非

月刊競技プログラミングは役に立つ

本記事は豊田高専コンピュータ部 Advent Calendar 2019の12/17日の記事です

みなさん、競技プログラミングしてますか?私はしてます

今回は競技プログラミングを初めてよかったなぁと思ったことを紹介して沼に落とそうという記事です。

1.プログラミングの能力が向上する

言わずもがな。

私が競技プログラミングを始めたきっかけとして、授業でプログラミングをしたけどなんも覚えてないとなったことが始まりです。

プログラミングは何回も書いて身に着けるしか上達の方法はないと思ってます。

何か自分のホームページやアプリを作りたいがそこまで自信がない...

と、そんなあなたに競技プログラミング!!!

簡単な問題で変数の入力から条件分岐、forループなどの処理まで実際の問題を解決しながら楽しく学べちゃうんです!!!!

2.授業のプログラミング課題が楽になる

競技プログラミングをずっとやってる人間とやってない人間でプログラミングの練度が大きく変わってきます。

課題でのみプログラミングをやる人が時間をかけてやる課題なんぞ競プロer(競技プログラミングをする人の意)にとっては簡単です。

クラスメートがヒイヒイ言ってる中で圧倒的差をつけよう! f:id:honehaniwa:20191217001749p:plain

3.頭がよくなった気になる

毎週末にコンテストで100分間も問題を考え続けるなんて普通の学生にとってはありえない(少なくとも土日に自主的に勉強しないやろという意)ですが、競プロerは毎週自分のレートを1でも大きくするために自分の脳をフル回転して問題に取り組みます。 ちょっと位頭がよくなった気がしませんか?私はしてます。

全体を通して

競技プログラミングは正直好みが分かれると思ってます。でも、たとえ苦手でもこういった論理的思考を鍛えることは今後においても損ではないはずです。 自分の体験としては低学年時代に万年留年候補トップ争いをしてた自分が気づいたら大学編入を成功させてたり学内で一番レーティングが高くなったり1年で深層学習で機械翻訳してみたりと結構救われてたりします。あ、競技プログラミングをしてると機械学習の勉強をする際に役立ちます。 それではみなさんも競技プログラミングを是非やってみてください!

競技プログラミングサイトを紹介しておきます

AtCoder -始めるならまずここ。週末にレート変動のあるコンテストを開催 AOJ -授業チックな問題から競プロ問題まで AtCoder Problems -AtCoderの過去問埋め支援サイト

AtCoder緑になりました

f:id:honehaniwa:20191124224456p:plain

DFSするだけを通せずに詰みました。
精進します。

AtCoder水色になりました

変色しました。90回コンテストに参加しました。あほくさ
茶->緑でもキツかったのに緑->水はもっとキツかったです...

水色になるまでに何をしたか

い つ も の
まあ一般的な変色記事でよくあるセットをやった感じで、
・ 蟻本(3-1くらいまでなら大体理解してる)
・ 精進
f:id:honehaniwa:20191123234933p:plain

f:id:honehaniwa:20191123234845p:plain
精進
ABC-A~C、ARC-A、AGC-Aを全部埋めた。
最近だとAtCoder Problems のRecommendationsが偉いのでEasyとModerateを貪欲に解きました。
・後輩に教える
自分が所属してるT高専コンピューター部は自分が最高学年&&一番レートが高い(は?)なので、 JOIとかパソコン甲子園前の講座は大体自分が担当してました。
教えてみるとあやふやな知識が固まって頭に定着する感じでよいので結構おススメです。
・バチャ
高専生は高専とかいう異常空間にいるので他校の高専生も含めコミュニティがあります。
高専生&(緑上位 or 水->緑落ち)勢で入水したいバチャしたりしてました。
Twitterは偉大

自分語り

ここから有益な情報はほぼ無いです。自分語りです。

競技プログラミングに出会う前

・虚無を過ごしてた高専3年生
・成績下位10%(毎年留年候補)
・大会ガチ勢の運動部に何故か所属してる陰キャオタク
・プログラミングなんもわからんし一応授業で習ったCがなんとか...
・部活を辞めて昆布に入り、高専プロコンの自由部門に出ようとするが校内予選で落ちる
・ギリギリパソコン甲子園に出れる年齢だったので参加するついでに勉強するか~で始めた
Ratedコン初submitから初めてAtCoderに参加しました。

出会ってから

・専門の授業が楽になる
アルゴリズムC言語での演習授業が楽しくなる->真面目に授業を受けだす
・勉強のモチベが上がる
競技プログラミングをすると頭がよくなった気分になる
・大学編入に進路変更
虚無->アルゴリズム楽しい!もっと色々勉強したい!で進学に方向転換。
coinsとか地元のつよつよ旧帝大とかは無理だったけど技科大に滑り込みセーフ

競技プログラミングで人生変わりました!(マジ)

競技プログラミング、しようね!

言い訳コーナー

ここから何で90回も参加して水色にしかなれてないのかの言い訳コーナーです。
・日曜21時のABCは遅刻確定
3年の時は学生寮にいたので休日点呼が21時丁度なんですよね...
なので確定5~10分遅刻で参加してました。
・精進不足
成績が悪い->勉強のモチベが皆無なのでプログラミングが勉強と思ってるうちは少しマシな勉強と思って嫌々やってました。
途中から楽しくなって精進し続けてるけど変色しなさ過ぎて辛くなってた
・平成ABC->令和ABC(ABC+ARC問題セット) 前までは4完で1600頭打ちだったので3完(+たまに4完)くらいで伸びたんですけど、令和ABCから急に伸びにくくなりました。
(AtCoder自体の知名度が上がったとかもありそう)
令和ABCから冷えることが多くてつらかったです...
・冷えるコンテストに体当たり->爆散
温まる->このままAGC(or ARC級Ratedコン)やるぞやるぞ->う し た ぷ に き あ く ん 笑なことを何回もやってました。

f:id:honehaniwa:20191124004302p:plain
こんな感じ
・一回当たりのコンテストで増えるレートが小さすぎる ここら辺は精進すれば伸びると信じるしかないです...
青まではまだまだ遠い...

最後に

数打てば数弱でも水色になれます!
今冷えてて苦しい人もこんだけ苦労してやっと水になった人もいるので一緒に頑張りましょう!という感じです。
(茶色時代にこれだけ競技プログラミングやっててレートこれだけならセンスないし辞めたら?的なことを言われたこともあります)
明日のABCで冷えて緑変色記事を書かないことだけを祈ります...