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.

ABC116をHaskellで

Posted at

A - Right Triangle

問題 ABC116A

シグネチャを決める。

abc116a :: Int  -- |AB|
        -> Int  -- |BC|
        -> Int  -- |CA|
        -> Int  -- 答え
abc116a ab bc _ca = div (ab * bc) 2

角Bが直角なので、ヘロンの公式はいらない。

B - Collatz Problem

問題 ABC116B

シグネチャを決める。

abc116b :: Int  -- s
        -> Int  -- 答え

普通にする

出現した数を覚えておき、もう一度出現したときに、今の添え字が答えになる。

import qualified Data.IntSet as IS

abc116b :: Int -> Int
abc116b s = loop IS.empty s 1

f :: Int -> Int
f x = if even x then div x 2 else 3 * x + 1

loop :: IM.IntSet -> Int -> Int -> Int
loop is a cnt
  | IS.member a is = cnt
  | otherwise = loop (IS.insert a is) (f a) (succ cnt)

公式解説の「また」

コラッツ数列なので、最終的に $4 \to 2 \to 1 \to 4$ のループになる。
つまり、それ以外の数が重複することはない(するなら、上のループに到達できない)ので、
1,2のときは答えは4、それ以外のとき、初めて4になった添え字+3が答え。

abc116b 1 = 4
abc116b 2 = 4
abc116b s = (4 +) $ length $ takeWhile (4 /=) $ iterate f s

C - Grand Garden

問題 ABC116C

シグネチャを決める。

abc116c :: Int    -- N
        -> [Int]  -- h_i
        -> Int    -- 答え

Data.List.Split.wordsBy により、0な要素を捨て、それで区切られる部分列に分割することができる。linesBy と異なり、wordsBy は連続する空白をまとめて捨ててくれる。

こうして正規化した分割に対して、全てを1減らす。その個数分だけ水やりの回数をカウントする。
分割それぞれについて自身を再帰する。

結果

import Data.List.Split

abc116c :: Int -> [Int] -> Int
abc116c _n hs = loop hs

loop :: [Int] -> Int
loop hs = sum $ map (succ . loop . map pred) $ wordsBy (0 ==) hs

ユーザ解説の方法

より高速な解法 by MMNMM に従う。
これはつまり、解説にある絵の水色で塗られた「壁」の向かい合う間を埋めるために水やりを1ずつ必要とするので、その総数の半分が答えになる、という仕組み。

abc116c _n hs = flip div 2 $ sum $ map abs $ zipWith (-) (hs ++ [0]) (0:hs)

D - Various Sushi

問題 ABC116D

シグネチャを決める。

abc116d :: Int      -- N
        -> Int      -- K
        -> [[Int]]  -- t_i, d_i
        -> Int      -- 答え

わからなくて解説を見た。以下、ほぼ抜き書きな解説。

$p$ 種類のネタで獲得できるおいしさ基礎点の最大値を $f(p)$ とする。
スシをおいしさの降順で整列し、前 $K$ 個と後半に分ける。
前 $K$ 個のネタの種類数が $x$ おいしさの総和が $s$ のとき、$f(x) = s$ であるといえる。
前 $K$ 個は種類を気にせず大きい方から選択したのだから、それよりスコアが上がる選択肢はないからである。

またそのことを踏まえると、この $K$ 個からいずれかのネタを粛正し、それ以外の選択しているネタで補填することで、より少ないネタの種類数 $y < x$ での $f(y)$ を作ろうとすると、おいしさはより落ちる(または等しい)ものしか残っていないので、$f(y) \leq f(x)$ であるとわかる。
また、種類を減らすとボーナスポイントも減るため、最終スコアは確実に少なくなるので、種類を減らす方向に調べる必要はない。

逆に、おいしさを減らす危険を冒してでも種類を増やし、かつおいしさを極力減らさないようにするには、

  • 前 $K$ 個のうち、おいしさの小さい物から順に諦めて、それ以降の中から、おいしさの大きいものから順に加える。
  • ただし、種類数を減らさないために、既存のもののうち、ネタごとに、おいしさ最大のものは食べること確定
  • 追加する側は、種類数を増やすために、新規のネタのものだけ。つまり、まだ選ばれていないネタのうち、おいしさ最大のものだけが候補

となる。このようにして $z > x$ について $f(z)$ を順に構成し、ボーナスポイントを加えた後の最大値を選ぶ。

データ構造としては、選択しているネタの集合を管理する IntSet と、おいしさ×種類の (Int,Int) のリスト、逆順に取り出すためのスタック(はリスト)があればできそうだ。

結果

import qualified Data.IntSet as IS

abc116d :: Int -> Int -> [[Int]] -> Int
abc116d _n k tds =
-- 種類ボーナスポイントを加えて最大値をとる
    maximum $ zipWith (+) (loop total1 is1 ds1 dts2) (map (^2) [x1 ..])
  where
  -- おいしさの降順に前K個と残りに分ける
    (dts1, dts2) = splitAt k $ sortBy (flip compare) [(d, t) | t:d:_ <- tds]
-- 前K個で選んだネタ集合と、諦められるスシのおいしさの昇順リスト
    (is1, ds1) = foldl' step (IS.empty, []) dts1
    step (is, ds) (d,t)
      | IS.member t is = (is, d:ds)
      | otherwise      = (IS.insert t is, ds)
-- 前K個のおいしさの総和
    total1 = sum $ map fst dts1
-- 前K個の種類数
    x1 = IS.size is1
-- 種類数を増やしおいしさの減少を抑えるやり方で一つずつ交換
    loop acc is dds@(dx:ds) ((d,t):dts)
      | IS.member t is = loop acc is dds dts -- 使えないdtは捨てる
      | otherwise      = acc : loop (acc - dx + d) (IS.insert t is) ds dts
    loop acc _s _ _ = [acc] -- 候補のどちらかが尽きたら終わり
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?