AHC025参加記【最終90位】

1.はじめに

 初めて記事を書きながら参加します
 だらだらと書くかと思いますので、適宜読み飛ばしてください

2.問題

https://atcoder.jp/contests/ahc025/tasks/ahc025_a

 天秤を使って、できるだけ等しい重さでアイテムを分割する 

 

 入力:アイテム数N, 分割数D, クエリ回数Q
 質問:左の天秤に乗せる数, 右の天秤に乗せる数, 乗せるアイテムのインデックス
 出力:各アイテムをどの分割に属させるかを1列で

3.1日目(10/14土)

 インタラクティブな問題で難しそう...
 分割と言うから、分割位置を指定するかと最初思ったが、それぞれのアイテムを任意の集合に分けられるようで自由度は高そう

 ランダム要素は無いが、分割数とクエリ回数によっては完璧に判断できるパターンもありそうなので、複数の解法を採用するのが最終的には良さそうですね

 天秤問題は世の中にありそうなので検索で知見を集めつつ、貪欲解をまずは考えることから始めます

 

 ビジュアライザを触ってから気付きましたが、クエリの回数は使い切らないとエラーが返されるようで、そこのケアをすることは忘れないようにします

 アイテムの重さをW0~Wn-1とおいて、クエリで得た不等号式に合うように重さを解くことを考えると、ソルバーが使えるのかな?
 その考えなら、効率のいい不等号式を考える問題になりそうです

 

 アイテムを1つずつ比較して、重さの順番を把握して重さ上位をグループに分けて、残り細かいものを一番軽い所に詰めていくのはどうか?

 →Qの最小値が2Nなので、一つずつソートしていると間に合わないパターンがありそう
  マージソートをベースに、軽い方だけはある程度ソートが効くようなソートを考えてみた(重い方はあまり正しくないソート状態になる)

  重い側を先に埋めていくので、重い側がちゃんとわかるようなソートにしました

seed=2

 ↑初めに、重いものをグループ分けし軽いグループに割り振っていく

 seed=2は、見た目結構いい感じになりましたが、最後にクエリ数を残しすぎて軽いもののソートが甘いためスコアが伸びていないように感じます

seed=2

 この時点(18:40)で、70位/200人なのであまり良い解法とは言えなさそうです
 →Qが十分足りていて、ソートがやり切れる場合は強そう

 上位を想像すると、最後のクエリまで試して手持ちの情報の精度を上げているような感じでしょうか?

 複数の変数を同時に評価する方法としては、実験計画法も近い考えな気がしてきました(考え方は知っているが使ったことはない)
 1週間あるので、この辺りも勉強しながら取り入れていきたいですね

 

※寝る前に追記

 アイテムの重さが指数分布から生成されている記載が気になってグラフ化
 1~(10^5*N/D)の範囲らしい
 重いアイテムから振り分けていく考え自体は大事そう

seed=2, N=94

4.2~3日目(~10/16月)

 色々試して、44位(10/16 20時時点)に上がりました!
 今のところの解法は、ケースによって2パターンあります

1.アイテムをソートして、降順に軽いグループへ合流させていく
2.適当にグループ分けした後、グループ内でソートして2分探索でバランスを取る

 まず、ソートはマージソート O(nlogn) を採用しています
 1と2の切り分けは、ソートの計算量を見積もってその後の操作が間に合うかどうかで分けました(下のコード

  self.state = 0
  mergeCnt = math.log(self.N, 2) * self.N
  if (self.Q-mergeCnt) < (self.N-self.D)*(self.D/2):
    self.state = 1
    mergeCnt1 = math.log(self.D, 2) * self.D
    mergeCnt2 = math.log(self.N/self.D, 2) * self.N/self.D
    if (self.Q-mergeCnt1-mergeCnt2*self.D)/self.D*2 < self.D*2:
      self.state = 2  # お急ぎモード

 

 1の方は何となくイメージ付くかと思うので、2のグループ分け後にバランスを取る解法をgifにしました

 バランスのとり方としては、
・一番重いグループから一番軽いグループへ、大小関係を逆転させない最大のアイテムを渡す(2分探索
・渡せなかったら、一番重いグループからランダムなグループへランダムなアイテムを渡す
 としていました(焼きなましをイメージ

 

 比較のクエリ数削減のため一度聞いた比較を保存しておいて、同じ比較をしたい時はオフラインで返答できるようにしています
 さらに、「a>b」を知っている状態で「b>c」が判明した際に、「a>c」という回答をオフラインに保存するよう、再帰的な処理も作りました(さらっと書いたけど地味に大変でした

 イメージ的には「b>c」判明の際に、bより重い各要素にcの方が軽いという情報を持たせて、cより軽い各要素にbの方が重いという情報を持たせるようにしています

  # 不等号関係を伝播する
  def sign_propagate(self, host, key, sign):
    rev = {">": "<", "<": ">", "=": "="}
    if sign in "><":
      rsign = rev[sign]
      for k in self.signGraph[host][rsign]:
        if k not in self.signGraph[key][rsign]:
          self.signGraph[k][sign].add(key)
          self.signGraph[key][rsign].add(k)
          output = "{} {} {} {}".format(len(k.split()), len(key.split()), k, key)
          output2 = "{} {} {} {}".format(len(key.split()), len(k.split()), key, k)
          self.signDict[output] = sign
          self.signDict[output2] = rsign
          self.sign_propagate(k, key, sign)
          self.sign_propagate(key, k, rsign)

  # 取得した不等号関係を更新
  def sign_update(self, output, res):
    rev = {">": "<", "<": ">", "=": "="}

    x = list(map(int, output.split()))
    nL, nR = x[0], x[1]
    L, R = x[2: 2+nL], x[-nR:]
    strL, strR = " ".join(map(str, L)), " ".join(map(str, R)), 
    output2 = "{} {} {} {}".format(nR, nL, strR, strL)
    self.signDict[output] = res
    self.signDict[output2] = rev[res]

    # if nL<=2 or nR<=2:
    if self.Q<1500 or nL<=2 or nR<=2:
    # if True:
      self.sign_propagate(strL, strR, res)
      self.sign_propagate(strR, strL, rev[res])
      self.signGraph[strL][res].add(strR)
      self.signGraph[strR][rev[res]].add(strL)

 次に試したいことの案
・パターン1の時、大量に余ったクエリの有効活用法
・パターン2の時、更新に向かいやすい近傍の見つけ方
再帰比較メモを活用したマージソートの改良
・重量データから理想の解答を見て考察

 

5.4日目(10/17火)

・重量データから理想の解答を見て考察 について

 seed=0のような、グループが2つの場合は重量データがわかっていれば厳密な解を求めることができます

 以下の問題が類似していて、動的計画法で差の少ない数を求めた後、経路復元していけばグループ分けをすることができます

atcoder.jp

seed=0の理想解?

 重さが分かれば、グループが多くともかなり正確な解が出せそうです
 比較した情報から、各アイテムの重さを推定したい気持ちになり、ベイズ推定などを調べてみましたが、何も分かりませんでした;;
 手動で良い解が出せたことに満足して次の検討に移ります


・パターン1の時、大量に余ったクエリの有効活用法
・パターン2の時、更新に向かいやすい近傍の見つけ方 について

 パターン2では、重いグループから軽いグループへアイテムを渡せない時、ランダムに渡していましたが、ループ回数によって悪化したりするため別の方法を考えました
 一方的に渡すのではなく、交換することにしました

 重いグループのアイテム > 軽いグループのアイテム となるように選び、
 尚且つ交換後もグループの大小関係が変わらないように交換しました

 また、その遷移が出来なくなった後は、重いグループ側のアイテムを2つ選び同様の条件で交換判定をクエリ一杯まで行っています

 この近傍は、クエリが大量に余るパターン1の場合特に有効に作用して、かなり得点が向上しました(瞬間29位!

 

6.5日目~(10/18~)

 色々やった気がしますが、コーディングに夢中で記事に落とせていません
 代わりに最終解法をちゃんとまとめます

7.最終解法

 制約によって、3パターンの操作をしています
 いずれの場合も、グループは降順ソートを維持しています
 ↓実際のコードの主要部分です(関数名が終わっていますが気にせず...)

    while self.remCnt>0 and time()<self.eTime:
      if self.state==2:  # かなりクエリに余裕がない場合
        if self.replace(0, 1, 1): continue
        if self.replace(0, 2, 1): continue
        if self.swap(0, 1): continue
      if self.state==1:
        if self.replace(0, 1, 1): continue
        if self.replace(1, 1, 1): continue
        if self.swap(0, 1): continue
      if self.state==0:  # クエリに余裕がある場合
        if self.replace_0(0, 1): continue
        if self.replace_0(1, 1): continue
        for _ in range(self.D):
          i, j = randrange(self.D), randrange(1, self.D)
          ret=self.swap_bs(randrange(i+1), randrange(1, j+1))
        if ret: continue
        if self.swap_0(0, 1): continue
        if self.swap_0(1, 1): continue

 replace関数
 replace関数では、重いグループから軽いグループへランダムに選んだアイテムを渡そうとする処理をしています(AからBへ渡すと仮定します)
 渡した後のBの重さが、Aより軽いorA以外で最も重いグループより軽い時、渡せると判断していました

 swap関数
 swap関数では、グループAからアイテムを2つ、グループBからアイテムを1or2つランダムに選び(a, bとします)交換しようとします
 a>b かつ 交換後のBの重さが、Aより軽いorA以外で最も重いグループより軽い時、交換できると判断していました

 replace_0関数
 replace_0関数では、グループA内のアイテムをソートし、Bへ渡せる最大のアイテムを2分探索で探して渡そうとします

 swap_0関数
 swap_0関数では、グループB内のアイテムをソートし1or2つのアイテムを降順に探索してグループAのアイテム1つと交換しようとします

 swap_bs関数
 swap_bs関数は、グループBからランダムに1つグループAへ渡しておき、AからBへ渡せる最大のアイテムを2分探索で探して渡そうとします

8.提出コード

 pypy3

atcoder.jp

9.終わりに

 今回のコンテストは、よい方針が思いつかず「上位は推定とかすごいことをしてるんだろうなぁ」と半ば諦めながら取り組んでいました
 蓋を開けてみると、すごい人はすごい(!)のは変わりませんが、同じような方針で更に上位に入っている方も多く見えました
 自分の知識・方針でやり切ることが出来なかったと思うと非常に悔しいです

 次のヒューリスティックでまた会いましょう!

 



 

AHC024参加記【最終27位】

1.はじめに

 制限時間4時間の短期コンテストは決勝コンを除けば、AHC021(6月頃)以来なので久々ですね
 今回は開始前に意識しようと考えていたことを事前に書いておきます

 

・問題文をよく読む 制約をよく読む
 →おそらく一番大事です。特に厳しい側に制約を勘違いしていると、全然スコアが伸びずに終わってからガッカリすることになります(AHC021でやらかしました)

 

・強い貪欲を考える(順位表も参考に)
 →多くの人が同じことをすると思いますが、だからこそ強い貪欲解法に気付けていないといくら工夫しても伸びないように思います

 

・考えに詰まったら紙に書いて整理or少し歩く
 →気分を変えたりすると急にアイディアが出てきたりするので、短期とはいえ1時間に1度くらいはリフレッシュをするようにします

2.問題

atcoder.jp

 問題文を要約すると、

・色分けされている地図の隣接関係を保持したまま小さく圧縮せよ

 

 実装中に気を付けないといけないポイントとしては、以下の2つ

・地図の中に空白(白マス)があってはいけない

・同じ色が離れ小島になってはいけない

ストーリーの画像

3.解法

 思いついて実装できた順に書いていきます

・周囲を白マスで埋める

 これだけではスコアはほぼ出ませんが、これを実現するためには、
 正しい提出をするためのチェック関数が必要で、それをこの後も使いまわせたのは良かったなと思います

 

 正しい提出をするためには、以下の2つが必要です

 ・各色が連結であること

 ・各色の隣接関係が保たれていること

 

 「各色が連結であること」は、bfsで探索して別のタイミングでもその色が出てきたら連結でない。とみなしていました

 「各色の隣接関係が保たれていること」は、隣接色のリスト(set())で持っておいて、変更前後で一致するかチェックしました

 最初壁に接している箇所は、空白マスとも接している必要があります

 

 上記2つのチェックを、O(N^2)で判定しているので非常に遅いです
 より高速な判定を他の方はしているはずなので勉強します

・縦、横に切断して詰める

 開始1時間くらいで思いついてこれはいけそうだなと思いました
 いつかのAHCで、ケーキを縦横に切る問題があってそれをイメージしました

垂直方向の切断イメージ

seed=0, score=1,778, total=275,784

 この時点で本番130位相当のスコアが出ました


・中途半端な長さで切断してみる

 前述の方法では、1列or1行がすべて消せないと実施できないため、切断長さにランダムさを加えてみたところ、少しスコアが向上しました

 

・飛び出ている所を均す

 切断していく解法の都合上、1マスだけ飛び出ている箇所が邪魔に見えたのでその箇所を周りの色に変える処理を入れました

 処理としては、自身の周囲4マスを見て丁度3箇所が自身と違う色なら、周囲で一番多い色に変えています

 最初に強めに(n=5回)均して、後は250回に1回程度の頻度で均していました

seed=0に均しフィルター適用後
・最後に、消して良い箇所を空白にする

 最初に書いたことと同じ意味になりますが、切断フェーズ終了後に空白マスと隣接しているマスを消してみて、問題ないか確認する処理をしました

 ここの処理で何秒かかるか不確定なため、切断フェーズを少し早めに切り上げています(開始から1.7秒で中断)

・1マスだけの色を隣に増やす

 ビジュアライザを見た時に、1マスだけの色を含む行列に関しては確実に切れないことに気付き、増やせる方向があれば増やす処理を入れました

 次の案がこれの代替になると途中で気付いて、この処理は最終的には使っていません

 

・ランダムに1色変更する

 色のついているマスをランダムに選んで、その周囲4マスで変更できるマスがあれば自身と同じ色に変更します

 切断のネックになっている箇所は、1マス色の箇所だけとは限らないためこの工夫は割と効きました

 この処理は、切断処理を試して切断できなかった時に実行しています

 

4.最終提出のビジュアライズ

 

5.提出コード

 PyPy3で書いています

 気付き、ダメ出しなどあれば言ってもらえると喜びます

atcoder.jp

6.終わりに

 4時間の短期コンテストは、思いつかなかったら爆死のイメージがありましたが、今回は思いつけた側になれたようで、27位の黄パフォを初めて獲ることが出来ました!

 10/14~長期コンがまたあるので、そちらも頑張りたいです

 以上、読んでいただけた方ありがとうございました

 

 

AHC023参加記【最終135位】

1.はじめに

 AHC022に続いての長期コンテストということもあり、前回の学びを書きます

・思いついた解法は必ずメモして、試す

・変数のサイズによって解法の不得手が無いか考えてみる

・根拠の無いパラメータ調整に時間を割かない

 

 平日は仕事もあり、中々時間を作れないため日中にアイデアを溜めて、夜に色々試すしかない。というのもありました

2.問題

 H×Wの土地(20*20)に、Tカ月(100カ月)でK種類の作物を育てる問題でした

 土地には水路があり通れないことや、栽培中の作物の上も通ることができない。という制約がありました

 また、植える月と収穫する月が作物毎に決まっており、植える月だけは先出ししても良いとのことでした

atcoder.jp

3.初日の取り組み

・常に空けておく通路を作って、アクセスできる区画に作物を詰め込む

・同じ経路でも月によって通れる通れないがあるため、H*W*Tの3次元で考える

 そうして出来た、初提出のビジュアライズがこちら

seed=0, Score=279,950

 やりたい事は一旦できていることが確認できました

 

 次に考えた方針としては

・入口から遠いほど作物を置きにくい→奥から決めていく

・最後の月に必ずまとめて収穫できることから、収穫日が遅く、植える日が早い作物から決めていく

・植える場所を決めたら、入口までの最短経路に[植える月]と[収穫する月]をメモして、後から植える作物がそれを邪魔しないようにする

 上記の方針を盛り込んだ提出のビジュアライズがこちら  

 

seed=0, Score=620,725

 一気にそれらしくなりましたね

 ちなみに、この提出はコンテスト開始約4時間のタイミングで、なんと順位表で5位になり、かなりテンションがあがりました

 

 ちなみに、通れる通れないのメモの仕方として、H*Wの1次元配列にビット列でメモをしていました。どういうことかと言うと、

 植える日=1月目、収穫する日=3月目の作物を植える際は、

植えた区画には  0b0010 と記録

通路の区画には、 0b0101 と記録しておくことでビット列で月日を表現できます

 Pythonには整数の上限が無いので、2^99も問題ないようでした

 (あまりに大きいと速度が落ちるなどあるのでしょうか?

4.2日目以降

 正直、最初の方向性以上に得点ができる考えが見つからず、ただ改良を重ねる日々が続きました

 幸い、アイデアだけは浮かんでいたので色々試した過程を紹介しようと思います

 

・通路に使う区画の優先度を決める

 先のビジュアライズで2マス幅の通路があるのが気になったため、通路があるにしろ1マスにしたいなという思いから、最短経路のうち優先度の高い区画を選ぶようにしました

 優先度の付け方は、これまでに通路として使われた回数。を使いました

 後に植える作物は、以前に植えた作物と同じ道を通りやすくなる。という算段です

seed=0, Score=721,925, Total=36,133,425

 明らかに通路の幅が狭まり、スコアも一気に上がりました

 この時、同時に採用したアイデアとして植える月に通る経路と収穫の月に通る経路を別々に探索する。というものでした(こっちの方が寄与度大きいかも

 ただ、区画に植えてある作物の変数と、区画毎の植える月を把握する変数と、区画毎の収穫する月を把握する変数と、などなど管理する変数が一気に増えたせいでバグさせまくりながらなんとか実装しきりました...

 

・先に通路に使う優先度を計算しておく

 これは、作物を植え始めてから優先度を決めるのではなく、先にある程度優先度を決めておいて、それに加えて作物を決定しながら優先度を上げていくようにしました

seed=3の時の優先度 左:事前計算 右:探索後

 上は、エクセルで可視化したもので、なんとなく壁から離れたところを通らせようとしているのは伝わるかなと思います

 各区画について、上下左右の壁との距離*2+2の計算をしているだけです(+2はお気持ち程度

 

 また、植える区画を検討する順番についても、入口からの最短距離+周囲の壁の数 を採用して、壁周辺から作物を決めていくようにしました

seed=0, Score=747,900, Total=38,066,625

 この時点で、3日目(9/5(火) でしたが、このまま進んでも40Mまで届かないな?とは思っていました

 別解法として、

・植える毎に全時間の最短経路を計算し直して、回り道も含めた経路を見つける

・作物毎に区画を探索するのではなく、区画毎に作物を詰めていく

 など考えて実装までしましたが、あまり得点は出ませんでした

 

 それでも、探索順にランダム性を持たせて、最も高いスコアを採用する(乱択貪欲?)や、少しでもループ数を稼ぐために処理を軽くしたりして、最終的に39Mのスコアまで何とか出すことが出来ました

seed=0, Score=763,575, Toral=39,074,125

5.終わりに

 システス後の順位は、135位と自分にしては頑張ったほうかなと思います

 平日もAM2時くらいまで粘って寝坊しかけたりしましたが、それだけこの1週間のめりこむことが出来ていたのかなと

 

 また今回の結果で、水色に入ることが出来たので結果としては大満足です!

 終わってから、一気にみんなの解法がTLに流れてくるのも面白かったですね

 最上位の方は焼きなましに持って行けていたり、通路で区切ってdpで最適化など、この1週間で全く考えもつかなかった内容が多くありました

 次のAHCまでに、概念だけでも理解したいですね

 同じく取り組んだ方々、お疲れさまでした!

はじめまして

軽い自己紹介

 社会人1x年目の uta(うた) です

 2020年11月頃に少しだけAtcoderに触れて、2023年6月から本格的に競プロに取り組み始めました

 アルゴ水を目標に、コンテストに出た過程などをアウトプット出来たらなと思っています

 

レーティング

 2023/9/11現在のレーティングは以下になります

 (横軸飛んでて見にくい...

Algorithm

Heuristic

 最近、ヒューリスティックの長期コンで青パフォ取れたおかげで順調に入水できてしまいました

 アルゴも入水できるように頑張りたいですね