コンテンツにスキップ

Hard

068 D - String Equivalence

解説1

  • 下記方針に基づいた最終実装例
  • 実装を見ると特に明確なように, 長さNの標準形は長さN-1の文字列に一文字足して作られます.
  • 出題例をもとに特に小さいNで実験してもわかります.
  • 追加できる文字はaを先頭に, 各文字列の中の最大の文字の一つ次までです.

ここまでの情報をもとにすれば素直に実装できます. まず補助関数を二つ作ります.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#r "nuget: FsUnit"
open FsUnit

// ある文字の次の文字
let succ (c:char) = int c |> (+) 1 |> char
// 文字列の中の最大の文字の一つ次
let m xs = List.max xs |> succ

succ 'a' |> should equal 'b'
succ 'b' |> should equal 'c'
succ 'z' |> should equal '{'

m ['a'] |> should equal 'b'
m ['a';'b'] |> should equal 'c'

今回の問題の範囲では困らないためそのままにしているものの, succ 'z'の挙動が気になるなら適切に書き換えてください.

さて, 長さ2の標準形の一つabに対して長さ3の標準形を作りましょう. 関数mによってaからcまで追加できます. 文字の追加の仕方はいろいろあって文字列の連結で素直に後ろにつなげてもいいでしょう. ここでは文字列(.NETとしては文字の配列)を文字のリストにし, リストの連結もconsを使って前に追加します.

1
2
3
let xs = ['b';'a']
xs |> ['a'..(m xs)] |> List.map (fun c -> c::xs))
|> should equal [['a';'b';'a'];['b';'b';'a'];['c';'b';'a']]

長さ2の標準形はaaabだから両方に作用させましょう.

1
2
3
let xss = [['a';'a'];['b';'a']]
xss |> List.map (fun xs -> ['a'..(m xs)] |> List.map (fun c -> c::xs))
|> should equal [[['a'; 'a'; 'a']; ['b'; 'a'; 'a']]; [['a'; 'b'; 'a']; ['b'; 'b'; 'a']; ['c'; 'b'; 'a']]]

これで長さ3の標準形が得られました. しかしこれはchar list list listです. 実際に欲しいのはstring listで, char liststring = char[]に変える必要もあります.

リストのネストを一つ減らすのはList.concatで, List.collect = List.map >> List.concatを使ってつなげて書けます. char list -> stringは.NETレベルのメソッドSystem.String.Concatを使います. リストはほしい文字列の逆になっているからList.revの処理も必要です.

これを再帰でまとめると次の解答が得られます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let solve N =
  let succ (c:char) = int c |> (+) 1 |> char
  let m xs = List.max xs |> succ
  let rec frec n =
    if n=1 then [['a']]
    else frec (n-1) |> List.collect (fun xs -> ['a'..(m xs)] |> List.map (fun c -> c::xs))
  frec N |> List.map (List.rev >> System.String.Concat)

let N = stdin.ReadLine() |> int
solve N |> List.iter stdout.WriteLine

解説2: 末尾再帰

少し書き方を変えれば末尾再帰にできます. 結論だけ書くと次の通りです.

1
2
3
4
5
6
7
8
9
let solve N =
  let succ (c:char) = int c |> (+) 1 |> char
  let m xs = List.max xs |> succ
  let rec frec n acc =
    if n=1 then acc
    else acc |> List.collect (fun xs -> ['a'..(m xs)] |> List.map (fun c -> c::xs)) |> frec (n-1)
  frec N [['a']] |> List.map (List.rev >> System.String.Concat)

stdin.ReadLine() |> int |> solve |> List.iter stdout.WriteLine

069 C - Align

解説

はじめに

公式解説の他にもいくつか解答を見ていると, 偶奇で場合分けをしているコードもあれば, 一本で計算しきっているコードもありました. ただ私にはあまり直観的でなくすっと理解しにくかったため, ここではcojnaさんのコード例を参考にした実装を紹介します.

簡単な方針

大きな数値から小さな数値を引いた方がよいのは明らかです. ソートして大きい数のリストhsと小さい数のリストlsにわけ, 大きい数と小さい数を順に並べ続ければいいでしょう. 特に分けたリストを再帰的に先頭から取れば十分です.

問題は全要素数が奇数の場合の処理で, hslsから一つずつ取り続けて余った項をどこに置くかが問題です.

具体例で実装

入力例1で上の方針を追いかけます.

1
let N, Aa = 5, [|6L;8L;1L;2L;3L|]

まずは「ソートして大きい数のリストhsと小さい数のリストlsにわけ」の部分を実装します.

1
2
3
4
5
6
#r "nuget: FsUnit"
open FsUnit

let (xs,ys) = Aa |> Array.sort |> Array.toList |> List.splitAt (N/2)
xs |> should equal [1L; 2L]
ys |> should equal [3L; 6L; 8L]

大きい方は大きい順に取るためList.revで順序を反転します.

1
2
3
4
5
#r "nuget: FsUnit"
open FsUnit

let zs = ys |> List.rev
zs|> should equal [8L; 6L; 3L]

あとはxszsの先頭から再帰的に処理します. 再帰関数をfrecとして次のような呼び出しを前提にしましょう.

1
2
3
4
let (xs,ys) = Aa |> Array.sort |> Array.toList |> List.splitAt (N/2)
let (l,ls) = (List.head xs, List.tail xs)
let (h,hs) = ys |> List.rev |> fun zs -> (List.head zs, List.tail zs)
frec (h-l) l h (ls,hs)

はじめから初項を分離しなくてもいいとは思いますが, cojnaコードに合わせています. h-lの部分が結果を積むaccumlator変数です. hはhigh, lはlowで値の大小の分割を表しています. 最後の(ls,hs)はタプルにせず分けても構いません. 次の実装を前提にタプルにしています.

さて, 再帰関数の本体を考えましょう.

1
2
3
4
let rec frec acc low high = function
  | (l::ls),(h::hs) -> frec (acc + h-low + high-l) l h (ls,hs)
  | [],[h] -> acc + ???
  | _,_ -> acc

functionの部分は次の省略表記です.

1
2
3
4
5
let rec frec acc low high (xs,ys) =
  match (xs,ys) with
    | (l::ls),(h::hs) -> frec (acc + h-low + high-l) l h (ls,hs)
    | [],[h] -> acc + ???
    | _,_ -> acc

これは次のように書いても構いません.

1
2
3
4
5
let rec frec acc low high xs ys =
  match (xs,ys) with
    | (l::ls),(h::hs) -> frec (acc + h-low + high-l) l h ls hs
    | [],[h] -> acc + ???
    | _,_ -> acc

以下前者のコードを前提に考えます. まずパターンマッチのmatchの第一行から考えましょう.

1
2
let rec frec acc low high xs ys =
  (l::ls),(h::hs) -> frec (acc + h-low + high-l) l h ls hs

まさにリストを先頭と残りに分割して処理しています. 再帰呼び出しのacc + h-low + high-lの項は, 一つ前の先頭の項に対して大きい数と小さい数の差を取るそのままの処理です. low, high, xs, ysの項も分割した項をそのまま積めば問題ありません.

残り二行を確認しましょう.

1
2
3
4
let rec frec acc low high xs ys =
  match (xs,ys) with
    | [],[h] -> acc + ???
    | _,_ -> acc

[],[h]は項数が奇数の場合の余りの処理で, 最後の_,_が項数が偶数の場合の処理です. 後者は積み切った値を素直に返せばよく何も考える必要はありません. したがってあとは一つ余った項の処理だけです.

結論から言えばmax (h-low) (high-h)です. はじめにsplitAt (N/2)でわけました. この分け方で最後の項がlshsのどちらに入るか変わります. どうしても揺れが起こるためmaxでその揺れを吸収しています.

入力例1と新たに作った以下のもう一つの入力例をもとに確認しましょう.

入力例1での最後の余りの処理は次のようになります.

1
2
3
4
5
6
7
8
9
xs -> [1L; 2L]
ys -> [8L; 6L; 3L]

low -> 2L
high -> 6L
h -> 3L

h-low -> 1L
high-h -> 3L

したがってこちらはhigh-hを取るべきです. 具体的に全体としてどのような並び方を選んだのかを考えるのも大事です. 実際には次のようになっています.

  • h-low: 8 1 6 2 3
  • high-h: 1 8 2 6 3

つまり初項を大きい方から取るか, 小さい方から取るかが最後の取り方で決まります.

さて, 新たな入力例はlet Aa = [|1L;4L;5L|]とします. この余りの処理は次のようになります.

1
2
3
4
5
6
7
8
9
Aa -> [|1L;4L;5L|]
xs -> [1L]
ys -> [5L;4L]

low -> 1L
high -> 5L
h -> 4L
h-low -> 3L
high-h -> 1L

入力例1と違ってh-lowを取るべきです. 具体的に全体としてどのような並び方を選んだかと言えば次の通りです.

  • h-low: 5 1 4
  • high-h: 1 5 4

もちろん他の可能性がないかも考えるべきではありますが, 前の項との差を取るアルゴリズムの組み方からしてありうるのはこの二通りしかありません. あとはこれを一般的にきちんと書き切れば適切なコードができます.

070 C - Snuke Festival

自分用の記録

公式解説では二分探索を提案しています. それぞれ次のような解答例があります.

F#版はstart-1stop+1を外して書けないかと思って轟沈し, Haskell版も番兵なしで書けないかと思って轟沈し, 自力実装もTLEではまり倒したため, CやPythonなどのコードを見つついろいろ試し, 最終的にはRustのコードを参考にしました. ここでもこれに基づいた解説をつけます.

解説

参考にした命令型のRustのコードの焼き直しを説明したあと, 関数型に書き換えたバージョンを説明します.

まずは公式解説と同じく配列で入力を取ってソートします. あとの都合があるためBaもソートします.

1
2
3
  let Xa = Aa |> Array.sort
  let Ya = Ba |> Array.sort
  let Za = Ca |> Array.sort

以下ソートした配列Xa, Ya, Zaで考えます. 何も考えずRustを直移植した結果は次の通りです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  let Xa = Aa |> Array.sort
  let Ya = Ba |> Array.sort
  let Za = Ca |> Array.sort
  let mutable i = 0
  let mutable k = 0
  let mutable ans = 0L
  for b in Ya do
    while i<N-1 && Xa.[i+1]<b do i <- i+1
    while k<N-1 && Za.[k]<=b  do k <- k+1
    if Xa.[i]<b && b<Za.[k] then ans <- ans + (int64 (i+1) * int64 (N-k))
  ans

アルゴリズム上のポイントはBaのソートとi, kの引き継ぎです. AaCaをソートしているため配列の引数からそのまま個数が計算できます. さらにBaのソートのおかげで, Baに関する添字をjとすれば, j+1に関する結果を計算するときjで計算したikから再開でき, 余計な計算が省略できます. Zaの関する計算は問題の素直な不等式ではなくZa.[k]<=bにした上でN-kを計算しています. (解説を書きながら気づいたのですが, Array.sortDescendingを使えば問題の条件のまま計算できます.)

F#コードのポイントはans <- ans + (int64 (i+1) * int64 (N-k))にもあります. ここでint64 (i+1) * (N-k)としてしまうとWAが出ます. 実際に出てはまり倒しました. 理由はint(i+1) * (N-k)が計算されるとオーバーフローを起こす場合があり, このかけ算自体もint64で計算しないといけないからです. 配列の添字はintint64にはできないため, 配列の添字を使った計算には注意が必要です.

命令型での焼き直しができました. 一般的には命令型の方が速くメモリ効率もよいため, これで終わらせても問題ありません. しかし関数型競技プログラミングを標榜している以上やはり関数型に書き換えます.

まずはfor b in Ya doから書き換えます. 最終的にはlet mutable ans相当の値を返したいため, foldを使って処理すればいいでしょう. 具体的には次のように書けます..

1
2
3
4
5
6
7
  let mutable i = 0
  let mutable k = 0
  let mutable ans = 0L
  (0L, Ya) |> Array.fold (fun acc b ->
    while i<N-1 && Xa.[i+1]<b do i <- i+1
    while k<N-1 && Za.[k]<=b  do k <- k+1
    if Xa.[i]<b && b<Za.[k] then acc + (int64 (i+1) * int64 (N-k)) else acc)

F#では最後に計算した式の値を返してくれるため, これを関数の最後に書けば問題ありません. ポイントはifに対してelse節を書いて値を返す部分です. そもそもelse節がないとエラーを吐いてくれるため, そこで気付けるでしょう.

あとはmutableikをうまく処理する必要があります. 積んだ結果を引き回す必要があるため, accをタプルに変えて積む必要があります.

ループは一般に再帰で書き換えられます. これもelse節で値を返す必要があり, 特に次のように書けます.

1
2
  let rec a i b = if i<N-1 && Xa.[i+1]<b then a (i+1) b else i
  let rec c k b = if k<N-1 && Za.[k]<=b  then c (k+1) b else k

関数名は一文字でacです. 短いプログラムだからこれで十分意図は伝わるでしょう. 気にいらなければもう少し長く説明的な名前でも構いません. この関数を使うとfoldは次のように書き換えられます.

1
2
3
4
5
  ((0L,0,0),Ya) ||> Array.fold (fun (acc0,i0,k0) b ->
    let i = a i0 b
    let k = c k0 b
    let acc = if Xa.[i]<b && b<Za.[k] then acc0 + (int64 (i+1) * int64(N-k)) else acc0
    (acc,i,k)) |> fun (acc,_,_) -> acc

acc(acc,i,k)に書き換わりました. 計算結果もタプルで返します. 先程と違い, foldの最終的な返り値もタプルになってしまうため, 最後に必要な第一要素だけ取り出す関数を噛ませてあります. 最終的な全体実装は次の通りです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let solve N Aa Ba Ca =
  let Xa = Aa |> Array.sort
  let Ya = Ba |> Array.sort
  let Za = Ca |> Array.sort
  let rec a i b = if i<N-1 && Xa.[i+1]<b then a (i+1) b else i
  let rec c k b = if k<N-1 && Za.[k]<=b  then c (k+1) b else k
  ((0L,0,0),Ya) ||> Array.fold (fun (acc0,i0,k0) b ->
    let i = a i0 b
    let k = c k0 b
    let acc = if Xa.[i]<b && b<Za.[k] then acc0 + (int64 (i+1) * int64(N-k)) else acc0
    (acc,i,k)) |> fun (acc,_,_) -> acc

let N = stdin.ReadLine() |> int
let Aa = stdin.ReadLine().Split() |> Array.map int64
let Ba = stdin.ReadLine().Split() |> Array.map int64
let Ca = stdin.ReadLine().Split() |> Array.map int64
solve N Aa Ba Ca |> stdout.WriteLine

コードをもっと短くしたければ次のように読み込み時点でソートできます.

1
2
3
let Aa = stdin.ReadLine().Split() |> Array.map int64 |> Array.sort
let Ba = stdin.ReadLine().Split() |> Array.map int64 |> Array.sort
let Ca = stdin.ReadLine().Split() |> Array.map int64 |> Array.sort

071 C - Linear Approximation

解説

公式解説通り入力Aaをシフトした配列をBaとします. 解説の便宜のためBaをソートした配列をCaとしましょう.

公式解説通りに実装すれば問題ありません. 他の人のコードを見ていると中央値の扱いにぶれがあるようです. 単にBa.[N/2]だけとしているコードもあれば, 偶奇でわけたコードもあります.

ちなみに私はさらに違う処理にしました. 選ぶべきbが整数でなければならないため, Nが奇数のときはCa.[N/2], Nが偶数のときはCa.[N/2]Ca.{N/2-1}を中央値の候補としています. もちろんNが偶数のときは二つの値を計算して小さい方を取ります.

どう書くとすっきりするかは人によるものの, 私は中央値の候補を配列にして処理しました. 具体的には次のように中央値の候補を作っています.

1
2
  let Ba = Aa |> Array.mapi (fun i a -> a - (int64 (i+1)))
  let Ma = Ba |> Array.sort |> fun Ba -> if N%2=1 then [|Ba.[N/2]|] else [|Ba.[N/2-1];Ba.[N/2]|]

最終的な悲しさの最小値は中央値の配列Maから次のように計算しています.

1
  Ma |> Array.map (fun b -> Ba |> Array.sumBy (fun x -> abs(x-b))) |> Array.min

よほど複雑な処理をかませる場合は適切な対処が必要ですが, Array.map f |> Array.sumArray.sumByで書くとすっきりします.

私がはまり倒したため念のため書いておきます. 次のように書くとMaの値を取ってしまい正しい悲しさの最小値が得られません.

1
  Ma |> Array.minBy (fun b -> Ba |> Array.sumBy (fun x -> abs(x-b)))

おまけ

次の二つのコードの結果が面白いです.

1
2
3
4
let n = int (stdin.ReadLine())
let a = stdin.ReadLine().Split() |> Array.mapi (fun i x -> int64 x - int64 i - 1L) |> Array.sort
let b = if n % 2 = 0 then (a.[n / 2] + a.[n / 2 - 1]) / 2L else a.[n / 2]
printfn "%d" (Array.sumBy (fun x -> abs (x - b)) a)
1
2
3
4
let n = int (stdin.ReadLine())
let a = stdin.ReadLine().Split() |> Array.mapi (fun i x -> int64 x - int64 i + 1L) |> Array.sort
let b = if n % 2 = 0 then (a.[n / 2] + a.[n / 2 - 1]) / 2L else a.[n / 2]
printfn "%d" (Array.fold (fun (l : int64) k -> l + abs (k - b)) 0L a)

違うのは最後の和を取る部分でsumByなのかfoldかです. 実行時間は変わらないものの消費メモリが一割違います. 少なくともMono 4.0でメモリ効率を考えるならfoldの方がよいようです.

そしてさらに私の提出コードも見てみましょう.

1
2
3
4
5
6
7
8
let solve N Aa =
  let Ca = Aa |> Array.mapi (fun i a -> a - (int64 (i+1)))
  let Ba = Ca |> Array.sort |> fun Ba -> if N%2=1 then [|Ba.[N/2]|] else [|Ba.[N/2-1];Ba.[N/2]|]
  Ba |> Array.map (fun b -> Ca |> Array.sumBy (fun x -> abs(x-b))) |> Array.min

let N = stdin.ReadLine() |> int
let Aa = stdin.ReadLine().Split() |> Array.map int64
solve N Aa |> stdout.WriteLine

偶数項のとき和を二回取っているため明らかにこちらの方が遅いはずですが, Mono 4.0よりも高速です. Mono 4.0のリリースが2015年, .NET core 3.1.201のリリースが2021年で後者の方が新しいため, 処理系の高速化のおかげでしょう.

072 C - 4/N

数学から決まる議論

特に小さいNに対して大学受験でもよくある問題です. 数学の話でもあって数学系学習と並行した議論を目指す私のスタンスからは大事なため, 公式解説の議論を簡単にくり返します.

対称性によってどれから議論してもよいためまずはhから考えましょう. 必要条件からの絞り込みでhを小さい方から増やして具体的に確認し続ければよく, まさにアルゴリズミックに次のように議論を進めます.

  • h=1として4/N - 1を計算する.
    • これは1/n+1/kに等しいはずだから再びn=1として左辺に移行する.
      • 4/N-1-11/kが等しくなる整数kが取れるか確認する.
    • 取れなければn=2で確認する.
      • 4/N-1/2-11/kが等しくなる整数kが取れるか確認する.
    • 取れなければn=3で確認する.
      • 4/N-1/2-11/kが等しくなる整数kが取れるか確認する.
    • ...
    • 順次n=hまで確認する.
  • h=2として4/N-1/2を計算する.
    • ...

と続けます. 原理的にこれしか議論しようはないものの, 時間内に解けるかが懸念点です. そして問題文でh,n,w≤3500が保証されているため, この範囲内でけりが着くはずと思って全探索します. もちろん3つ全てでループするとTLEするため, 2つ決まれば3つ目は自動的に決まる性質を使って二重ループで片付けます.

解答1: 素直なループ

1つ求めればよいため, 1つ決まったら計算を停止させます. 命令型的なfor文とbreakを使えればよいのですが, 残念ながらF#にはbreakがないようで, 使うならwhileとフラグ管理です.

ただし命令型として素直なforbreakがないだけです. Haskellの遅延リスト・遅延評価と同じく, 遅延評価してくれるseqを使いましょう. 次のような適切な書き方をすれば明示的なbreakが不要です.

1
2
3
4
5
  seq {
    for h in 1L..N do
      for k in 1L..M do
        if someJudge then yield value
  } |> Seq.head

for h in 1L..N doがカウントアップのforで, 二重ループだから二つ出てきます. 条件を満たした値を取る部分がif b then yield valueです. この場合はyieldキーワードはなくてもよいようですが手癖で書いています. 詳しくは公式のシーケンスを見てください. seqとコンピュテーション式による見慣れない構文には慣れるしかありません.

seq {...}でいわば遅延リストを作り, その先頭の値をSeq.headで取れば必要な計算だけで処理が終わります. Schemeのように有理数を処理してくれる言語もありますが, F#標準で有理数はないため整数の範囲でおさまるように標準的な対処を取ります. 適当なところで打ち切った数による積で構成する三つ目の整数がintに入る保証がないため, int64で計算する点に注意すれば, あとは特に問題ないでしょう. 結論としては次のようなコードで通ります.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let solve N =
  seq {
    for h in 1L..3500L do
      for k in 1L..h do
        let d = 4L*h*k-N*(k+h)
        if d>0L && (N*h*k)%d=0L then yield (h,k,(N*h*k)/d)
  } |> Seq.head |> fun (h,k,w) -> [|h;k;w|]

let N = stdin.ReadLine() |> int64
solve N |> Array.map string |> String.concat " " |> stdout.WriteLine

出力処理は何となく既存の出力処理を流用するために配列にしました. タプルのまま次のようにも書けます.

1
solve N |> fun (h,n,k) -> printfn "%d %d %d" h n k

解説2: 再帰

「競技プログラミングのためのF#入門」で無限ループを含めてループは再帰で書けると書きました. 実際ここでも再帰で書けます. 提出結果を見ると再帰の方がメモリを食っています. 2022-12時点では効率を求めるよりも通るコードを書く, さらには基本的なアルゴリズムの教育用コンテンツを作る方に主眼があるため, 別解として再帰によるコードも紹介します.

forのコードを素直に書き換えればよく, 結論としては次のように書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let solve N =
  let rec frec h k =
    let d = 4L*h*k - N*(k+h)
    if d>0L && (N*h*k)%d=0L then (h,k,(N*h*k)/d)
    elif k<h then frec h (k+1L)
    else frec (h+1L) 1L
  frec 1L 1L

let N = stdin.ReadLine() |> int64
solve N |> fun (h,k,w) -> printfn "%d %d %d" h k w

ポイントはもちろんifの条件分岐です. 最初に条件をみたして値を返すか(停止するか)判定し, 停止しなければhkを適切にカウントアップします. ここでは上記のseq+ループアルゴリズムに合わせました.

コードが読みにくくなるためここでは勧めませんが, 例えば次のようにまとめて書けます.

1
2
3
4
5
let solve N =
  let rec frec h k = let d = 4L*h*k - N*(k+h) in if d>0L && (N*h*k)%d=0L then (h,k,(N*h*k)/d) elif k<h then frec h (k+1L) else frec (h+1L) 1L
  frec 1L 1L

stdin.ReadLine() |> int64 |> solve |> fun (h,k,w) -> printfn "%d %d %d" h k w

さらに次のようにも書けます.

1
2
let solve N = let rec frec h k = let d = 4L*h*k - N*(k+h) in if d>0L && (N*h*k)%d=0L then (h,k,(N*h*k)/d) elif k<h then frec h (k+1L) else frec (h+1L) 1L in frec 1L 1L
stdin.ReadLine() |> int64 |> solve |> fun (h,k,w) -> printfn "%d %d %d" h k w

let ... inはOCamlのコードでよく出てきます.

073 C - Strange Bank

解答1: 公式解説, シンプルなループ処理

公式解説の処理をF#で単純に書き直すならこのコードのようになるでしょう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
open System
let n : int = int(Console.ReadLine())
let mutable res = n
let mutable i = 0
while i <= n do
    let mutable cc = 0
    let mutable t = i
    while t > 0 do
        cc <- cc + t % 6
        t <- t / 6
    t <- n - i
    while t > 0 do
        cc <- cc + t % 9
        t <- t / 9
    if res > cc then
        res <- cc
    i <- i + 1
printfn "%d" res

最初のwhile i <=n doforで簡単に書き換えられるとして, 可変変数を不変変数に置き換えつつ, 内部のwhile t > 0 doをどう関数型らしく書き換えるかがまず問題です. F#解説で書いたように再帰で書き換えます.

tは各whileの直前で設定し直していて最終的にccを返せばよいため, tは単純に関数の入力パラメータにすればよいでしょう. 6で割るwhileループを表す再帰関数は次のように書けます.

1
let rec frec cc t = if t>0 then frec (cc+t%6) (t/6) else cc

正の数を正の数で割り続けたあまりは0になるため, then節が短くなるように条件式を書き換えています. 9で割り続ける処理はmod kkが変わるだけで同じ処理です. それもまとめた再帰関数は次のように書けます.

1
let rec frec k cc t = if t>0 then frec k (cc+t%k) (t/k) else cc

次にメインのwhile i<=n doを考えます. これは最終的に積んだ値resを返すループだからfoldを使えばよいでしょう. 結論としては次のように書けます.

1
2
3
4
5
6
let solve N =
  let rec frec k cc t = if t>0 then frec k (cc+t%k) (t/k) else cc
  (N,[|0..N|]) ||> Array.fold (fun res i ->
    let cc6 = frec 6 0 i
    let cc9 = frec 9 cc6 (N-i)
    if res <= cc9 then res else cc9)

最後のif式ではelseをつけ忘れないようにしましょう. もちろんつけないと型エラーで動きません.

最後に, そもそも意味がわからずはまり倒したため, let cc6 = frec 6 0 i; let cc9 = frec 9 cc6 (N-i)で何をしているかを説明する自分用のメモ. まず次のように書き換えて考えるといいでしょう.

1
2
    let cc6 = frec 6 0 i
    let cc9 = frec 9 0 (N-i)

こうすると次のように意味がはっきりします.

  • cc6: i6のベキだけで処理した回数
  • cc9: 残りのN-i9のベキだけで処理した回数

あとは全探索の部分です. ある数i6のベキで処理し切ったら残りのN-i9のベキで処理するしかありません. これを全探索で全てのiに対して確認しています.

解説2: ユーザ解説, メモ化再帰

ユーザ解説はDPで解いています. 私の前にF#でDPで解いている人がいます. 実行時間も短いためこれも考えてみましょう. 上記コードはちょっと長いため単純化したコードを紹介します. 2022-12時点で全てではないものの, Educational DP Contest / DP まとめコンテストにF#で取り組んだ結果があるため, こちらも参考にしてください. (2022-12時点では特にHaskell・OCamlコードを焼き直しただけで, 私は自力でDPを解く力量がありません.)

まずF#版のarray6array9にあたる配列生成を考えます. もちろんいくつか書き方はあり, 比較的簡潔なのは次のコードでしょうか.

1
2
3
  let rec p k x acc = if x>N then (acc-1) else p k (k*x) (acc+1)
  let array6 = [|0..(p 6 1 0)|] |> Array.map (pown 6)
  let array9 = [|0..(p 9 1 0)|] |> Array.map (pown 9)

再帰関数のpはベキを何回まで取ればいいか計算する関数です.

次は本丸のメモ化再帰です. 定型的な書き方があるため, コピペで使えるようにReference.fsxにメモしています.

まずはコードを示してそれにコメントしましょう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  let memorec f =
    let memo = System.Collections.Generic.Dictionary<_,_>()
    let rec frec j =
      match memo.TryGetValue j with
        | exist, value when exist -> value
        | _ -> let value = f frec j in memo.Add(j, value); value
    frec
  let count frec n =
    if n=0 then 0
    else
      let c1 = n - (array6 |> Array.findBack (fun x -> x<=n))
      let c2 = n - (array9 |> Array.findBack (fun x -> x<=n))
      1 + min (frec c1) (frec c2)
  (memorec count) N

メモ化の部分がlet memorec fです. let memo = System.Collections.Generic.Dictionary<_,_>()はF#のMapではなく.NETの辞書を読んでいます. memo.Addで破壊的に書き換わってくれた方が便利だからです. let rec frec jはメモがあればその値を返し, ない場合は値を詰めて返します.

処理の本体がcountです. 第一引数のfrecmemorec中でfrecとして呼び出す関数で, 対応を明確にするために名前を揃えています.

まずc1から考えましょう. 6のベキで大きい方から削るため, (array6 |> Array.findBack (fun x -> x<=n))で削れる中で最大の数を取ります. c1は削って残った数で, frec c1でメモ化再帰または動的計画法でc1に辿り着くまでの最小操作回数が取れます. 実際にcount関数のelse節でprintfn "%A" (c1,c2,frec c1, frec c2)で出力して確認してみてください.

同じ計算を9に対しても適用して, 小さい方を取れば最小回数が得られます. 最後に1を足すのはmin (frec c1) (frec c2)は最終ステップ一手前の値だからです.

あとはcountmemorecでガワをかぶせて計算すれば求める結果が得られます. メモ化のための辞書memoをクロージャーmemorecで隠蔽している分コードが読みにくいかもしれません. オリジナルは最終的に呼び出す関数が素直な再帰になっているため, こちらの方がわかりやすいかもしれません.

074 D - Harlequin

解説

特記事項はなく, 解説通り次のように書けばいいでしょう.

1
2
3
4
5
let solve = Array.forall (fun x -> x%2=0) >> fun b -> if b then "second" else "first"

let N = stdin.ReadLine() |> int
let Aa = Array.init N (fun _ -> stdin.ReadLine() |> int)
solve Aa |> stdout.WriteLine

関数の合成としてsolveの引数を省略して書いています. 関数の合成は>>です. 数学での関数合成は$f \circ g$で$g$を作用させてから$f$を作用させます. F#でこれを書きたいならf << gです. パイプラインの発想と同じく左から右に流して書くにはg >> fです.

上記のsolveは引数をつけると次のように書けます.

1
let solve Aa = Aa |> Array.forall (fun x -> x%2=0) |> fun b -> if b then "second" else "first"

「全て偶数」の代わりに「奇数が存在する」と書きたければ次のようにも書けます.

1
let solve = Array.exists (fun x -> x%2=1) >> fun b -> if b then "first" else "second"

075 D - Face Produces Unhappiness

解説

考え方としてはユーザー解説の方がわかりやすいでしょう. 引用します.

で、これらを考えていくと、回転させるのは、ある同一方向を向いている塊を回転させれば良さそうということになる。 やっと400点レベルにまで落ちてきた。 方向が一致している区間を縮約するとLRLRLRやRLRLRLになっているはず。 例えばLRLRLRの最初のRを回転させるとLLLRLRでLRLRとなり、方向が一致していない箇所が2つ減る。 なので、LかRのどちらかを貪欲に回転させて、方向不一致の区間を減らしていったときの幸福人数が答え。 幸福人数はN-(LかRのグループ数)となる。各グループで1つは幸福でないため。

あとはこれをどうコードに落とし込むかにかかっています.

最大値を計算する部分に集中します.

  • 元からの幸福度を計算する. これは前後の向きが一致しているか, 各位置の前後のペアの文字を調べて確認すればよい.
  • 部分列をうまく回転させると回転させた両端の2だけ幸福度が上がる. 特に最大2*Kだけ幸福度が上がる.

この和を取れば幸福度最大の状況の幸福度が計算できるはずです. ただしLLLLLRRRRRの例のような真の最大幸福度が実現される場合, 問題の制約によって端の人の幸福度の処理が必要で, この処理を忘れてはいけません.

実装1

F#の文字列は文字列のモジュールもある一方で文字のシーケンス(Seq)です. 文字列のモジュールにほしい処理がなくても, シーケンスの関数を拝借して処理できます. 実際Seq.pairwiseで前後ペアのシーケンスが取れるため, これで元の幸福度計算用の処理を進めればいいでしょう. 例えば次のような結果が得られます.

1
2
3
4
#r "nuget: FsUnit"
open FsUnit

"LRLR" |> Seq.pairwise |> should equal (seq [('L', 'R'); ('R', 'L'); ('L', 'R')])

前後ペアを取った結果のシーケンスから幸福度を計算するには, シーケンスのたたみ込みで和を取ればよく, 典型的にはSeq.foldで計算できます. ここではSeq.sumByを紹介します.

1
  let s = S |> Seq.pairwise |> Seq.sumBy (fun (a,b) -> if a=b then 1 else 0)

これが処理の本体で, あとは次の通りです.

1
2
3
4
5
6
7
let solve N K S =
  let s = S |> Seq.pairwise |> Seq.sumBy (fun (a,b) -> if a=b then 1 else 0)
  min (s+2*K) (N-1)

let N,K = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let S = stdin.ReadLine()
solve N K S |> stdout.WriteLine

実装2

HaskellにはあってもF#にはない便利関数は数限りなくある一方, 珍しくHaskellだと前後ペアを作る便利関数がないようです. こういう場合はzipzipWithを使うのが典型的な処理です. そしてHaskellコードの移植時に慣れていないとはまる部分でもあります. ちなみにHaskellのzipWithはF#だとSeq.map2です.

さてどうやって前後ペアを作るかというと, zipを使って二つのシーケンスをタプルのシーケンスにまとめます.

1
2
3
4
#r "nuget: FsUnit"
open FsUnit

Seq.zip [1;2] [3;4;5] |> should equal (seq [(1,3);(2,4)])

上の例のようにシーケンスのzipは二つのリストの長さが一致しなくても短い方に自動的に合わせてくれます. しかしリストや配列では長さが違うとエラーになるため注意してください.

これを使うと前後ペアは次のように作れます.

1
2
3
4
5
#r "nuget: FsUnit"
open FsUnit

let S = "LRLR"
(S, Seq.tail S) ||> Seq.zip |> should equal (seq [('L', 'R'); ('R', 'L'); ('L', 'R')])

あとは実装1と同じように処理すれば求める結果が得られます.

076 C - たくさんの数式

解説

文字列がもっと長いと気になってはくるものの, (末尾再帰ではない)再帰でさっと書いてしまえばいいでしょう. HaskellではfoldMなどで豪快な処理が書けるものの, (今の私のF#の腕前では)シンプルな移植ができないのも理由の一つです.

計算用の再帰関数は次のように実装できます.

1
2
3
  let rec frec a y = function
    | [] -> a+y
    | x::xs -> frec (a+y) x xs + frec a (y*10L + x) xs

入力の文字列を一桁数値のリストに変換する処理は次のように書けます.

1
  let ys = S |> Seq.map (fun c -> int64 c - int64 '0') |> Seq.toList

ここで1を単にint64 cだけにすると49Lが返ってきてはまり倒します. 上のようにint64 '0'を引いて整数として1Lが返るようにするか, c |> string |> int64のように文字列にしてからint64を通しましょう. 例を見るとわかるようにInt32の範囲を飛び越えるため, オーバーフロー対策でInt64を使うのは必須です.

その他

逐次計算していくのではなく, いったん文字列から必要な数を切り出す(ベキ集合を作る)タイプの実装でHaskellコードを見ると, replicateMを使ってブーリアンのベキ集合を使ってどの文字を取ってくるか判定しています. Reference.fsxreplicateMベキ集合で検索すれば対応する関数のリスト版実装があります. これを参考にHaskellコードの移植を考えてもいいでしょう.

077 A - 01 Matrix

解説

アルゴリズムを考えるのが大変なだけで, 公式解説通りに素直に実装すればよいでしょう.

F#の文字列連結は単純な+でよく, 連続した文字からなる文字列はString.init (W-A) (fun _ -> "1")で作れます. List.iterfor文で順次stdout.WriteLineしても構いません. あえて文字列のリスト(や配列)を作りたければ, 例えば次のような形でB行とH-B行分の文字列を生成すればいいでしょう.

1
2
3
let solve H W A B =
  List.init B (fun _ -> String.init A (fun _ -> "0") + String.init (W-A) (fun _ -> "1"))
  @ List.init (H-B) (fun _ -> String.init A (fun _ -> "1") + String.init (W-A) (fun _ -> "0"))

F#でのリストの連結はList.appendまたは@演算子です.

TODO

  • 配列やシーケンスにしたときどれだけ速度が変わるか?

078 C - K-th Substring

解説

公式解説の方針そのままの実装を考えます. 条件をみたす部分文字列をどう作るかがポイントで, 今回の条件ではとにかく手当たり次第に作ると諦めるのが肝心です.

文字列の各i番目からi+K-1番目までの部分文字列を作り倒すには, 例えば次のように書けばよいでしょう.

1
2
3
4
5
  let S,K = "aba",4
  let n = S.Length - 1
  [|0..n|] |> Array.map (fun i -> [|i..(min n (i+K-1))|] |> Array.map (fun j -> S.[i..j]))

  // val it: string[][] = [|[|"a"; "ab"; "aba"|]; [|"b"; "ba"|]; [|"a"|]|]

この返り値はもちろん文字列の配列の配列で, 型はstring[][]です. これをフラットにする, つまりstring[]にするにはArray.concatを作用させればよいです. しかし標準でArray.mapの結果をArray.concatしてくれる関数Array.collectがあるため, 素直にこれを使えばいいでしょう.

1
2
3
4
5
  let S,K = "aba",4
  let n = S.Length - 1
  [|0..n|] |> Array.collect (fun i -> [|i..(min n (i+K-1))|] |> Array.map (fun j -> S.[i..j]))

  // val it: string[] = [|"a"; "ab"; "aba"; "b"; "ba"; "a"|]

上の出力を見るとわかるように"a"が二つ出てきます. あとはこの重複をArray.distinctで潰し, Array.sortしてK番目を取れば終わりです.

079 D - Handstand

解説

これも基本方針は公式解説です. 自力実装ではまり倒したため, 今回はPythonコードを参考にしました.

まずは前後で文字が切り替わる番地を取得しましょう. シンプルなのは次のようなfold処理です.

1
2
3
    ([0], [|0..N-2|])
    ||> Array.fold (fun acc i -> if S.[i]=S.[i+1] then acc else (i+1)::acc)
    |> List.rev |> List.toArray

公式解説の後半ではいくつかややこしい条件分岐処理があります. これをシンプルに処理するために番地にNを追加します.

1
2
3
4
5
  let Ia =
    ([0], [|0..N-2|])
    ||> Array.fold (fun acc i -> if S.[i]=S.[i+1] then acc else (i+1)::acc)
    |> fun xs -> N::xs
    |> List.rev |> List.toArray

これで公式解説で言うi_kを要素とする配列が作れました. あとはX_kの配列を作れば終わります.

配列を作ってからArray.maxを作っても構いません. ただ今回は添字に関する処理をしながら逐次最大値を計算していくだけで求める値が得られるため, はじめからfoldを使って処理します. 配列IaNを追加していていわば余計な項を含んでいます. このため添字に関してやや面倒な処理が必要です. 具体的には次のように書きます.

1
2
3
4
5
  let l0 = Array.length Ia - 1
  (0, [|0..l0-1|])
  ||> Array.fold (fun acc i ->
    let j = min (i+2*K + (int S.[Ia.[i]] - int '0')) l0
    max acc (Ia.[j]-Ia.[i]))

foldIa自身ではなくIaの添字の配列で回します. Nの追加があるため[|0..l0-1|]とループはIaの最後まで回らないようにします. ポイントはjの構成です. 解説のS_{i_k} = 0 or 1での添字の変化はint S.[Ia.[i]] - int '0'で対応します. さらに「k>rならi_k=N+1」の処理がまさにmin hoge l0の部分です. これで添字を作ればIa.[j]で必要な値が取れます.

TODO

080 C - Remainder Reminder

解説

公式解説通りに素直に実装します. 強いて言うならmapしてからsumではなく, 一気にsumByしてしまうと少し速くなります. 例えば次のように書くといいでしょう.

1
2
let solve N K =
  if K=0L then N*N else [|1L..N|] |> Array.sumBy (fun b -> (N/b) * max 0L (b-K) + max (N%b-K+1L) 0L)

081 C - Boxes and Candies

解説

公式解説通りに素直に実装します. 競プロと言えどプログラミングである以上, 簡潔さと明確さを兼ね備えてほしいため, 条件分岐をどうすっきりまとめるかが焦点です. 特に今回はややこしい条件分岐はmaxでまとめられます. 私自身, 執筆時点でまだまだ不慣れな部分です.

さっと正解を書けたとしても, 他の人, 特にショートコードを書く人達のコードをいくつか眺めると勉強になります.

結論としては次のように書けばよいでしょう.

1
2
3
4
let solve N x Aa =
  ((0L,0L),Aa)
  ||> Array.fold (fun (acc,a0) a -> let c = max (a0+a-x) 0L in (acc+c, a-c))
  |> fst

総和がほしいタイプのループ処理だからfoldですっきり書けます.

082 C - Different Strokes

解説

公式解説通りに素直に実装します.

今回入出力は次のように処理する前提で解説します.

1
2
3
let N = stdin.ReadLine() |> int
let Xa = Array.init N (fun _ -> stdin.ReadLine().Split() |> Array.map int64 |> fun x -> x.[0],x.[1])
solve Xa |> stdout.WriteLine

降順ソートはArray.sortDescendingArray.sortByDescendingがあり, 今回は後者を使えばよいでしょう.

次は和を取る部分です. 配列はF#流の0はじまりとします. 公式解説では別途b_iの和を取っておくような形になっていました. しかし配列の添字が偶数ならa_iを, 奇数なら-b_iを足すようにすればb_iの和を別途用意する必要はありません.

問題は配列の添字をどう用意するかです. Array.indexedで元の配列Xaを添字づけてから処理する方法もあれば, 添字の配列でArray.foldArray.sumByを回す方法もあります. ここでは次のようにArray.foldで添字を積む方法を取ります.

1
2
3
4
5
let solve Xa =
  Xa
  |> Array.sortByDescending (fun (a,b) -> a+b)
  |> Array.fold (fun (acc,i) (a,b) -> let c = if i%2=0 then a else -b in (acc+c, i+1)) (0L,0)
  |> fst

Array.foldで持ち回る変数として和のaccだけではなく添字のiも積みます. 添字にあたるiを削るため最後にfstで和だけを取っています.

Array.foldに食わせるラムダでlet c = hoge inを使っています. let inをうまく使うと一行でも見やすく書けて便利です. もちろん二行に分けて書いても構いません.

083 A - Darker and Darker

参考

解説

破壊的な実装

公式解説通りに実装します. 今回のポイントは破壊的・非関数型的な実装です. F#には独自のキュー(関数型のキュー)がなく, .NETの破壊的なキューしかありません. 関数型のキューを自前実装するのも面倒です. 余程の強烈なこだわりでもない限り素直に.NET実装を使えばいいでしょう.

念のため書いておくと, いわゆる関数型言語でも効率が必要な場合は破壊的な実装を使います. 関数の内部など影響範囲が確実に限定されていれば問題はなく, それを突き詰めて関数とアクションの分離にまで持ち上げたのがHaskellです. AtCoderでHaskellの実装を見るとシンプルな実装はしづらいようです.

さらについでに書いておくと, 少なくともAtCoder上のOCaml勢は破壊的な実装を厭いません. 困ったらOCaml勢の実装も参考にしましょう. 今回も解説用のコードはOCamlを参考にしました.

入出力

入出力とその変数は次のようにします.

1
2
3
4
5
let solve H W Aa = "これから実装"

let H,W = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Aa = Array.init H (fun _ -> stdin.ReadLine())
solve H W Aa |> stdout.WriteLine

変数の初期化

まずはキューを初期化しつつ, フラグ管理・操作回数管理用の配列を作ります. F#では配列の配列ではなく二次元の配列があり, 今回は二次元の配列が便利そうなのでこちらを使います.

1
2
3
  let q = System.Collections.Generic.Queue<int*int>()
  let Da = Array2D.init H W (fun _ _ -> -1)
  Aa |> array2D |> Array2D.iteri (fun i j c -> if c='#' then Da.[j,i] <- 0; q.Enqueue(j,i))

Daが本体のループでゴリゴリ書き換える変数です. そもそもとしてF#の配列は破壊的なデータ型で, mutableをつけなくてもDa.[j,i] <- 0で破壊的に変更できます. Da.-1, #0で初期化しています. 最終的に何回目の処置で黒に変わったかを表す二次元配列で, -1はまだ書き換えできていない状態を表します.

さらに#の場所もキューに積みます. ここでAai,jはそれぞれy座標とx座標で混乱するため, 上記のDaqは添字を入れ変えてx,yと書けるようにしています.

本体のループ処理

let mutable ans = 0で最終的に返す変数を宣言します. あとは適宜qに積みつつqが尽きるまでループします. qに値があるかどうかはプロパティq.Countで判定できます.

1
2
3
4
  let mutable ans = 0
  while q.Count <> 0 do
    "いろいろな処理"
  ans

いろいろな処理の部分を考えます. 何はともあれqから値をポップします.

1
    let x0,y0 = q.Dequeue()

.NETではq.Dequeue()です. ポップした値を中心にして周囲4マスの値を確認します. 周囲4マス分の移動を表す配列Maを用意します.

1
  let Ma = [|(-1,0);(0,-1);(1,0);(0,1)|]

さらに配列の範囲外参照と訪問状態をチェックする関数を準備します.

1
  let isWhite x y (Da:int[,]) = 0<=y && y<H && 0<=x && x<W && Da.[x,y]=(-1)

ここで白(.)かどうかの判定は初期値ではなく書き換えたかどうかを判定したいため, 入力のAaではなくDaの値で判定します. 一緒に配列の範囲内か確認するべく値Da.[x,y]ではなく配列自体を渡しています.

この二つを使って次のように周辺4マスの状態を確認しつつ値を更新します.

1
2
3
4
    let x0,y0 = q.Dequeue()
    Ma |> Array.iter (fun (dx,dy) ->
      let x,y = x0+dx, y0+dy
      if isWhite x y Da then ans <- Da.[x0,y0]+1; Da.[x,y] <- ans; q.Enqueue(x,y))

Maの各値をArray.iterで参照します. 周囲4マスのどれかを表すx,yを準備してx,yの値が白(.)かどうかを判定します. 点x,yがまだ白ならDa.[x0,y0]+1Da.[x,y]を更新します. 最後に新たに黒(#)にしたフラグ立てとしてq.Enqueue(x,y)でキューにx,yを積みます.

途中ansをバリバリ書き換えているためこれで最終的な解答になるか微妙な不安がありますが, 都度キューに積んだ値を見てループしているため, これで常に最新の変更回数が取れています.

084 C - Shopping Street

三つの実装

まずはユーザー解説にあったビットマスクを使った実装を紹介します. しかし執筆時点でビットマスクに慣れていないためにいま一つ腑に落ちず, Haskell実装を参考にビットマスクではな真偽値の形で全パターンを作る実装を確認しました. この実装は簡潔な割に見通しもよいコードで何をどうすればいいかようやく見えました. そこでさらにHaskell実装を参考にパターン網羅部分をビットマスクに書き換える処理を実装しました.

ビットマスクに慣れていなければ解説2の実装を読むといいでしょう. 最後に解説3でビットマスク化したときの実装を載せています.

解説1: ビットマスクを使う方法

解説する実装はどちらかと言えば関数型らしい実装にしています. しかしfor文による処理の方がわかりやすいかもしれません. 本質的に全く同じ処理をしているため, 読みやすい方で実装を確認してください.

入力

F_{ijk}j,kは指示の上で曜日と午前午後でわかれています. しかしこれは時間指定用パラメーターが10個あると思って処理した方が便利です. 特にループ用に[|0..9|]の意図を明確にする変数としてjkNum = 10-1を用意します.

さらに次のように入力を取得して変数に束縛します.

1
2
3
4
5
6
let solve N (Fa:int[][]) (Pa:int[][]) = "処理"

let N = stdin.ReadLine() |> int
let Fa = Array.init N (fun _ -> stdin.ReadLine().Split() |> Array.map int)
let Pa = Array.init N (fun _ -> stdin.ReadLine().Split() |> Array.map int)
solve N Fa Pa |> stdout.WriteLine

全体構成

全パターンを総なめして最大値を取ります. 以下のコードでは0はじまりで配列を作っているため, 総パターン数にあたるtotal2^10-1とします. 最大値を取るため, たたみ込むループとみなせ, 大枠は次のfoldです.

1
2
3
4
  let total = (1 <<< 10) - 1
  let jkNum = 10-1
  (-System.Int32.MinValue, [|0..total|])
  ||> Array.fold (fun acc i -> "何かの処理")

ループはビットマスクで回します.

各パターンのチェック

foldの内部を考えます. 各開店パターンごとに店が両方営業しているか調べ, 両方開いていれば利益を計算します. 特に開店している時間帯の個数をcとして積みます. c=0は一つも開店しない除外パターンだから最大値は更新しません. それ以外の場合は利益を計算して最大値候補を比較して更新します.

iとお姉ちゃんが両方開店している時間帯の個数に対する配列をCaとすれば, fold内部は次のように書けます.

1
2
3
4
5
6
7
  let total = (1 <<< 10) - 1
  let jkNum = 10-1
  (-System.Int32.MinValue, [|0..total|])
  ||> Array.fold (fun acc n ->
    let (c, Ca) = "適当に計算"
    if c=0 then acc
    else let p = (0, [|0..N-1|]) ||> Array.fold (fun acc jk -> acc + Pa.[jk].[Ca.[jk]]) in max acc p)

最後のelse let p = ...は問題文での利益計算そのままです. あとは店ごとに両方開店している時間帯の個数を数えれば終わりです.

開店時間帯の個数計算

F_{ijk}に関する処理です. 店舗数はNで開店時間帯の総和を求めたいため, Ca0埋めしたArray.zeroCreate Nで初期化し, 時間指定の10個のループを回します.

1
2
3
4
5
6
  ||> Array.fold (fun acc n ->
    let (c, Ca) =
      ((0, Array.zeroCreate N), [|0..jkNum|])
      ||> Array.fold "適当な関数"
    if c=0 then acc
    else let p = (0, [|0..N-1|]) ||> Array.fold (fun acc jk -> acc + Pa.[jk].[Ca.[jk]]) in max acc p)

いま見たいのはfun acc n -> hogeの部分であるため, 以下そこだけ抜き出しましょう.

1
2
3
    let (c, Ca) =
      ((0, Array.zeroCreate N), [|0..jkNum|])
      ||> Array.fold "適当な関数"

foldで積み回す変数は(c, Ca)として, ループの変数はjkにします.

1
2
3
    let (c, Ca) =
      ((0, Array.zeroCreate N), [|0..jkNum|])
      ||> Array.fold (fun (c, Ca) jk -> "処理")

次にビットマスクに関する変数nを処理します. 基本はn >>> jkで, 開店状況を取るためにさらに&&& 1をかませます. 該当時間帯で開店していればlet bit = n >>> jk &&& 1が立つため, bit = 1なら開店状況を更新します. もちろん開店している店の個数に関するcも同時に更新する必要があります. まとめると次のように書けます.

1
2
3
4
      ((0, Array.zeroCreate N), [|0..jkNum|])
      ||> Array.fold (fun (c, Ca) jk ->
        let bit = n >>> jk &&& 1
        (c+bit, if bit=0 then Ca else Ca |> Array.mapi (fun i t -> t+Fa.[i].[jk])))

最後のタプルを作る部分が一行にまとめると読みづらいなら適当に書き換えるといいでしょう.

1
2
3
4
5
6
7
8
      ((0, Array.zeroCreate N), [|0..jkNum|])
      ||> Array.fold (fun (c, Ca) jk ->
        let bit = n >>> jk &&& 1

        if bit = 0 then (c+bit, Ca)
        else
          let Ca1 = Ca |> Array.mapi (fun i t -> t+Fa.[i].[jk])
          (c+bit, Ca1))

補足: ビット演算

私自身, この記事の執筆時点で全くビット演算に慣れていません. どういう計算になっているか確認したければ次のコードを実行して出力を見るといいでしょう.

1
2
3
4
let n = 5
let Ni = 1 <<< n - 1
let Nj = n - 1
[| for i in 0..Ni do for j in 0..Nj do (i, j, i>>>j, i>>>j &&& 1)|] |> Array.iter (printfn "%A")

解説2: ビットマスクの代わりに全開店状態の真偽値を生成して確認する

ビットマスクに慣れていないため, 比較用に真偽値で開店状態を全て生成する実装も試してみました. Haskellを参考にしています.

1024通りの真偽値を生成

F#でのリスト版replicateMを実装して, Oaとして開店状態の真偽値を表す配列を生成しました.

1
2
3
4
  let replicateM n xs =
    let k m acc = List.collect (fun y -> List.collect (fun ys -> [y::ys]) acc) m
    List.foldBack k (List.replicate n xs) [[]]
  let Oa = replicateM 10 [false;true] |> List.toArray |> Array.map (List.toArray)

Faを真偽値に変換

さらにHaskell実装と同じくFa0,1から真偽値に変換します.

1
  let Ga = Fa |> Array.map (Array.map ((=) 1))

処理の大枠のfoldの構成

解説1と同じくfoldでたたみ込めばよいため, 大枠は次のように書けます.

1
2
  (System.Int32.MinValue, Oa)
  ||> Array.fold (fun acc opn -> "利益計算して最大値を更新")

foldの内部

opn[|false; false; false; true; true; false; false; false; true; false|];のような1024通りの開店状況のうちの一つです. まず一つはお店が開いていなければならないため, 少なくとも一つはtrueが必要です. 特に次のような分岐処理が入ります.

1
2
3
  (System.Int32.MinValue, Oa)
  ||> Array.fold (fun acc opn ->
    if Array.exists id opn then max acc "利益計算" else acc)

利益計算

あとは利益計算処理を書けば終わりです. この準備のもとでF_{ijk}から開店状況(開店店舗数)を取得し, 利益を計算する関数calを作りましょう.

上記のGaと入力のPaの各行ごとにopnと比較して計算すればよく, 特にこれらの各行がきちんと対応しているため, Array.map2で同時に各行を取得しつつ処理した結果の総和を取れば問題文のP_{1,c_1}+P_{2,c_2}+...+P_{N,c_N}が計算できます. 特に利益計算は(Ga, Pa) ||> Array.map2 (cal opn) |> Array.sumとして, cal関数を作れば十分です.

cal関数の構成

最後にcal関数の構成を考えます. まずGa(Fa)の各行gopnで渡した開店状況を比較して開店個数を取ります. 真偽値に変換しているためArray.map2で次のように書けます.

1
Array.map2 (&) opn g

これでフラグが立っている店舗数を計算すればよく, Array.sumByを使えば次のように計算できます.

1
Array.map2 (&) opn g |> Array.sumBy (fun b -> if b then 1 else 0)

これで開店店舗数nがわかったため, あとはPaの各行pに対して開店店舗数にあたるp.[n]を取れば終わりです. 特にArray.getを使えばパイプラインで次のように流せます.

1
  let cal opn g p = Array.map2 (&) opn g |> Array.sumBy (fun b -> if b then 1 else 0) |> Array.get p

最後に処理をまとめると次のように書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let solve N (Fa:int[][]) (Pa:int[][]) =
  let replicateM n xs =
    let k m acc = List.collect (fun y -> List.collect (fun ys -> [y::ys]) acc) m
    List.foldBack k (List.replicate n xs) [[]]
  let Oa = replicateM 10 [false;true] |> List.toArray |> Array.map (List.toArray)
  let Ga = Fa |> Array.map (Array.map ((=) 1))

  let cal opn g p = Array.map2 (&) opn g |> Array.sumBy (fun b -> if b then 1 else 0) |> Array.get p
  (System.Int32.MinValue, Oa)
  ||> Array.fold (fun acc opn ->
    if Array.exists id opn then max acc ((Ga, Pa) ||> Array.map2 (cal opn) |> Array.sum) else acc)

参考

replicateMfilterMなどいくつかのアクションに対して, F#のリスト版の実装をReference.fsxに収録しています.

解説3: 解説2の実装のビットマスク化

まず実装は次の通りです.

1
2
3
4
5
6
7
let solve N (Fa:int[][]) (Pa:int[][]) =
  let cal opn f p = (0,opn,f) |||> Array.fold2 (fun acc o b -> if o=b && o=1 then acc+1 else acc) |> Array.get p
  (-System.Int32.MinValue, [|0..1023|])
  ||> Array.fold (fun acc n ->
    let opn = [|0..9|] |> Array.map (fun jk -> n>>>jk &&& 1)
    if Array.exists (fun i -> i=1) opn then max acc ((Fa,Pa) ||> Array.map2 (cal opn) |> Array.sum)
    else acc)

calが少し書き換わります. map2 >> sumByではなくfold2で一気に和を計算しています. さらに真偽値ではなく0,1のままで処理を進めているため, map2 (&)の部分でo=b && o=1のような書き方が必要です.

あとのポイントはビットマスクからのopnの手動生成です. 私はこれでようやくビットマスクで何をやっているか理解できました. 私の感覚では解説1の実装より簡潔になった上に意味も把握しやすくなりました. ようやくすっきり理解できてとてもいい気分です.

085 C - Pyramid

解説

公式解説通りに素直に実装します.

入出力

1
2
3
let N = stdin.ReadLine() |> int
let Aa = Array.init N (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2])
solve Aa |> stdout.WriteLine

補助関数

l1距離を測る関数l1を定義します.

1
  let l1 (x1,y1) (x2,y2) = abs(x1-x2) + abs(y1-y2)

はじめ l1 x1 y1 x2 y2で定義したものの, 間違って右辺の出てくる順番でl1 x1 x2 y1 y2と書いてバグったため, (私にとって)間違えにくいタプルで書き直しました.

参照点の選出

Array.findで探せます.

1
  let (x0,y0,h0) = Aa |> Array.find (fun (_,_,h) -> h<>0)

F#のnot equala <> bです. ちなみにHaskellではa /= bです.

問題の条件によって必ず条件をみたす点が存在するためArray.tryFindなどを使う必要はありません.

全探索

まず中心のデータを一気に生成します.

1
2
  [| for x in 0..100 do for y in 0..100 do x,y |]
  |> Array.map (fun (x,y) -> x, y, h0 + l1 (x,y) (x0,y0))

あとは再びArray.findで条件をみたす要素を探します. これも必ず, それも一意的に存在するとわかっているためArray.tryFindなどで保険をかける必要はありません.

全ての入力が条件をみたすか確認する必要があるため, Array.findの中でAaに対するチェックのループが走ります.

1
2
3
4
  [| for x in 0..100 do for y in 0..100 do x,y |]
  |> Array.map (fun (x,y) -> x, y, h0 + l1 (x,y) (x0,y0))
  |> Array.find (fun (x,y,h) ->
    Aa |> Array.forall (fun (xi,yi,hi) -> hi = max 0 (h - l1 (x,y) (xi,yi))))

最後に返り値の数値のタプルからsprintfで文字列を生成しましょう.

1
2
3
4
5
6
7
8
let solve Aa =
  let l1 (x1,y1) (x2,y2) = abs(x1-x2) + abs(y1-y2)
  let (x0,y0,h0) = Aa |> Array.find (fun (_,_,h) -> h<>0)
  [| for x in 0..100 do for y in 0..100 do x,y |]
  |> Array.map (fun (x,y) -> x, y, h0 + l1 (x,y) (x0,y0))
  |> Array.find (fun (x,y,h) ->
    Aa |> Array.forall (fun (xi,yi,hi) -> hi = max 0 (h - l1 (x,y) (xi,yi))))
  |> fun (x,y,h) -> sprintf "%d %d %d" x y h

086 D - Coloring Dominoes

入出力

共通の入出力です.

1
2
3
4
let N = stdin.ReadLine() |> int
let S1 = stdin.ReadLine()
let S2 = stdin.ReadLine()
solve N S1 S2 |> stdout.WriteLine

解説1

再帰で素朴に実装します.

補助関数

まずは補助関数を準備します.

1
2
3
  let MOD = 1_000_000_007L
  let (.*) x y = (x*y)%MOD
  let isVertical i = S1.[i]=S2.[i]

MODはまさに余りを取るための数です. .NETでは数値を_で区切って読みやすく書けます. Int32でも問題ないとは思うものの, 念のためLをつけてInt64にします.

let (.*)と括弧をつけると演算子が定義できます. 計算結果を%で処理し忘れないように全てこの演算子で計算します.

あと公式解説でいうX(縦並び)判定用の関数としてisVerticalを用意しました.

再帰関数

まず先頭から確認をはじめます. 縦が一致していたら3をかけて1進め, そうでなければ横の並びと判定して6をかけて2進めます. 最後まで来たら積んできた値を返します. したがって次の処理ではじめればいいでしょう.

1
2
3
4
5
6
7
  let rec frec acc i =
    if i=N then acc
    elif i=0 then if isVertical i then frec (acc.*3L) (i+1) else frec (acc.*6L) (i+2)
    else
      "残りの処理"

  frec 1L 0 // 呼び出し

あとは地道に各iに対して前後を比べて処理します. 公式解説の判定をミスなく実装するだけです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  let rec frec acc i =
    if i=N then acc
    elif i=0 then if isVertical i then frec (acc.*3L) (i+1) else frec (acc.*6L) (i+2)
    else
      match isVertical (i-1), isVertical i with
        | true,true   -> frec (acc.*2L) (i+1)
        | false,true  -> frec acc       (i+1)
        | true,false  -> frec (acc.*2L) (i+2)
        | false,false -> frec (acc.*3L) (i+2)
  frec 1L 0

読みづらくも書きにくくもなく, 程々の長さの実装で問題はないでしょう.

解説2

公式解説を参考にHaskell実装を参考に実装します.

方針

大事なのは縦並びか横並びかで, 文字が続くかどうかを判定すればよく, いちいち二つの文字列を比べなくても一つの文字列だけ見れば判定できます. 一つの文字列を見て縦並び・横並びを判定したリストを作っておいて, 前後のペアを順に確認すれば目的の処理が完遂できます.

本質的には変わりませんが, こちらは再帰ではなくfoldで実装します.

補助関数

解説1と同じ補助関数を準備します.

1
2
  let MOD = 1_000_000_007L
  let (.*) x y = (x*y)%MOD

文字列の処理

F#のList.groupと違い, HaskellのData.List.groupは文字列を先頭から見て同じ文字が続く限りグループ化する関数です. 具体的には次のような挙動を取ります.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#r "nuget: FsUnit"
open FsUnit

    let rec group = function
      | [] -> []
      | x::xs ->
        let ys = List.takeWhile ((=) x) xs
        let zs = List.skipWhile ((=) x) xs
        (x::ys)::group zs
    group (List.ofSeq "Mississippi") |> should equal [['M'];['i'];['s';'s'];['i'];['s';'s'];['i'];['p';'p'];['i']]

ここで定義した再帰関数のgroupがHaskellのData.List.groupのF#実装です. これを使って文字のリストに分割し, 内部の各リストの長さを取れば1のときは縦並び, 2のときは横並びです.

1
  let patterns = S1 |> Seq.toList |> group |> List.map (List.length)

サンプルの実行結果は次の通りです.

1
2
3
4
5
6
#r "nuget: FsUnit"
open FsUnit

let S1 = "RvvttdWI"
S1 |> Seq.toList |> group |> should equal [['R'];['v';'v'];['t';'t'];['d'];['W'];['I']]
S1 |> Seq.toList |> group |> List.map (List.length) |> should equal [1;2;2;1;1;1]

大枠

先程定義したpatternsを処理します. 初項によって初期値は36か変わります. 縦か横かは既に判定済みなため, 前後のペアをList.pairwiseで素直に作って順次確認すれば十分です. これをまとめると次のようにfoldが書けます.

1
2
3
  let patterns = S1 |> Seq.toList |> group |> List.map (List.length)
  let hp = List.head patterns
  List.pairwise patterns |> List.fold f (if hp=1 then 3L else 6L)

関数fは解説1と本質的に同じで次のように書けます.

1
2
3
4
5
  let f acc = function
    | (1,1) -> acc.*2L
    | (1,2) -> acc.*2L
    | (2,1) -> acc
    | _     -> acc.*3L

全体をまとめましょう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let solve N (S1:string) =
  let MOD = 1_000_000_007L
  let (.*) x y = (x*y)%MOD

  let rec group = function
    | [] -> []
    | x::xs -> let ys = List.takeWhile ((=) x) xs in let zs = List.skipWhile ((=) x) xs in (x::ys)::group zs
  let f acc = function
    | (1,1) -> acc.*2L
    | (1,2) -> acc.*2L
    | (2,1) -> acc
    | _     -> acc.*3L

  let patterns = S1 |> Seq.toList |> group |> List.map (List.length)
  let hp = List.head patterns
  List.pairwise patterns |> List.fold f (if hp=1 then 3L else 6L)

let N = stdin.ReadLine() |> int
let S1 = stdin.ReadLine()
solve N S1 |> stdout.WriteLine

087 D - Friend Suggestions

入出力

1
2
3
4
let N,M,K = stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2]
let Xa = Array.init M (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
let Ya = Array.init K (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N M K Xa Ya |> Array.map string |> String.concat " " |> stdout.WriteLine

解説

練習も兼ねて(破壊的な)Union-Findを簡易実装して対応します. 提出された解答を眺めていたら, 少なくともRustではpetgraph crateがあってAtCoderでも使えるようです.

破壊的な簡易Union-Find

クラスとしての実装はUnionFind.fsxに置いてあります.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  type UnionFind = { par: int[]; size: int[]}
  let uf = { par = Array.init N id; size = Array.create N 1 }

  let rec root x =
    if uf.par.[x] = x then x
    else let r = root uf.par.[x] in uf.par.[x] <- r; r
  let find x y = root x = root y
  let unite x y =
    let rx,ry = root x, root y
    if rx=ry then false
    else
      let large,small = if uf.size.[rx]<uf.size.[ry] then ry,rx else rx,ry
      uf.par.[small] <- large
      uf.size.[large] <- uf.size.[large]+uf.size.[small]
      uf.size.[small] <- 0
      true
  let size x = let rx = root x in uf.size.[rx]

入力の変換

直接的な友達・ブロックの隣接行列を作りながらUnion-Find木を作ります. 隣接行列は.NETのResizeArray()Addでゴリゴリと破壊的に作ります.

1
2
3
4
5
6
7
8
  let Fa = Array.init N (fun _ -> ResizeArray<int>())
  Xa |> Array.iter (fun (a0,b0) ->
    let a,b = a0-1,b0-1
    Fa.[a].Add(b); Fa.[b].Add(a); unite a b |> ignore)
  let Ba = Array.init N (fun _ -> ResizeArray<int>())
  Ya |> Array.iter (fun (c0,d0) ->
    let c,d = c0-1,d0-1
    Ba.[c].Add(d); Ba.[d].Add(c))

Faが友達(friends), Baがブロックの配列です. 入力のXaの処理の中でunite a bをかませてUnion-Find木を作っています.

最終的な計算

公式解説通り次の量を計算します.

1
2
3
(頂点 i の連結成分のサイズ)
− (頂点 i と頂点 j が同じ連結成分に含まれて、人 i と人 j が友達関係もしくはブロック関係にある j の数)
− 1

各頂点iに対して連結成分のサイズはsize i, 友達関係にある頂点jの数はFa.[i].Countで計算できます. ブロック関係にある頂点jの数はUnion-Findfindを使って次の処理で計算できます.

1
    let blocks = Ba.[i].ToArray() |> Array.sumBy (fun b -> if find i b then 1 else 0)

あとは各頂点ごとの計算を次のようにArray.mapで処理します.

1
2
3
4
  [|0..N-1|]
  |> Array.map (fun i ->
    let blocks = Ba.[i].ToArray() |> Array.sumBy (fun b -> if find i b then 1 else 0)
    size i - Fa.[i].Count - blocks - 1)

まとめ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type UnionFind = { par: int[]; size: int[]}
let solve N M K Xa Ya =
  let uf = { par = Array.init N id; size = Array.create N 1 }

  let rec root x =
    if uf.par.[x] = x then x
    else let r = root uf.par.[x] in uf.par.[x] <- r; r
  let find x y = root x = root y
  let unite x y =
    let rx,ry = root x, root y
    if rx=ry then false
    else
      let large,small = if uf.size.[rx]<uf.size.[ry] then ry,rx else rx,ry
      uf.par.[small] <- large
      uf.size.[large] <- uf.size.[large]+uf.size.[small]
      uf.size.[small] <- 0
      true
  let size x = let rx = root x in uf.size.[rx]

  let Fa = Array.init N (fun _ -> ResizeArray<int>())
  Xa |> Array.iter (fun (a0,b0) ->
    let a,b = a0-1,b0-1
    Fa.[a].Add(b); Fa.[b].Add(a); unite a b |> ignore)
  let Ba = Array.init N (fun _ -> ResizeArray<int>())
  Ya |> Array.iter (fun (c0,d0) ->
    let c,d = c0-1,d0-1
    Ba.[c].Add(d); Ba.[d].Add(c))

  [|0..N-1|]
  |> Array.map (fun i ->
    let blocks = Ba.[i].ToArray() |> Array.sumBy (fun b -> if find i b then 1 else 0)
    size i - Fa.[i].Count - blocks - 1)

let N,M,K = stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2]
let Xa = Array.init M (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
let Ya = Array.init K (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N M K Xa Ya |> Array.map string |> String.concat " " |> stdout.WriteLine

088 D - XOR World

入出力

1
2
let A,B = stdin.ReadLine().Split() |> Array.map int64 |> (fun x -> x.[0],x.[1])
solve A B |> stdout.WriteLine

解説

素直な計算ではTLEする

最大で10^12個の項の計算が入るため, 次のようなごく素直な計算はTLEです.

1
let solve (A:int64) B = [|A..B|] |> Array.reduce (fun a b -> a^^^b)

ちなみに手元で計算したベンチマークは次の通りです.

1
2
3
4
5
6
7
8
9
let benchmark i =
  let N = pown 10L i
  let sw = System.Diagnostics.Stopwatch()
  sw.Start()
  let mutable x = 0L
  for i in 0L..N do x <- x^^^i
  sw.Stop()
  printfn "FOR 10^%2d: %A" i (sw.Elapsed)
for i in 0..10 do benchmark i

結果は次の通りです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FOR 10^ 0: 00:00:00.0000468
FOR 10^ 1: 00:00:00.0000048
FOR 10^ 2: 00:00:00.0000004
FOR 10^ 3: 00:00:00.0000024
FOR 10^ 4: 00:00:00.0000228
FOR 10^ 5: 00:00:00.0002399
FOR 10^ 6: 00:00:00.0022921
FOR 10^ 7: 00:00:00.0227260
FOR 10^ 8: 00:00:00.2289895
FOR 10^ 9: 00:00:02.3168958
FOR 10^10: 00:00:23.2159475

10^10の時点で既に20秒もかかっています.

実装

公式解説通りの実装は結果から言えば次の通りに書けます.

1
2
3
4
5
6
let solve A B =
  let g x = Array.get [|x;1L;x+1L;0L|] ((x+4L)%4L |> int)
  g (A-1L) ^^^ g B

let A,B = stdin.ReadLine().Split() |> Array.map int64 |> (fun x -> x.[0],x.[1])
solve A B |> stdout.WriteLine

ちなみに(x+4L)%4LA = 0への対策です. F#では(-1)%4 = -1で配列外参照が起きます. ここでは場合分けではなくmodの部分に手を入れました.

問題はgの実装です. 条件文をいろいろ書いて頑張ってもいいのですが, 以下の実験・具体例での確認を前提に上記のようにすっきり書いた方がよいでしょう.

一気通貫に確認

結論から言うと(私には)見にくくこれでは何とも言えないように思います.

ただ数が少ない場合は単純実装で簡単に確認できるため, まずは単純実装で様子を見ます.

1
2
3
[|0..16|]
|> Array.map (fun i -> sprintf "i = %2d, i%%4 = %d, sum = %2d" i (i%4) ([|0..i|] |> Array.reduce (fun a b -> a^^^b)))
|> Array.iter (printfn "%A")

結果は次の通りです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"i =  0, i%4 = 0, sum =  0"
"i =  1, i%4 = 1, sum =  1"
"i =  2, i%4 = 2, sum =  3"
"i =  3, i%4 = 3, sum =  0"
"i =  4, i%4 = 0, sum =  4"
"i =  5, i%4 = 1, sum =  1"
"i =  6, i%4 = 2, sum =  7"
"i =  7, i%4 = 3, sum =  0"
"i =  8, i%4 = 0, sum =  8"
"i =  9, i%4 = 1, sum =  1"
"i = 10, i%4 = 2, sum = 11"
"i = 11, i%4 = 3, sum =  0"
"i = 12, i%4 = 0, sum = 12"
"i = 13, i%4 = 1, sum =  1"
"i = 14, i%4 = 2, sum = 15"
"i = 15, i%4 = 3, sum =  0"
"i = 16, i%4 = 0, sum = 16"

周期性の存在を前提に見れば何とは01がポツポツ現われる程度の事情は見えます. プログラムで手を抜いてはいけないようなので具体的に確認します.

具体的に確認

公式解説を前提に次のように具体的に項を2つずつまとめて書いてみます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(0)                                                                |> should equal 0
(0^^^1)                                                            |> should equal 1
(0^^^1)^^^(2)                                                      |> should equal 3
(0^^^1)^^^(2^^^3)                                                  |> should equal 0
(0^^^1)^^^(2^^^3)^^^(4)                                            |> should equal 4
(0^^^1)^^^(2^^^3)^^^(4^^^5)                                        |> should equal 1
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6)                                  |> should equal 7
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)                              |> should equal 0
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)^^^(8)                        |> should equal 8
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)^^^(8^^^9)                    |> should equal 1
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)^^^(8^^^9)^^^(10)             |> should equal 11
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)^^^(8^^^9)^^^(10^^^11)        |> should equal 0
(0^^^1)^^^(2^^^3)^^^(4^^^5)^^^(6^^^7)^^^(8^^^9)^^^(10^^^11)^^^(12) |> should equal 12

周期4を前提に調べましょう.

  • 0: はじめは問答無用で0
    • もしくは0に対して新たに0をXORするから0
  • 1: 01をXORで足すから1
  • 2: 1にXORで2を足すから3
  • 3: 1のペアが2つできてXORは0
    • もしくは33をXORするから0

次の周期です.

  • 4: 0に対して新たに4をXORするから4
  • 5: 1のペアが3つできてXORは1
    • 4で初期化されたと思うと1のペアが1つで1
  • 6: 1に対して新たに6をXORするから7
  • 7: 1のペアが4つできてXORは0
    • もしくは77をXORするから0

次の周期です.

  • 8: 0に対して新たに8をXORするから8
  • 9: 1のペアが4つできてXORは1
    • 8で初期化されたと思うと1のペアが1つで1
  • 10: 1に対して新たに10をXORするから11
  • 11: 1のペアが4つできてXORは0
    • もしくは1111をXORするから0

もちろん一気通貫の場合と結果は同じですが, mod 4で何故どんな値が出るかはっきりしました. これをまとめたのが最初の実装です.

ついでに: 数学での実験

念のため書いておくと数学でもこの手の実験・具体例の確認はとても大事です. 具体例を確認した結果をそのまま数学的帰納法で証明に持ち込む単純な場合もあります. もっと言えば面白い具体例, 特に反例ができればそれで論文が書ける場合さえあります. 有名な予想に対して反例を提出して解決して有名になった人もあり, その論文・講演がいまでも語り草になるほどです.

Mr. Counterexampleとして世界的に名を馳せた日本人数学者として永田雅宜がいます. 私の専門だった作用素環でも荒木の場の量子論・量子統計力学からのIII型フォン・ノイマン環の構成や, パワーズによる量子統計力学を媒介にした連続無限個の$\mathrm{III}_{\lambda}$環の構成は特に有名です.

089 E - Colorful Hats 2

入出力

1
2
3
let N = stdin.ReadLine() |> int
let Aa = stdin.ReadLine().Split() |> Array.map int64
solve N Aa |> stdout.WriteLine

解説

公式解説通りに実装します.

MOD計算

計算漏れしないように演算子を定義してそれを使いましょう.

1
2
  let MOD = 1_000_000_007L
  let (.*) a b = (a*b)%MOD

MODInt32でも問題ないと思いますが, たまにオーバーフローしてはまり倒すため, 怪しそうな場合はとにかくInt64に倒します.

大枠

iごとに計算した結果をかけて積んでいけばよく, 単純にArray.foldでループを回します. 変数名は公式解説に合わせます.

積む変数は最終的な計算用の値であるTixi,yi,ziです. 前の人までの帽子の値が必要なためxi,yi,ziStateに積む必要があります. 初期値はかけ算の初期値だからTi = 1, 帽子の数は(xi,yi,zi) = (0,0,0)でよく, 特に次のように書けます.

1
2
3
  ((1L,(0L,0L,0L)), Aa)
  ||> Array.fold (fun ((t,(x,y,z)) ai) -> ("適切に埋める")
  |> fst

最終的に必要なのはtの値だからそれをfstで切り取ります. タプルを切り取るのはfstsndまでしかないため, この最後の切り取りが単純になるようにStateを構成しています.

folderの構成

まずtを更新します. filterlengthを組み合わせるのが素直な実装です. ここではsumByで次のように処理します.

1
    let ti = [|x;y;z|] |> Array.sumBy (fun w -> if w=ai then 1L else 0L)

いまはInt64tを積むため, filter >> lengthで処理する場合は最後にint64をかませる必要があります.

次は(x,y,z)の更新です. これは単純な場合分けで十分です.

1
    let xyz = if ai=x then (x+1L,y,z) elif ai=y then (x,y+1L,z) else (x,y,z+1L)

あとはこれらの値をタプルにまとめて次のステップに回します.

1
    (t.*ti, xyz)

MODつきのかけ算にするよう注意しましょう.

まとめ

ここまでの処理をまとめると次のように書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let solve N Aa =
  let MOD = 1_000_000_007L
  let (.*) a b = (a*b)%MOD
  ((1L,(0L,0L,0L)), Aa)
  ||> Array.fold (fun (t,(x,y,z)) ai ->
    let ti = [|x;y;z|] |> Array.sumBy (fun w -> if w=ai then 1L else 0L)
    let xyz = if ai=x then (x+1L,y,z) elif ai=y then (x,y+1L,z) else (x,y,z+1L)
    (t.*ti, xyz))
  |> fst

let N = stdin.ReadLine() |> int
let Aa = stdin.ReadLine().Split() |> Array.map int64
solve N Aa |> stdout.WriteLine

090 D - Even Relation

入出力

1
2
3
let N = stdin.ReadLine() |> int
let Ia = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2])
solve N Ia |> Array.iter stdout.WriteLine

解説

DFSで素直に木を走査します.

隣接リスト生成

二点間の距離の処理によって全体の処理が変わるため, そこに注意すればあとは素直に作るだけです. ここではw&&&1とビットの論理和で処理します.

1
2
3
4
  let Aa =
    (Array.init N (fun _ -> []),Ia)
    ||> Array.fold (fun Aa (u,v,w) ->
      Aa.[u-1]<-(v-1,w&&&1)::Aa.[u-1]; Aa.[v-1]<-(u-1,w&&&1)::Aa.[v-1]; Aa)

他の問題ではResizeArrayで処理したときもありますが, ここではListで処理しています. 今回せっかくなので試してみたら, 少なくとも今回のケースではListの方が速いようでした.

DFS

再帰とfoldで隣接リストを走査します. 今回はArray.zeroCreate Nで初期化した配列の値を再帰の中でゴリゴリ書き換える形で実装します.

根と初期値を適当に決める必要があります. ここではdfs関数をpiを根のインデックス, ciを子ノードのインデックス, v0,1の値として構成します. 特に次の形で計算をはじめます.

1
2
  let rec dfs pi ci v Xa = "処理を書く"
  Array.zeroCreate N |> dfs 0 0 0

dfsを実装しましょう. 本体のfold処理は次のように書きます.

1
2
3
4
5
  let rec dfs pi ci v Xa =
    // 値の書き換え処理が必要
    Array.get Aa ci
    |> List.filter (fun (i,_) -> i <> pi)
    |> List.fold (fun acc (gci,w) -> dfs ci gci (v^^^w) acc) Xa

まずArray.get Aa cidfsの引数で指定した隣接リストの子ノードを取ります. Array.filterで親のノードを除外した上でfoldの中でXaをゴリゴリ書き換えます. foldgcidfsに「子ノードの子ノード」として食わせる前提で孫(grandchild)の想定で名付けた変数です. 再帰的にdfs ci gciで呼び出して値を更新します. ここでv^^^wと排他的論理和で距離を処理しています. 親子で偶奇が違う場合だけフラグが立ちます.

最後にコメントアウトで残した書き換え処理を考えましょう. ciで渡した子ノードの値を書き換えます. ここではv^^^1として排他的論理和で値を書き換えます. 結果的にdfsは次のように書けます.

1
2
3
4
5
6
  let rec dfs pi ci v Xa =
    Array.set Xa ci (v^^^1)
    Array.get Aa ci
    |> List.filter (fun (i,_) -> i <> pi)
    |> List.fold (fun acc (gci,w) -> dfs ci gci (v^^^w) acc) Xa
  Array.zeroCreate N |> dfs 0 0 0

まとめ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let solve N Ia =
  let Aa =
    (Array.init N (fun _ -> []),Ia)
    ||> Array.fold (fun Aa (u,v,w) ->
      Aa.[u-1]<-(v-1,w&&&1)::Aa.[u-1]; Aa.[v-1]<-(u-1,w&&&1)::Aa.[v-1]; Aa)

  let rec dfs pi ci v Xa =
    Array.set Xa ci (v^^^1)
    Array.get Aa ci
    |> List.filter (fun (i,_) -> i <> pi)
    |> List.fold (fun acc (gci,w) -> dfs ci gci (v^^^w) acc) Xa
  Array.zeroCreate N |> dfs 0 0 1

let N = stdin.ReadLine() |> int
let Ia = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2])
solve N Ia |> Array.iter stdout.WriteLine

091 D - Coloring Edges on Tree

入出力

1
2
3
let N = stdin.ReadLine() |> int
let Ia = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1],x.[2])
solve N Ia |> Array.iter stdout.WriteLine

解説

公式解説ではBFSで解説していました. ここでDFSで木を走査します.

隣接リスト生成

問題の指示によってi本目の辺はa_ib_iを含むとしているため, 隣接リストを作るときに辺の番号も同時に割り振ります. ループのときの配列の添字と混同しないようにここでedgeeを採用して記述します.

1
2
3
4
  let Ga =
    ((0, Array.init N (fun _ -> List.empty)), Ia)
    ||> Array.fold (fun (e,Ga) (a,b) -> Ga.[a-1] <- (e,b-1)::Ga.[a-1]; Ga.[b-1] <- (e,a-1)::Ga.[b-1]; (e+1,Ga))
    |> snd

ここでは配列の中身をリストにしました. 問題90ではResizeArrayよりリストの方が速かったため, 何となくリストにしています. 何が速いかはきちんと検証するべきです.

DFS

再帰とfoldで隣接リストを走査します. 今回はArray.zeroCreate Nで初期化した配列の値を再帰の中でゴリゴリ書き換える形で実装します. IntMap(いわゆる辞書)を使っているHaskell実装もあります.

dfs関数には親ノード・子ノード・色と書き換える配列を渡せばよいでしょう. 大枠として次のように書けばよいはずです.

1
2
  let rec dfs pi ci color Xa = "処理"
  Array.zeroCreate (N-1) |> dfs 0 0 0

ではdfsの中身を考えましょう. まず指定した子ノードがつながっている頂点を取るためGa.[ci]を取ります. この中から親ノードを排除したいためfilterをかませます.

1
    let Cq = Ga.[ci] |> List.filter (snd >> (<>) pi)

これに対して最終的にノード番号とのタプルになるように色を順に割り振ります. dfsの引数に入っているcolor以外の色を割り当てるように色を振るため少し工夫します. 前段のfilterと合わせると要素数の処理が面倒なため, 遅延処理のSeqを使って力づくで辻褄を合わせます. 結論から言うと次のように書きます.

1
    let Cq = Ga.[ci] |> List.filter (snd >> (<>) pi) |> Seq.zip (Seq.initInfinite ((+) 1) |> Seq.filter ((<>) color))

Seq.zipseqを二つ与えてそれらをタプルにしたseqを返します.

1
2
3
#r "nuget: FsUnit"
open FsUnit
Seq.zip [1..4] [11..15] |> should equal [(1,11);(2,12);(3,13);(4,14)]

二番目のリストの最後の15が結果で消えている点に注意してください. List.zipArray.zipでは「長さが等しいリスト・配列を与えなさい」と怒られます. Seq.zipなら余計な長さの部分を切り落としてくれるため, Seq.initInfiniteで無限リストを生成し, 無限リスト中の不要な要素をfilterで切りつつ, 必要なところだけ切り出す都合のいい処理が書けます. あとはこれと入力の配列をfoldで処理します.

1
2
3
  let rec dfs pi ci color Xa =
    let Cq = Ga.[ci] |> List.filter (snd >> (<>) pi) |> Seq.zip (Seq.initInfinite ((+) 1) |> Seq.filter ((<>) color))
    (Xa, Cq) ||> Seq.fold (fun Xa (color,(e,gci)) -> dfs ci gci color (Array.set Xa e color; Xa))

fold中ではdfsの引数を子ノード・孫ノードのcigciにずらして, Cq中で指定した色と更新した配列を与えます.

最後にdfsの結果から出力用のデータを生成します. 出力では色の数も返す必要があるからです. 出力のXaから最大値を返せばよく, 例えば次のように書けばよいでしょう.

1
2
3
4
5
  let rec dfs pi ci color Xa =
    let Cq = Ga.[ci] |> List.filter (snd >> (<>) pi) |> Seq.zip (Seq.initInfinite ((+) 1) |> Seq.filter ((<>) color))
    (Xa, Cq) ||> Seq.fold (fun Xa (color,(e,gci)) -> dfs ci gci color (Array.set Xa e color; Xa))
  Array.zeroCreate (N-1) |> dfs 0 0 0
  |> fun Xa -> Array.append [|Array.max Xa|] Xa

SeqArrayもリストのcons(::)のような先頭への追加がなく, 配列同士の結合のappendしかありません. 配列にせず, 最終的に返すべき改行区切りの文字列を生成してもいいでしょう.

092 B - Unplanned Queries

入出力

1
2
3
let N,M = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Ia = Array.init M (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N M Ia |> stdout.WriteLine

解説

公式解説通りに素直に実装します. アルゴリズムを考えるのが全てで実装は簡単です. 各頂点の出現数を数えて偶奇判定するだけのシンプルなプログラムでよいため, 次のように書けば終わりです.

1
2
3
4
5
6
7
8
9
let solve N M Ia =
  (Array.zeroCreate N, Ia)
  ||> Array.fold (fun Ga (a,b) -> Ga.[a-1]<-1+Ga.[a-1]; Ga.[b-1]<-1+Ga.[b-1]; Ga)
  |> Array.forall (fun x -> x%2=0)
  |> fun b -> if b then "YES" else "NO"

let N,M = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Ia = Array.init M (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N M Ia |> stdout.WriteLine

093 D - Transit Tree Path

解説1, DFS

公式解説通りにDFSで実装します.

入出力

1
2
3
4
5
let N = stdin.ReadLine() |> int
let Aa = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> fun x -> int x.[0], int x.[1], int64 x.[2])
let Q,K = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Xa = Array.init Q (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N Aa Q K Xa |> Array.iter stdout.WriteLine

距離cだけint64にしています.

隣接リストの生成

ここまでに何度か出てきたのと同じように素直に実装すれば問題ありません.

1
2
3
  let Ga =
    (Array.init N (fun _ -> []), Aa)
    ||> Array.fold (fun Ga (a,b,c) -> Ga.[a-1] <- (b-1,c)::Ga.[a-1]; Ga.[b-1] <- (a-1,c)::Ga.[b-1]; Ga)

DFSの実装

Array.zeroCreate N |> dfs (K-1) (-1) 0Lを前提に実装すると次のようにすっきり書けます.

1
2
3
  let rec dfs v p d Da =
    Array.set Da v d
    (Da, Ga.[v]) ||> List.fold (fun Da (u,c) -> if u=p then Da else dfs u v (d+c) Da)

F#の配列は破壊的なデータ型で, Array.setunitを返すだけで書き換えた配列を返してくれるわけではないため, Array.set Da v dを別立てにしています. foldの中身は隣接リスト内の値が親ノードと一致していたら積んだ値を返し, それ以外は入力のdに対して隣接リストが持つ距離cを素直に足して積むだけです.

二点間の距離の計算

あとはKからの最短距離を素直に計算します.

1
  Xa |> Array.map (fun (x,y) -> Da.[x-1]+Da.[y-1])

まとめ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let solve N (Aa:(int*int*int64)[]) Q K Xa =
  let Ga =
    (Array.init N (fun _ -> []), Aa)
    ||> Array.fold (fun Ga (a,b,c) -> Ga.[a-1] <- (b-1,c)::Ga.[a-1]; Ga.[b-1] <- (a-1,c)::Ga.[b-1]; Ga)
  let rec dfs v p d Da =
    Array.set Da v d
    (Da, Ga.[v]) ||> List.fold (fun Da (u,c) -> if u=p then Da else dfs u v (d+c) Da)
  let Da = Array.zeroCreate N |> dfs (K-1) (-1) 0L
  Xa |> Array.map (fun (x,y) -> Da.[x-1]+Da.[y-1])

let N = stdin.ReadLine() |> int
let Aa = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> fun x -> int x.[0], int x.[1], int64 x.[2])
let Q,K = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Xa = Array.init Q (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N Aa Q K Xa |> Array.iter stdout.WriteLine

解説2: ダイクストラ法

他のF#解答を参考にダイクストラ法での実装も紹介します. もとのコードは二つポイントがあります.

  • 破壊的な実装である.
  • PriorityQueueの代わりにSetを使っている.

もとの入力をリストで処理している一方, 私は配列で実装しています. 処理系も違うため単純な速度比較はできません. どなたか入力を合わせて私のコードと速度比較してみてください. そのうち自分で実装してみようとは思っています.

入出力

1
2
3
4
5
let N = stdin.ReadLine() |> int
let Aa = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> fun x -> int x.[0], int x.[1], int64 x.[2])
let Q,K = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Xa = Array.init Q (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N Aa Q K Xa |> Array.iter stdout.WriteLine

大枠

DFSと同じで次のように書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let solve N (Aa:(int*int*int64)[]) Q K Xa =
  let Ga =
    (Array.init N (fun _ -> []), Aa)
    ||> Array.fold (fun Ga (a,b,c) -> Ga.[a-1] <- (b-1,c)::Ga.[a-1]; Ga.[b-1] <- (a-1,c)::Ga.[b-1]; Ga)
  let Da = dijkstra N (K-1) Ga
  Xa |> Array.map (fun (x,y) -> Da.[x-1]+Da.[y-1])

let N = stdin.ReadLine() |> int
let Aa = Array.init (N-1) (fun _ -> stdin.ReadLine().Split() |> fun x -> int x.[0], int x.[1], int64 x.[2])
let Q,K = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Xa = Array.init Q (fun _ -> stdin.ReadLine().Split() |> Array.map int |> fun x -> x.[0],x.[1])
solve N Aa Q K Xa |> Array.iter stdout.WriteLine

つまりDFSがdijkstraに変わっただけです.

ダイクストラ法

私はまだ一般的なダイクストラ法をきちんと理解できていません. 詳しくはアルゴリズムの本を参照してください.

今回の実装に関しては次の通りです.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let dijkstra N K (Ga: (int * int64) list []) =
  let Da = Array.create N System.Int64.MaxValue
  Da.[K] <- 0L
  let rec loop (Da:int64[]) q =
    if Set.isEmpty q then (Da,q)
    else
      let (c,v) = Set.minElement q
      let q0 = Set.remove (c,v) q
      if c <= Da.[v] then
        ((Da,q0), Ga.[v]) ||> List.fold (fun (Da,q) (bi,ci) ->
          let s = Da.[v]+ci
          if s < Da.[bi] then Da.[bi] <- s; (Da, Set.add (s,bi) q) else (Da,q))
      else (Da,q0)
      |> fun (Da,q) -> loop Da q
  loop Da (Set.singleton (0L,K)) |> fst

もとのコードはloopが完全に破壊的です. このコードもDa.[K] <- 0Lloopの中でのDa.[bi] <- sが厳密には破壊的なコードです. わざわざ非破壊的にするほどでもないため, 大目に見て実装しています.

ポイントは優先度つきキューの代わりにSetを使っている点です. キュー代わりのSetに積んだ値から最小値を取り出し, キューが尽きるまでループをくり返しています. もちろんSetでは速度は出ません. また配列のArray.setと違ってSet.addは更新したSetを返してくれる非破壊的な関数です.

ちなみに.NET6で優先度つきキューが実装されたものの, AtCoder上のF#は.NET Core 3.1.201で使えません.

TODO 確認: 解説2とオリジナルの破壊的な実装の速度比較

094 D - Blue and Red Balls

解説

数学的な組み合わせ処理とmodつきnCrの実装だけです. 後者は自分用のライブラリ・関数を用意しておくべきです.

入出力

1
2
let N,K = stdin.ReadLine().Split() |> Array.map int64 |> (fun x -> x.[0],x.[1])
solve N K |> Array.iter stdout.WriteLine

順列・組み合わせ系の関数

modつきの計算の場合, いくつかの処理が簡略化できます. 逆数を取る走査もかけ算で表せるためそれを前提にした実装です. 単純に順列はp, 組み合わせはcにしています.

1
2
3
4
5
  let MOD = 1_000_000_007L
  let p n r = let rec frec acc n r = if r=0L then acc else frec ((n*acc)%MOD) (n-1L) (r-1L) in frec 1L n r
  let rec powm x n = if n=0L then 1L else if n%2L=0L then powm (x*x % MOD) (n/2L) else (x * (powm x (n-1L)) % MOD)
  let inv a = powm a (MOD-2L)
  let c n r = ((p n r) * (inv (p r r))) % MOD

これ以外の順列・組み合わせ系の関数実装サンプルがArithmetics.fsxにあります. 必要に応じて参照してください.

iに対する計算

1
  [|1L..K|] |> Array.map (fun i -> c (N-K+1L) i * c (K-1L) (i-1L) % MOD)

まとめ

1
2
3
4
5
6
7
8
9
let solve N K =
  let MOD = 1_000_000_007L
  let p n r = let rec frec acc n r = if r=0L then acc else frec ((n*acc)%MOD) (n-1L) (r-1L) in frec 1L n r
  let rec powm x n = if n=0L then 1L else if n%2L=0L then powm (x*x % MOD) (n/2L) else (x * (powm x (n-1L)) % MOD)
  let inv a = powm a (MOD-2L)
  let c n r = ((p n r) * (inv (p r r))) % MOD
  [|1L..K|] |> Array.map (fun i -> c (N-K+1L) i * c (K-1L) (i-1L) % MOD)
let N,K = stdin.ReadLine().Split() |> Array.map int64 |> (fun x -> x.[0],x.[1])
solve N K |> Array.iter stdout.WriteLine

095 D - Xor Sum 4

解説

入出力

2^{60}int64の範囲におさまります.

1
2
3
let N = stdin.ReadLine() |> int64
let Aa = stdin.ReadLine().Split() |> Array.map int64
solve N Aa |> stdout.WriteLine

mod計算用の演算子定義

1
2
  let (.+) a b = ((a%MOD)+(b%MOD))%MOD
  let (.*) a b = ((a%MOD)*(b%MOD))%MOD

たまにはまる場合があるため, abにも都度%MODをかませています.

実装

計算するのは排他的論理和で各ビットごとに計算した結果を積めば十分です. 特に10進表記のaの各iビットは(a>>>i)%2Lで取れます. ビットごとの和が必要なため, ビットでの各i桁ごとにAa |> Array.sumBy (fun a -> (a>>>i)%2L)を計算します.

1
2
  [|0..60|]
  |> Array.map (fun i -> Aa |> Array.sumBy (fun a -> (a>>>i)%2L))

解説にあるように各ビットごとの問題のXORの総和は0の個数 * 1の個数です. 最終的には10進数としての和を取る必要があるため, 各iごとに2^iをかける必要があります. この和はfold2で次のように計算できます.

1
2
3
  let Xa = [|0..60|] |> Array.map (fun i -> Aa |> Array.sumBy (fun a -> (a>>>i)%2L))
  (0L,[|0..60|],Xa) |||> Array.fold2 (fun acc i y ->
    acc .+ ((pown 2L i) .* y .* (N-y)))

本体はiyの計算です. 解説にある0の個数 * 1の個数y .* (N-y)です. 上で書いたように, これに2^i = pown 2L iをかけています. あとはそこまでの和accに積めば総和が計算できます.

096 D - Make Them Even

共通の入出力

1
2
3
let H,W = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Ia = Array.init H (fun _ -> stdin.ReadLine().Split() |> Array.map int)
solve H W Ia |> List.iter stdout.WriteLine

解説1: 破壊的・命令型の処理

採用するアルゴリズム

たまには完全命令型の処理も紹介します. アルゴリズムは公式解説とは少し変えます. まずは各行を左から順に処理して右端に集め, 残った右端は上から処理する形に変えます. 文章でわかりにくい場合は以下の実装を見ればすぐにわかるでしょう.

ちなみに入力をいちいち書き換える形で実装しているため, REPLで実行するたび入力の価を読み込み直すのが面倒でした.

結果を保存する変数Xa

特に深い理由はありませんが, ここではResizeArray<string>としてはじめから最終的に返す文字列の形で積みます. ついでに文字列生成関数も作ります.

1
2
  let toStr i j k l = sprintf "%d %d %d %d" i j k l
  let Xa = ResizeArray<string>()

各行を左から処理する

入力の値が奇数な場合, そこを-1して書き換えつつ, 右の価に+1して書き換えます.

1
2
3
4
5
6
  for i in 0..H-1 do
    for j in 0..W-2 do
      if Ia.[i].[j]%2 = 1 then
        Ia.[i].[j] <- Ia.[i].[j]-1
        Ia.[i].[j+1] <- Ia.[i].[j+1]+1
        Xa.Add(toStr (i+1) (j+1) (i+1) (j+2))

F#のジェネレーター(?)は0..H[|0..H|]Hまで作ってくれます. PythonやRustと挙動が違うため注意してください. はじめに書いたように一筆書き形式ではなく各行は右端で処理を止めるため, 列に関して0..W-2としている点に注意してください.

他の言語では+=で簡単に+1できる部分がいちいち全て書かなくてはいけません. ただこれは2022年時点で変更可能な変数が起こしてきた事故を受け, いろいろな言語は不変な変数を導入しています. 特にF#と同じく何も書かなければ標準で不変な仕様にしている言語さえ増えています. やってほしくない処理を面倒にしてそもそも敬遠させる言語設計です. これを極端にしたのがHaskellのモナド機構です.

右端の処理

単純に右端を上から処理します.

1
2
3
4
5
  for i in 0..H-2 do
    if Ia.[i].[W-1]%2 = 1 then
      Ia.[i].[W-1] <- Ia.[1].[W-1]-1
      Ia.[i+1].[W-1] <- Ia.[i+1].[W-1]+1
      Xa.Add(toStr (i+1) W (i+2) W)

特に言うべきことはないでしょう. 強いていうならIa.[i].[W-1] <- Ia.[1].[W-1]-1の更新は不要です.

出力用処理

これも特に言うことはありません.

1
  Xa.ToArray() |> fun Xa -> Array.append [|string Xa.Length|] Xa

解説2: 公式解説に沿った実装

一筆書き経路の構成

ここでは先頭の奇数行を左から読み, 偶数行を右から読む形にします. 配列処理上は一行目が配列の零行目になるため偶奇が反転します. もちろん配列の零行目を右から読み始めても構いません.

1
2
3
4
5
  let Ja =
    Ia |> Array.mapi (fun i Ra ->
      if i%2=0 then Ra |> Array.mapi (fun j v -> ((i+1,j+1),v))
      else Ra |> Array.mapi (fun j v -> ((i+1,j+1),v)) |> Array.rev)
    |> Array.concat

反転させている部分は添字を持っていないと面倒なため, 値だけではなく添字も持たせています. ついでに添字は入力Iaの添字ではなく, 問題指示の1-originの添字に変換しています. 一筆書き仕様に変えているため, 最後にArray.concatを使って二重配列から単なる配列に変換しました.

fold処理の大枠

一筆書きのJaを使ってfoldで処理します. 値の入れ替えは文字列化してリストで記録します.

問題は値の入れ替えともとの配列の値を見た書き換え処理です. もともと偶数であったとしても隣を書き換えた結果, 奇数として処理する必要が出てきます. 入力の配列の価を書き換えずに処理するには前の項の偶奇をfoldに積みます.

これらをまとめるとfoldで取り回す値は([],true,(0,0))とすればよいでしょう. はじめの値が変更した場所を積むリスト, 次の真偽値は前の値の偶奇判定結果, 最後の値は入力の配列の添字です.

これをもとに大枠は次のように書けます.

1
2
  (([],true,(0,0)), Ja) ||> Array.fold (fun (acc,b,(i,j)) ((k,l), v) -> "適当な処理")
  |> fun (s,_,_) -> (List.length s |> string)::(List.rev s)

最終的には最小処理回数も返す必要があり, それはリストの長さであるため, 文字列化して先頭に積みます. 破壊的な処理ではResizeArrayAddで積みましたが, 今回はリストにconsで積んだため最後にList.revが必要です.

foldの中身

前の値が偶数か奇数か, 新たな値が偶数か奇数かで四通りの判断が必要です. 言葉よりも実装を見る方が速く正確でしょう.

1
2
3
4
5
6
7
  (([],true,(0,0)), Ja) ||> Array.fold (fun (acc,b,(i,j)) ((k,l), v) ->
      match (b, v%2=0) with
        | (true,true)  -> (acc,true,(k,l))
        | (true,false) -> (acc,false,(k,l))
        | (false,true) -> ((toStr i j k l)::acc,false,(k,l))
        | _            -> ((toStr i j k l)::acc,true,(k,l)))
  |> fun (s,_,_) -> (List.length s |> string)::(List.rev s)

前の値がtrue, つまり偶数だったなら変更はなくaccに値を積みません. ただしfoldで新たに来た値にその真偽を積み, 奇数だった場合は次の処理で変更を積みます.

前の値がfalseだったときを考えます. もとの値vが偶数だと変更処理が入って奇数になるため, bfalseを積む必要があります.

097 C - Base -2 Number

入出力

1
2
let N = stdin.ReadLine() |> int
solve N |> stdout.WriteLine

参考

正のnに対するn進展開とn進展開を十進展開に直す関数をライブラリに記録しています. 具体的にはArithmetics.fsxdecimalToNarynaryToDecimal関数です. 必要に応じて参照してください.

解説1: 再帰関数

大枠

うまく実装すれば処理できると思いますが, ここでは単純にN=0かどうかで場合わけします. 本体の処理は再帰関数で対応するため, 大枠は次のように実装します.

1
2
3
  let rec frec acc n = "再帰関数"
  if N=0 then [0] else frec [] N
  |> List.map string |> String.concat ""

再帰関数は数値のリストを作って, 最後にString.concatで連結します.

再帰関数

まず引数にわたってくるn2で割ってどんどん小さくします. n=0になったら積んできたリストを返せばよいため, if n=0 then accは規定路線です. あとは本体の再帰プロセスを考えます.

-2進の部分で少し工夫が必要です. 結論から書くと次のように書けます.

1
2
3
  let rec frec acc n =
    if n=0 then acc
    else let k = abs (n%2) in frec (k::acc) ((k-n)/2)

F#の%は負の数に対して負の値を返すため, absをかませて正の値にした上でaccに積みます. 次にfrecに食わせる値は(k-n)/2にしています. もちろんここは(n-k)/(-2)でいいのですが, 符号分を手計算で処理しています.

解説2: unfoldによる処理

同じ処理をunfoldで書きます. こちらは結論だけにします.

1
2
3
4
5
6
7
let solve N =
  if N=0 then [|0|]
  else N |> Array.unfold (fun k -> if k=0 then None else let m = abs(k%(-2)) in Some (m, (m-k)/2)) |> Array.rev
  |> Array.map string |> String.concat ""

let N = stdin.ReadLine() |> int
solve N |> stdout.WriteLine

unfold公式リファレンスまたはReference.fsxを参照してください.

098 C - Palindromic Matrix

はじめに

解説を書いていたら通ってはいけないコードが通っているようです. 例えばこの提出コードは次の入力が通らないもののACになっています.

1
aaa

いろいろ考えていたら混乱してきたため, 2022-12-24時点で解説は書き切らず明確なところまでで終わりにします.

入出力

1
2
3
let H,W = stdin.ReadLine().Split() |> Array.map int |> (fun x -> x.[0],x.[1])
let Ia = Array.init H (fun _ -> stdin.ReadLine())
solve H W Ia |> stdout.WriteLine

公式解説でのH,Wがともに奇数の場合の図

1
2
3
a b c b a
d e f e d
a b c b a

公式解説の補足

  • サイズ 1, 2, 4 のグループ: 解説ページの行列でa,bは四箇所, c,d,eは二箇所, fは一箇所あります. この行列(のブロック)として何箇所に現れるかをサイズと呼んでいます.
  • H,Wが奇数と奇数ではない場合, サイズ1の要素があると回文にならないため条件文に適切に反映させる必要があります.

用語

  • a,b,cなどのアルファベットを文字種と呼ぶ. アルファベットをいくつ含むかを「文字種の数」または「文字種の個数」と呼ぶ.
    • 特に"aaabbc"という入力に対して次のように定まる.
      • 文字種はa,b,c3個ある.
  • 入力の中で各アルファベットが何文字あるかを表す数を「文字の数」または「文字の個数」と呼ぶ.
    • 特に"aaabbc"という入力に対して次のように定まる.
      • 文字a3個.
      • 文字b2個.
      • 文字c1個.

基本的な考察

H,Wがどちらも偶数の場合

縦・横ともに回文を作るために必ず縦・横の鏡写しの分の四つ必要です. つまり全ての文字種の文字の個数は必ず4の倍数になっていなければなりません.

(H,W)がともに奇数の場合

公式解説で説明があった場合です. 先も図を引用した公式解説でいうfにあたる箇所, つまり回文(鏡映)の中心があり, ここは文字の個数が1だけあれば十分な場合があります. 他にも(H,W) = (1,3)でのabaaaaのように文字の個数が2の文字があっても許される場合があり 文字の個数が3の文字があっても許される場合があります.

少なくともHWのどちらが偶数の場合

ともに奇数の場合と違って鏡映の中心の文字が設定できないため, 文字の個数が奇数個の文字種があると破綻する場合があります. 例えば(H,W) = (1,4)の場合の"aaab"や, (H,W) = (1,6)の場合の"aaabbb"は不適です.

解説

前処理

入力の要素は自由に並べ替えられるため, 入力行列での文字の位置やどんな文字があるかは関係なく, 文字種の数と各文字の個数がいくつあるかを勘定すれば十分です.

1
  let Aq = Ia |> String.concat "" |> Seq.groupBy id |> Seq.map (snd >> Seq.length)

まずString.concat ""で入力の文字列を全て連結して一つの文字列にしています. Seq.groupBy idで文字種とその数をグループ化して取得します. 最後にある文字種がいくつあるかだけを取るべくSeq.map (snd >> Seq.length)をかけています.

(true,true)

小さいブロックから考えましょう.

まず(H,W) = (2,2)とします. このときありうるのは次の形だけです.

1
2
a a
a a

つまり全ての文字が一致して文字数は4です.

次に(H,W) = (4,4)とします. このときどこか一つに文字を置くと, その文字はちょうど鏡写しで必ず四つ存在します. 具体的には次のような形状です.

1
2
3
4
a b b a
c d d c
c d d c
a b b a

つまり現れる文字は常に4の倍数です. 変数m4はいったんmod 4でフィルターしていて, その結果から和による積み上げでs1s2を作っています. これらはどちらも0でなければなりません.

(true,false), (false,true)

これは縦・横が入れ替わっただけで本質的には同じです. 後者で考えましょう.

例えば(H,W) = (1,2)のような具体例を考えればわかるように, 公式解説の奇数・奇数ペアのfにあたる中心はありません. したがってただ一つだけある文字種があってはならないため, s1 = 0の条件が必要です.

次は二つだけある文字種がどれだけあってよいかを考えます. これも小さい方から具体的に考えましょう.

(H,W) = (1,2)で考えると次の形しかありません.

1
a a

次に(H,W) = (1,4)を考えると次の二通りが考えられます.

1
a a a a
1
a b b a

全て同じ文字種か, 文字種が二種類あって違う場合です.

ここで(H,W,Ia) = (1,4,[|"aaab"|])という不適格な場合を考えましょう.

ここで(H,W,Ia) = (1,6,[|"aaabbb"|])を考えます.

(false,false)

公式解説にあるブロックを引用します.

1
2
3
a b c b a
d e f e d
a b c b a

099 B - Simplified mahjong

入出力

1
2
3
let N = stdin.ReadLine() |> int
let Ia = Array.init N (fun _ -> stdin.ReadLine() |> int64)
solve N Ia |> stdout.WriteLine

解説

方針

公式解説とは違い, 次の方針で前から順に計算します.

  • iごとに自分自身でペアを作れるだけ作る.
  • iごとにあまりは一枚出るか出ないかで, これを次に持ち越す.
  • 前のiであまりがあった場合は, 積み残しとのペアを考えつつ自分自身でペアを作れるだけ作る.

次のfoldで素直に処理できます.

1
2
3
  ((0L,0L), Ia)
  ||> Array.fold (fun (acc,m) a -> "条件分岐を書く"
  |> fst

accがペアの数を積む変数でmがあまりの有無を表します. 最後に必要なのはペアの数を表すタプルの第一変数だからfstで切り出します.

fold内の条件分岐

まずあまりがなかった場合はごく単純に次のように書けます.

1
    if m=0L then let (q,r) = (a/2L,a%2L) in (acc+q,r)

次の二つはあまりがないときの分岐です. まず入力のA_i0の場合はペアを作りようがないためそのまま次に回します.

1
    elif a=0L then (acc,0L)

今度は入力のA_iが非零の値を持つため, 前からの積み残しの分を考えて計算します.

1
    else let (q,r) = ((a-1L)/2L, (a-1L)%2L) in (acc+q+1L,r))

積み残しと一つペアを作るため, 次に積み回すための商とあまりのq,ra-1から計算します. さらにa-1で計算した以上ペアがもう一つできているためペアのカウントはacc+q+1+1が必要です.

まとめ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let solve N Ia =
  ((0L,0L), Ia)
  ||> Array.fold (fun (acc,m) a ->
    if m=0L then let (q,r) = (a/2L,a%2L) in (acc+q,r)
    elif a=0L then (acc,0L)
    else let (q,r) = ((a-1L)/2L, (a-1L)%2L) in (acc+q+1L,r))
  |> fst

let N = stdin.ReadLine() |> int
let Ia = Array.init N (fun _ -> stdin.ReadLine() |> int64)
solve N Ia |> stdout.WriteLine

100 C - Vacant Seat

解説

公式解説通りに素直に実装します. F#ではforbreakがありません. それは再帰関数で処理できます. AOJのHaskellやOCamlのコードでも時々観測できます.

結論として次のように書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let rec frec l r s0 =
  let m = (l+r)/2
  stdout.WriteLine m
  let s = stdin.ReadLine()
  if s="Vacant" then ()
  else if (m%2=0 && s0=s) || (m%2=1 && s0<>s) then frec (m+1) r s0 else frec l m s0

let N = stdin.ReadLine() |> int
stdout.WriteLine 0
let s0 = stdin.ReadLine()
if s0="Vacant" then ()
else frec 0 (N-1) s0