0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

いもす法について【競技プログラミング】

Last updated at Posted at 2024-08-24

はじめに

  • dpについて、体系的にまとめられているという噂を聞いて、アルゴリズム実技検定(上エキ編)を買って、パラパラと眺めていたら、いもす法という言葉が出てきた(p41)。個人の考案者のハンドルネームから、そう呼ばれているとのこと。
  • いもすさんのホームページを読ませて貰い、累積和の考えを流用して、計算量を削減する方法だと分かったけど、天才肌の人っぽいからポイントのみ説明されて、余分なことは説明されてないので、自分の備忘のために余分な部分を書き加えながら、swiftで実装してみる。

いもす法による計算量削減例

  • 例題は、以下のような感じ
    • 0〜T時間の間に、お客さんがN人来店し、
    • 各お客さんi(i in 0..<N)の入店時間Siと退店時間Ei時間(0≦Si<Ei≦T)が与えられたとき、
    • そのお店にお客さんが最大何人いたかを答えなさい。
    • ただし、客iは時刻「Si」〜「Ei - 1」の間で1名とカウントされ、「Ei」の時刻ではカウントされない。
  • 例えば、入力例を下記のようにしてみる。
8 5 // 8時間に5人来店
0 3 // 客① 0時入店、3時退店
2 7
1 5
5 8
4 5
  • 上記について、時刻ごとの人数を格納する配列guest[i](i in 0...T)として、本能の赴くままにコードを書くと、以下の通り
let (T,N) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1])}[0]
var ses:[(Int,Int)] = []
for _ in 0..<N {
    let (s,e) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1])}[0]
    ses.append((s,e))
}

var guests = [Int](repeating:0,count:T+1)
for (s,e) in ses { // -- ①外側のforループ(客毎の入退店時間)
    for t in s..<e { // -- ②内側のforループ(客の滞在時間)
        guests[t] += 1
    }
}

print(guests) // [1, 2, 3, 2, 3, 2, 2, 1, 0]
print(guests.max()!) // 3
  • 上記のとおり、簡単に実装できたけど、二重ループ①②を使っているから、計算量はO(N・T)だね。
  • この計算量を、O(N+T)にするのが、いもす法。具体的には、ループ①とループ②を切り離すこととなる。
  • 配列guestを使うのは変わらないけど、ループを切り離す工夫として、累積和の考え方を使っている。
  • ループの部分のみを抜き出して書くと、具体的には、こんな感じ。
for (s,e) in ses { // -- ①旧外側のforループ(客毎の入退店時間)
    guests[s] += 1
    guests[e] -= 1
}

for t in 1...T { // -- ②累積和もどきを作成するforループ(0を飛ばしていることに注意)
    guests[t] += guests[t-1]
}
  • 上記のとおり、forループを分離したことにより、計算量はO(N+T)となる。
    • forループ分離のため、ループ①では増減のみをカウントし、ループ②で累積和もどきを計算している。
    • なぜ、「累積和もどき」と表現したかと言えば、本来の累積和の流儀では、累積和の先頭配列の中身は必ず0となり、サイズは元の配列より1大きくなるため。配列guestの先頭の要素が0でない時点で、標準的な「累積和」の作り方に沿っていない。
  • 変更後のコード全体は以下のとおり。といっても、forループ以外の部分に変更は無い。
let (T,N) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1])}[0]
var ses:[(Int,Int)] = []
for _ in 0..<N {
    let (s,e) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1])}[0]
    ses.append((s,e))
}

var guests = [Int](repeating:0,count:T+1)
for (s,e) in ses { // -- ①旧外側のforループ(客毎の入退店時間)
    guests[s] += 1
    guests[e] -= 1
}

print(guests) // [1, 1, 1, -1, 1, -1, 0, -1, -1]

for t in 1...T { // -- 累積和のforループ(0を飛ばしていることに注意)
    guests[t] += guests[t-1]
}

print(guests) // [1, 2, 3, 2, 3, 2, 2, 1, 0]
print(guests.max()!) // 3

お次は二次元配列

  • いもす法の便利な点は、二次元配列、三次元配列でも、計算量削減が出来ると言うこと。
  • 本家HPでは、二次元配列での削減効果の例として、以下が上げられている。
    • 横W×縦Hのフィールド上に、N種類のモンスターがいて、
    • 各モンスターiの出現可能領域(xi,yi)が、(xi in Ai..<Bi , yi in Ci..<Di)で与えられているとき、
    • フィールド上の地点(x,y)において、最大何種類のモンスターが現れる可能性があるか?
  • 例えば、入力例を下記のようにしてみる
8 5 4 // 横8×縦5のフィールド上に4種類のモンスター
0 8 0 5 // 全域に生息するモンスター
3 6 1 5 // 横:3..<6、縦:1..<5に生息するモンスター
1 4 2 4
2 8 3 4
  • さて、上記について、フィールド上で最大何種類のモンスターが現れるかを格納する二次元配列ms[j][i](i in 0..<W , j in 0..<H)を設定して、本の赴くままにコードを書くと、以下の通り。
let (W,H,N) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1],$0[2])}[0]
var abcds:[(Int,Int,Int,Int)] = []
for _ in 0..<N {
    let (a,b,c,d) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1],$0[2],$0[3])}[0] // 1ずらさない
    abcds.append((a,b,c,d))
}

var ms = [[Int]](repeating:[Int](repeating:0,count:W),count:H)

for (a,b,c,d) in abcds { // -- ① 外側のforループ(モンスター別の生息範囲)
    for j in c..<d { // -- ②-1 内側のy軸forループ
        for i in a..<b { // -- ②-2 内側のx軸forループ
            ms[j][i] += 1
        }
    }
}

for m in ms {
    print(m)
}
print(ms.flatMap{$0}.max()!)

///上記の出力
[1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 2, 2, 2, 1, 1]
[1, 2, 2, 3, 2, 2, 1, 1]
[1, 2, 3, 4, 3, 3, 2, 2]
[1, 1, 1, 2, 2, 2, 1, 1]
4
  • 上記コードでは、3重ループが形成されているので、計算量はO(N・H・W)となっているが、いもす法を利用することで、計算量O(N + H・W)に削減することが出来る。
  • 具体的には、ループを以下の通り分解する。
// 増減のみの記録(本家HPの図3のイメージ)
for (a,b,c,d) in abcds { // -- ① 旧外側のforループ(モンスター別の生息範囲)
    ms[c][a] += 1
    ms[c][b] -= 1
    ms[d][a] -= 1
    ms[d][b] += 1
}

//x軸に沿った累積和もどき(本家HPの図4,図5のイメージ)
for j in 0..<H { // -- ②-1 旧内側のy軸forループ
    for i in 1..<W { // -- ②-2 旧内側のx軸forループ
        ms[j][i] += ms[j][i-1]
    }
}

//y軸に沿った累積和もどき(本家HPの図6,図7のイメージ)
for j in 1..<H { // -- ②-1 旧内側のy軸forループ
    for i in 0..<W { // -- ②-2 旧内側のx軸forループ
        ms[j][i] += ms[j-1][i]
    }
}
  • 上記のイメージは、本家HP図3図4図5図6図7を参考にしてね。
  • 上記により、計算量はO(N + H・W)となった。
  • コード全体は、以下の通り。じつは、こっそりと配列msのサイズを1拡大しているよ。コレをしないと、ループ①で、インデックスが配列の範囲外になってエラーになるよ。
let (W,H,N) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1],$0[2])}[0]
var abcds:[(Int,Int,Int,Int)] = []
for _ in 0..<N {
    let (a,b,c,d) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1],$0[2],$0[3])}[0] // 1ずらさない
    abcds.append((a,b,c,d))
}

var ms = [[Int]](repeating:[Int](repeating:0,count:W+1),count:H+1) // 1ずつ拡張する

for (a,b,c,d) in abcds { // -- ① 旧外側のforループ(モンスター別の生息範囲)
    ms[c][a] += 1
    ms[c][b] -= 1
    ms[d][a] -= 1
    ms[d][b] += 1
}

for m in ms {
    print(m) // 出力①
}

/// 本来的には必要ないけど、msのサイズアップ(+1)を反映した
//x軸に沿った累積和もどき
for j in 0..<(H+1) { // -- ②-1 内側のy軸forループ
    for i in 1..<(W+1) { // ②-2 内側のx軸forループ
        ms[j][i] += ms[j][i-1]
    }
}

//y軸に沿った累積和もどき
for j in 1..<(H+1) { // -- ②-1 内側のy軸forループ
    for i in 0..<(W+1) { // ②-2 内側のx軸forループ
        ms[j][i] += ms[j-1][i]
    }
}

for m in ms {
    print(m) // 出力②
}
print(ms.flatMap{$0}.max()!) // 出力③

///出力
出力
[1, 0, 0, 0, 0, 0, 0, 0, -1]
[0, 0, 0, 1, 0, 0, -1, 0, 0]
[0, 1, 0, 0, -1, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0, 0, 0, -1]
[0, -1, -1, 0, 1, 0, 0, 0, 1]
[-1, 0, 0, -1, 0, 0, 1, 0, 1]

出力② (配列のサイズアップ部分が0となっている)
[1, 1, 1, 1, 1, 1, 1, 1, 0]
[1, 1, 1, 2, 2, 2, 1, 1, 0]
[1, 2, 2, 3, 2, 2, 1, 1, 0]
[1, 2, 3, 4, 3, 3, 2, 2, 0]
[1, 1, 1, 2, 2, 2, 1, 1, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]

出力
4

さいごに

  • いもす法って、他の人の投稿とかを見ると、難しそうなことを書いてあったけど、上記のような簡単な例であれば、それ程頭を使わずに理解できるね。
  • このくらいのアルゴリズムなら、スッキリ理解できて気持ちよいね。
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?