Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

ABC104をHaskellで

0
Last updated at Posted at 2025-12-27

A - Rated for Me

問題 ABC104A

シグネチャを決める。

abc104a :: Int    -- R
        -> String -- 答え

問題文の述べるとおりに場合分けをする。

結果

abc104a r
  | r < 1200  = "ABC"
  | r < 2800  = "ARC"
  | otherwise = "AGC"

B - AcCepted

問題 ABC104B

シグネチャを決める。

abc104b :: String -- S
        -> Bool   -- 答え

普通に

入力は少なくとも4文字あることが保証されているので、1,2文字目、中間、末尾の文字、と分けて、全て空でないことが保証できる。
また、入力には英文字しか含まれないことも保証されている。
よって、

  • 先頭が A
  • 2文字めが小文字
  • 中間を大文字と小文字で選り分けたとき、大文字は C がひとつだけ
  • 末尾が小文字

を調べればよい。

import Data.Char

abc104b1 (c1:c2:s) = c1 == 'A' && isLower c2 && cs == "C" && isLower cN
  where
    cs = filter isUpper $ init s
    cN = last s

有限オートマトン

問題の条件に合うような文字列を受理する有限オートマトンを考えると、

  • 最初に A を受け取る
  • 続いて小文字を1つ受け取る
  • 続けて小文字を0個以上受け取り、C を受け取る
  • 続いて小文字を1つ受け取る
  • 受理状態、さらに小文字が続いても受理状態

素直にコードに直す。

abc104b s = q0 s
  where
    q0 (c0:s0) = c0 == 'A' && q1 s0
--  q0 "" = False 不要
    q1 (c1:s1) = isLower c1 && q2 s1
--  q1 "" = False 不要
    q2 (c2:s2)
      | isLower c2 = q2 s2
      | c2 == 'C'  = q3 s2
    q2 _ = False -- "" と C以外の大文字の場合
    q3 (c3:s3) = isLower c3 && q4 s3
    q3 "" = False
    q4 (c4:s4) = isLower c4 && q4 s4
    q4 "" = True

q4q4 s4 = all isLower s4 に短縮できる。
これを q3 に埋め込めんで q3 s3 = not (null s3) && all isLower s3 に短縮できる。

q2 は分岐がいろいろなのでロジックが複雑だけど、

q2 s2 = case dropWhile isLower s2 of
  'C':s3 -> q3 s3
  _      -> False

q0q1q0 (c0:c1:s1) = c0 == 'A' && isLower c1 && q2 s1 と埋め込みできる。

結局、同等の計算が次のようにまとまる。

abc104b (c0:c1:s2) = c0 == 'A' && isLower c1 &&
  case dropWhile isLower s2 of
    'C':s3 -> not (null s3) && all isLower s3
    _      -> False

正規表現

いわゆる「正規表現」があれば /^A[a-z]+C[a-z]+$/ とマッチさせるだけの話なんだが。
(英文字しか来ないことを使えば /^A\w+C\w+$/ でもいい)

C - All Green

問題 ABC104C

シグネチャを決める。横着する。

abc104c :: Int     -- D
        -> Int     -- G
        -> [[Int]] -- p_i, c_i
        -> Int     -- 答え

動的計画法

答えは $[1, \sum p_i]$ この上限は 1,000 と小さな範囲。

難易度 $i$ の問題について、0 問から $p_i$ 問までそれぞれ解いたときの得点の追加を考え、$k$ 問解くことで得られる最大の点数、をDPで更新していく。

import Data.Array.Unboxed
import Data.Bool

abc104c :: Int -> Int -> [[Int]] -> Int
abc104c _d g pcs = head [k | (k,s) <- assocs arrD, s >= g]
  where
    sump = sum $ map head pcs
    arr0 = listArray (0, sump) $ 0 : repeat (-1) :: UArray Int Int
    arrD = foldl' step arr0 $ zip [100, 200 ..] pcs
    step arr (i100, p:c:_) = accumArray max (-1) (0, sump)
      [ (k + j, s + i100 * j + bool 0 c (j == p))
      | (k,s) <- assocs arr, s >= 0, j <- [0 .. p]]

はまやん氏のユーザ解説「でやったほうが良かったかも」に相当する。

公式解説のやり方

  • 中途半端に解くのはいずれかの難易度一つで十分
  • 全問解く難易度の選択を $2^D$ とおり総当たりする

という方針。難易度の低い方から順に、全問解くか保留するかを再帰的に選び、保留した最大の難易度を、後で中途半端に解くために覚えておく。

abc104c :: Int -> Int -> [[Int]] -> Int
abc104c _d g pcs = recur 0 0 0 0 $ zip [100, 200 ..] pcs
  where
    recur :: Int -- 解いた問題数
          -> Int -- 合計得点
          -> Int -- 解かなかった最大難易度の得点
          -> Int -- 同上の問題数
          -> [[Int]] -- 残りの p_i, c_i
          -> Int -- 答え
    recur cnt acc _ _ _ | acc >= g = cnt -- 早上がり
    recur cnt acc i100 pi []
      | i100 > 0, k < pi = cnt + k  -- 使えるものの個数が足りる
      | otherwise        = maxBound -- 使い切っても足りないか、使えるものがない
      where
        k = divrup (max 0 $ g - acc) i100
    recur cnt acc i100 pi ((j100, pj:cj:_) : ipcs) = min b1 b2
      where
        b1 = recur (cnt + pj) (acc + j100 * pj + cj) i100 pi ipcs -- j は全て使う
        b2 = recur cnt acc j100 pj ipcs                           -- j を保留

divrup x y = negate $ div (negate x) y

あっ。

「$c_i, G$ はすべて100の倍数である。」と、難易度 $i$ はひとつ $100i$ 点なので、全て 1/100 で考えれば良かったのか。

D - We Love ABC

問題 ABC104D

シグネチャを決める。長いので ByteString を使う。

import qualified Data.ByteString.Char8 as BS

abc104d :: BS.ByteString -- S
        -> Int           -- 答え

こういう問題は中央の B に注目して考え、左の A の個数と右の C の個数の積が、$j$ を固定した $(i,j,k)$ の個数。
$1 \leq j \leq N$ について順にこれを数えればよい。
$S_j$ が B のときは答えを数え、A,C のときは左右のその文字の個数が増減する。

? が混じることで、少しだけ変化する。
中央が B のときだけでなく、? のときも答えを数えることになる。
また、注目していない位置の ? は何であってもよいので、そのようなバリエーションの個数全てについて、ABC数を倍増させて数える必要がある。

中央が B のとき:
左の A 右の C を一つ選んだとき、それを選択できる文字列は $2^Q$ とおりある。
左は ? 右は C を選ぶとき、それを選択できる文字列は $2^{Q-1}$ とおりある。
左は A 右は ? を選ぶとき、それを選択できる文字列は $2^{Q-1}$ とおりある。
左は ? 右も ? を選ぶとき、それを選択できる文字列は $2^{Q-2}$ とおりある。

中央が ? のとき、上の $Q$ を $Q-1$ にしただけの場合がある。

これらを数えるために、

  • 現在注目している位置 $j$
  • $j$ より左の A の個数 cA
  • より左の ? の個数 cQL
  • より右の C の個数 cC
  • より右の ? の個数 cQR
    を追跡する。

結果

import Data.List
import Data.Bool

abc104d :: BS.ByteString -> Int
abc104d bs = summ $ snd $ mapAccumL step (0,0,cC0,cQ0) $ BS.unpack bs
  where
    cC0 = BS.count 'C' bs
    cQ0 = BS.count '?' bs
    p3Q = modPower 3 cQ0
    p3Q1 = bool 0 (modPower 3 (cQ0 - 1)) $ cQ0 > 0
    p3Q2 = bool 0 (modPower 3 (cQ0 - 2)) $ cQ0 > 1
    p3Q3 = bool 0 (modPower 3 (cQ0 - 3)) $ cQ0 > 2

    step (cA, cQL, cC, cQR) 'A' = ((succ cA, cQL, cC, cQR), 0)
    step (cA, cQL, cC, cQR) 'C' = ((cA, cQL, pred cC, cQR), 0)
    step (cA, cQL, cC, cQR) 'B' = ((cA, cQL, cC, cQR), res)
      where
        res = summ $ map prodd [[cA, cC, p3Q],[cQL, cC, p3Q1],[cA, cQR, p3Q1],[cQL, cQR, p3Q2]]
    step (cA, cQL, cC, cQR) '?' = ((cA, succ cQL, cC, cQR1), res)
      where
        cQR1 = pred cQR
        res = summ $ map prodd [[cA, cC, p3Q1],[cQL, cC, p3Q2],[cA, cQR1, p3Q2],[cQL, cQR1, p3Q3]]

-- モジュロ計算は省略

なぜ僕はまず「シグネチャを決める。」のか。

最近発表されたポエムに「競技プログラミングのコードは全てを main() に突っ込んでいて汚い。実務ではそんなプログラムは書かない。」というような主張があった。

C++ などでコードを書くガチ勢な人たちは大抵そうしているせいで、そうしないといけない、と思い込んでいるのかもしれないが、別にそんなことはない。
解説を書く人たちはぜひ、解説には綺麗なコードを示してほしいと願っているが、あえてそうしないという主張も読んだことがあるので(フレンズさんだったかな?要出典:追記しました)人それぞれとしか言いようがない。

人それぞれなので、自分は、(自分の力の及ぶ範囲で)エレガントなコードを示したい、と考えてこれを続けている。

Haskellで競技プログラミングをする人で「interact 使うとIOと純粋を簡単に隔離できていい」という主張がときどき見られる。
自分はこれの気持ちが全くわからない。

答えを求めるアルゴリズム本体こそが問題の中心で、read系処理までの「手前の部分」は、ORMのような、プログラムの外とのインピーダンス整合をとる部分にすぎない。

問題の入力データは、例えば今回のC問題では、全て整数の文字列表記で

\begin{array}{ll}
D & G \\
p_1 & c_1 \\
\vdots \\
p_D & c_D
\end{array}

と与えられるそれは、

abc104c :: Int     -- D
        -> Int     -- G
        -> [[Int]] -- p_i, c_i
        -> Int     -- 答え

というシグネチャで関数に与えられるべきものであることは自明で、このシグネチャを設定すれば、それより手前の前処理がどう read すればよいかも自明になる。
例えば実際にはこうなっている。

main :: IO ()
main = do
  [d,g] <- bsGetLnInts
  pcs <- replicateM d bsGetLnInts
  let ans = abc104c d g pcs
  print ans

bsGetLnInts :: IO [Int]
bsGetLnInts = unfoldr (BS.readInt . BS.dropWhile isSpace) <$> BS.getLine

ここを作るのは、AIにやらせるまでもなく、機械化できそう。
Rust だとこんなのが提案されて

入力形式の指定書式がそのまま採用されたライブラリが一般化しているようだ。

(公式ドキュメントに

The macro’s user interface is basically the same with tanakh’s input macro.

と明記されている。)

HaskellでもTemplate Haskellとか型レベルプログラミングとかで、やればできるだろう。

だから僕は、シグネチャを決める。

追記

発掘しました。

基本的に実装例にはwriter解がそのまま貼ってあります。writer解というのはつまり「私たちにとって読みやすいコード」です。
なので、
「人に見せるコードは整えて欲しい」に対する答えは「人に見せるコードではありません」
「人に見せるコードを用意して欲しい」に対する答えは「その必要はないと考えています」
「人に見せるコードでないなら解説に載せないで欲しい」に対する答えは「その通りだと思います」

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?