2026年10月4日日曜日

ABC212C - Min Differenceを二分探索で解いてみた

40代文系社内SEが、AtCoder Beginner Contest 212 C - MinDifferenceを解いてみました。

まず問題文を見ます。

$A$と$B$の2つの数列から数値を選んだときの差の最小値を求める問題です。

同じ数値が両方に含まれていると、答えは$0$になります。

全探索すれば答えは出ますが、C問題ですし全探索では間に合わないでしょう。

次に制約を見ます。

$1 \le N, M \le 2 \times 10^5$なので、予想通り全探索$O(NM)$では間に合いません。

$O(N \log M)$なら間に合うので、二分探索が使えないか考えます。

まず、数列の並び順については何も記載されていないのでソートが必要になりますが、ソートの計算量は$O(N \log N)$なので、入力から読み込んだあとで1回だけソートすれば問題ありません。

次に、差の最小値については、$A$から選んだ値($A_i$)以上という条件で$B$を二分探索します。一致する数値があれば$0$が答えになり、そうでなければ$A_i$より小さい数値の最大値か、$A_i$より大きい数値の最小値のどちらかとの差が答えになります。

以上より、以下の手順で解きます。

  1. $B$をソートする
  2. $A$から数値を順番に選択し、$A_i$とする
  3. $B$から$A_i$以上の値を二分探索で検索する
  4. $A_i$未満の最大値と$A_i$との差の絶対値と、$A_i$以上の最小値と$A_i$との差の絶対値のより小さい方を記録する
  5. 4で記録した数値の最小値を答えとする
Common Lispによる実装例

(let* ((n (read))
       (m (read))
       (a (make-array n :initial-contents (loop repeat n collect (read))))
       (b (make-array m :initial-contents (loop repeat m collect (read)))))

  (sort b #'<)

  (labels ((bsearch (ai &optional (l -1) (r m))
             (if (= (1+ l) r)
                 l
                 (let ((mid (+ l (floor (- r l) 2))))
                   (if (< (aref b mid) ai)
                       (bsearch ai mid r)
                       (bsearch ai l mid))))))

    (loop for ai across a
          for l = (bsearch ai)
          minimize (cond ((= l -1)
                          (abs (- (aref b 0) ai)))
                         ((= l (1- m))
                          (abs (- (aref b l) ai)))
                         (t
                          (min (abs (- (aref b l) ai))
                               (abs (- (aref b (1+ l)) ai)))))
            into it
          finally
             (format t "~A~%" it))))

二分探索の結果が$B$の両端になるときだけ少し注意が必要です。

AtCoder Beginner Contestを解いてみたリスト

2026年10月2日金曜日

ABC175C - Walking Takahashiを解いてみた

40代文系社内SEが、AtCoder Beginner Contest 175 C - Walking Takahashiを解いてみました。

まず問題文をみます。

座標$X$にいる高橋君をできるだけ原点に近づけるように移動させると、どこまで近づけることができるかという問題です。

移動には距離($D$)と回数($K$)の制限があります。

次に制約をみます。

$1 \le K \le 10^{15}$より、愚直に移動を繰り返すとTLEになります。

まず、問題を単純にするためにXが負の値のときは$-1$を掛けて正の値にします。

次に高橋君を原点を超えない範囲で負の方向に移動し、その座標を$G$とします。このとき、$G$を引き算で求めようとすると$O(K)$で間に合わないので割り算を使います。

移動回数: $X \div D$

$G$: $X - (D \times 移動回数)$

この時点で移動回数を使い切ったとき、$G$が答えになります。

まだ移動回数が残っているとき、$G$とさらに1回だけ負の方向に移動した座標を往復することになります。すなわち、残りの移動回数が偶数であれば$G$が答えになり、奇数であれば$G$引く$D$の絶対値が答えになります。

(let* ((x (read))
       (k (read))
       (d (read)))
  ;; 単純化するためにxを正の値にそろえる
  (when (< x 0)
    (setf x (* x -1)))

  ;; できるだけ原点に近づく
  (let* ((move-count (min k (floor x d)))
         (g          (- x (* d move-count))))
    (cond ((zerop (- k move-count))
           ;; これ以上移動できないとき
           (format t "~A~%" g))
          ((oddp (- k move-count))
           ;; 残りの移動回数が奇数のとき
           (format t "~A~%" (abs (- g d))))
          (t
           ;; 残りの移動回数が偶数のとき
           (format t "~A~%" g)))))

2026年9月27日日曜日

ABC154D - Dice in Lineを解いてみた

40代文系社内SEがAtCoder Beginner Contest 154 D - Dice in Lineを解いてみました。

まず問題文を見ます。

期待値を求め、合計を計算し、さらに最大値を求めると。$N$個から$K$個の合計を計算することになるので、愚直に計算すると$O(NK)$となるのでおそらくTLEになるでしょう。

次に制約を見ます。

$1 \leq N \leq K \leq 200000$ということで、やはり期待値の合計を計算するところは工夫が必要です。正$1000$面体のサイコロはほぼ球になる気がします。出目がわかりにくそう。

さて、解き方についてですが、まず期待値についてはサイコロの目をすべて合計して面数で割ります。

$1$から$K$までの合計を計算するには、$(1 + K) \times K \div 2$を使えば$O(1)$で求められます。例えば、一般的な正六面体の場合は、$(1 + 6) \times 6 \div 2 = 21$となります。

次に$N$個から$K$個の合計を計算するところですが、「連続する$K$個」というところがポイントです。まず先頭$K$個の合計を求め、合計1とします。合計2を求めるには、合計1に$K+1$個目の値を足し、1個目の値を引くことで$O(1)$で求められます。

例えば、入力例1では以下のようになります。

合計1: $1.0 + 1.5 + 1.5 = 4.0$
合計2: $4.0 + 2.5 - 1.0 = 5.5$
合計3: $5.5 + 3.0 - 1.5 = 7.0$

以上より、以下の手順で解きます。

  1. 各サイコロの期待値を計算する
  2. $K$個の期待値の合計を計算する
  3. 期待値の合計の最大値をとる
(defun solve ()
  (let* ((n (read))
         (k (read))
         (p (make-array n :initial-contents (loop repeat n collect (read))))
         (q (make-array n)))
    (loop for i from 0 below n
          for p-i = (+ (aref p i) 0d0)
          do (setf (aref q i) (if (= p-i 1)
                                  1
                                  (/ (* (1+ p-i) p-i) 2 p-i))))
    (loop with a = 0
          for i from 0 below n
          for qi = (aref q i)
          maximize (progn
                     (incf a qi)
                     (when (>= (- i k) 0)
                       (decf a (aref q (- i k))))
                     a)
            into it
          finally
             (format t "~,10F~%" it))))

(solve)

0d0は、倍精度浮動小数点数で計算するために使用しています。

2026年9月25日金曜日

ABC141D - Powerful Discount Ticketsを優先度付きキューで解いてみた

40代文系社内SEがAtCoder Beginner Contest 141 D - Powerful Discount Ticketsを解いてみました。

まず問題文をみます。

半額 小数点以下切り捨て になる割引券という夢のような設定です。それって最終的には0円もありえる?と思ったら入力例3にありました。

割引前の金額に対して割合で値引きをするので、割引前の金額が高いものからチケットを使用していきたいところです。問題には、「順番に購入する」とありますが、購入前のシミュレーションと考えれば購入順は気にしなくてよさそうです。

そのような要件に応えるようなアルゴリズムとしては、優先度付きキューがあります。優先度つきキューなら最大値の取り出しと値の追加が$O(\log N)$で完了します。

次に制約をみます。

品物の個数$N$と割引券の枚数$M$がともに最大$10^5$です。

割引券ごとに優先度つきキューから商品を取り出して割引を適用してキューに戻すという操作を繰り返しても、$O(M \log N)$で完了します。

以上から、以下の手順で実装します。

  1. 最大値を返す優先度付きキューを用意する
  2. 優先度付きキューに商品を追加する
  3. 割引券ごとに、優先度付きキューから商品を取り出して割引を適用してキューに戻す操作を繰り返す
  4. 優先度付きキューの値の合計を出力する

(defun solve ()
  (let* ((n (read))
         (m (read))
         (heapq (make-array 0 :fill-pointer 0 :adjustable t)))
    (loop repeat n
          for ai = (read)
          do (heap-push heapq ai))
    (loop repeat m
          for ai = (heap-pop heapq)
          do (heap-push heapq (floor ai 2)))
    (format t "~A~%" (reduce #'+ heapq))))


(defun shift-down (q idx)
  (let ((idx-l (+ (* idx 2) 1))
        (idx-r (+ (* idx 2) 2))
        (idx-next idx))
    (when (and (< idx-r (length q))
               (> (aref q idx-r) (aref q idx-next)))
      (setf idx-next idx-r))
    (when (and (< idx-l (length q))
               (> (aref q idx-l) (aref q idx-next)))
      (setf idx-next idx-l))
    (when (/= idx-next idx)
      (rotatef (aref q idx) (aref q idx-next))
      (shift-down q idx-next))))

(defun heap-push (q item)
  (vector-push-extend item q)
  (loop with idx = (1- (length q))
        for parent-idx = (floor (1- idx) 2)
        while (and (> idx 0)
                   (> (aref q idx) (aref q parent-idx)))
        do (rotatef (aref q idx) (aref q parent-idx))
           (setf idx (floor (1- idx) 2))))

(defun heap-pop (q)
  (cond ((zerop (length q))
         nil)
        ((= (length q) 1)
         (vector-pop q))
        (t
         (let ((ans (aref q 0)))
           (setf (aref q 0) (vector-pop q))
           (shift-down q 0)
           ans))))

(defun heap-top (q)
  (if (zerop (length q))
      nil
      (aref q 0)))

(solve)

Common Lispには優先度付きキューがないので、自前で実装しました。

AtCoder Beginner Contestを解いてみたリスト

2026年9月23日水曜日

ABC138D - Kiを解いてみた

40代文系社内SEが、AtCoder Beginner Contest 138 D - Kiを解いてみました。

まず問題文を確認します。

根が頂点1の木があり、それぞれの頂点はカウンターをもっています。部分木のカウンターに値を加算する操作を繰り返し、最終的なカウンターの値を求めます。

まず、「木」と記載されていることからグラフの問題であることが考えられます。また、部分木に値を加算するとありますが、おそらくそのまま実装するとTLEになるだろうと思われます。

次に制約を確認します。

頂点の個数$N$が最大$2 \times 10 ^ 5$であり、操作回数$Q$も最大$2 \times 10 ^ 5$であることから、毎回頂点1に操作をするケースで計算量が$10 ^ {10}$を超えて間に合いません。

どのように計算量を抑えるかと考えたとき、根から部分木に値を加算していくという操作が累積和のようにみえました。すべての頂点が一直線につながっているようなケースを考えると、イメージしやすいかと思います。

累積和が使えれば、値を加算する操作は最後に1回だけ処理すればいいので、計算量を$2 \times 10 ^ 5$に抑えられそうです。

以上より、以下の手順で実装します。

  1. a、bを隣接リストとして読み込む
  2. カウンターを用意して、カウンターpにxを加算する
  3. 頂点1からグラフを探索し、累積和の要領で値を加算する

(defun solve ()
  (let* ((n (read))
         (q (read))
         (graph (make-array (1+ n) :initial-element nil))
         (counter (make-array (1+ n) :initial-element 0)))

    ;; a, bを読み込み、隣接リストとして保持
    (loop repeat (1- n)
          for ai = (read)
          for bi = (read)
          do (push bi (aref graph ai))
             (push ai (aref graph bi)))

    ;; p, xを読み込み、カウンターに記録
    (loop for i from 1 to q
          for p-i = (read)
          for xi  = (read)
          do (incf (aref counter p-i) xi))
    (let ((visited (make-array (1+ n) :initial-element nil)))
      ;; 頂点1を訪問済にして深さ優先探索を開始
      (setf (aref visited 1) t)
      (dfs graph counter '(1) visited)
      (format t "~{~A~^ ~}~%" (coerce (subseq counter 1) 'list)))))

(defun dfs (graph counter visiting visited)
  ;; 深さ優先探索でグラフを探索し、カウンターを更新する
  (when (not (null visiting))
    (loop with pos = (pop visiting)
          for dest in (aref graph pos)
          when (not (aref visited dest))
            do (incf (aref counter dest) (aref counter pos)) ; 次の頂点にカウンターの値を加算する
               (setf (aref visited dest) t)
               (push dest visiting))
    (dfs graph counter visiting visited)))

(solve)

ACできたのですが、訪問済のチェックを省略していて1回WAになってしまいました。

2026年9月14日月曜日

ABC128C - Switchesをビット全探索で解いてみた

40代文系社内SEがAtCoder Beginner Contest 128 C - Switchesを解いてみました。

まず問題文を読んでみる。
1行目と3行目は問題ないが、2行目がうまくのみこめない。

「電球iはki個のスイッチに繋がっており、スイッチsi1, si2, ..., sikiのうちonになっているスイッチの個数を2で割った余りがpiに等しいときに点灯します。」

ちょっとずつ分解してみる。
まず、piは制約に0または1と記載されているので、2で割った余りがpiに等しいとは、つまり偶数か奇数ということ。
「全部がonだったら」とか、「どれかひとつがonだったら」なら容易にイメージできるのに。

残りの前半部分については、電球ごとに繋がっているスイッチは異なり、
個数がkiで示され、続けてsijでどのスイッチに繋がっているかが示される。

続けて制約をみる。
NもMも10以下というのはかなり小さい値だ。
それ以外は特に気になることはなかった。

スイッチの状態がon/offの2種類であることと、個数が最大10個という制約から、スイッチの状態が全探索できそうだ。
$2^{10} = 1024$

電球が点灯しているかどうかを判定するには、電球ごとにonになっているスイッチを数えなければならない。
電球もスイッチも最大10個だから、最大でも10 * 10で網羅できる。

あとは実装方法だが、スイッチの状態がon/offの2種類なのでbit全探索が使える。
0以上(2^N)未満までループで回せば網羅できる。
例えば入力例1だと、0(00)、1(01)、2(10)、3(11)の4通りになる。

以上より、以下3ステップで問題を解く。
1. スイッチの状態ごとに、全ての電球が点灯しているか判定する
2. 電球ごとに、点灯しているか判定する
3. 電球に繋がっているスイッチごとに、onになっているか判定する


(let* ((n (read))
       (m (read))
       (k (make-array m))
       (s (make-array m :initial-element nil))
       (p (make-array m)))

  ;; k, sを読み込み
  (loop for i from 0 below m
        for ki = (read)
        do (setf (aref k i) ki)
           (loop repeat ki
                 for sij = (read)
                 do (push sij (aref s i))))

  ;; pを読み込み
  (loop for i from 0 below m
        for p-i = (read)
        do (setf (aref p i) p-i))

  ;; on/off
  (loop for stat from 0 below (expt 2 n)
        ;; 点灯している電球の個数
        for light-on = (loop for light from 0 below m
                             ;; onになっているスイッチの個数
                             for switch-on = (loop for switch in (aref s light)
                                                   count (logbitp (1- switch) stat))
                             count (= (mod switch-on 2)
                                      (aref p light)))
        count (= light-on m)
          into it
    finally
       (format t "~A~%" it)))

AtCoder Beginner Contestを解いてみたリスト

2026年9月4日金曜日

ABC143D - Trianglesを二分探索で解いてみた

40代文系SEがAtCoder Beginner Contest 143 D - Trianglesを解いてみました。

様々な長さの棒が与えられ、その中から3本選んで三角形を作る選び方は何通りかを求める問題です。

まず注目したのは制約の$10^3$です。
あまり見かけない数字です。
$N^2$でも$10^6$なので2重ループが使えそうです。

3本の棒のうち、2本まで選ぶと3本目の長さの最大値が決まります。
棒の選び方にもともとの並び順は関係ないので、ソートしても問題ありません。
ということで2分探索が使えます。よっしゃ。

3本中2本を全探索で選び、残りの1本として選べる範囲を2分探索で求めることでACできました。
(defun solve ()
  (let* ((n (read))
         (l (make-array n :initial-contents (loop repeat n collect (read)))))
    (format t "~A~%" (solve2 l))))

(defun solve2 (l)
  (let* ((ll (make-array (1+ (length l)))))
    (loop for i from 0 below (length l) do (setf (aref ll i) (aref l i)))
    (setf (aref ll (1- (length ll))) (1+ (expt 10 3)))
    (sort ll #'<)
    (loop with ans = 0
          for i from 0 below (- (length ll) 2)
          do (loop for j from (1+ i) below (- (length ll) 1)
                   do (incf ans (solve1 ll i j)))
          finally
             (return ans))))

(defun solve1 (l i j)
  (labels ((f (min max)
             (if (= (1+ min) max)
                 min
                 (let* ((li (aref l i))
                        (lj (aref l j))
                        (mid (+ min (floor (- max min) 2)))
                        (lmid (aref l mid)))
                   (if (< lmid (+ li lj))
                       (f mid max)
                       (f min mid))))))
    (- (f j (1- (length l))) j)))

(solve)

AtCoder Beginner Contestを解いてみたリスト