Official

E - 花壇の区間選び / Choosing Flowerbed Intervals Editorial by admin

gemini-3.5-flash-thinking

概要

この問題は、長さ \(N\) の花壇の連続する区間 \([l, r]\) のうち、以下の 2 つの条件を同時に満たすものの個数を求める問題です。

  • 条件 1(品種の多様さ): 区間内の(異なる品種の数 \(D\)\(\times\)(区間の長さ \(r - l + 1\)\(\le K\)
  • 条件 2(高さのバランス): 区間内の(最大高さ)\(-\)(最小高さ)\(\le M\)

制約が \(N \le 2 \times 10^5\) と大きいため、すべての区間を個別に調べると \(O(N^2)\) 以上の時間がかかり実行時間制限に間に合いません。 この解説では、区間の「単調性」に注目し、尺取り法(Two Pointers)スライド最大値・最小値(deque)を組み合わせて \(O(N)\) で解く方法を解説します。


考察

単調性の活用

右端 \(r\) を固定したとき、左端 \(l\) を右に動かして区間 \([l, r]\) を縮めていくことを考えます。

  1. 条件 1 について 区間を縮めると、区間の長さ \((r - l + 1)\) は必ず減少します。また、含まれる品種の種類数 \(D\) も減少するか変化しません。 したがって、値 \(D \times (r - l + 1)\)\(l\) を大きくするにつれて単調に減少(または不変)します。 よって、ある \(r\) に対して条件 1 を満たす最小の左端を \(l_1\) とすると、任意の \(l \ge l_1\) について条件 1 を満たします。

  2. 条件 2 について 区間を縮めると、区間内の最大値は減少または不変、最小値は増加または不変となります。 したがって、最大値と最小値の差は \(l\) を大きくするにつれて単調に減少(または不変)します。 よって、ある \(r\) に対して条件 2 を満たす最小の左端を \(l_2\) とすると、任意の \(l \ge l_2\) について条件 2 を満たします。

求める区間の数

右端 \(r\) を固定したとき、両方の条件を同時に満たす左端 \(l\) の範囲は、 $\(\max(l_1, l_2) \le l \le r\)\( となります。このような \)l\( の個数は、 \)\text{limit} = \max(l_1, l_2)\( とおくと、 \)\text{limit} \le r\( のときに \)r - \text{limit} + 1$ 個存在します。

さらに、右端 \(r\) を右に進めたとき、条件を満たすために必要な左端の下限 \(l_1\) および \(l_2\) は、それぞれ右にしか進みません(単調非減少)。 この性質を利用して、右端 \(r\) を動かしながら、左端のポインタ \(l_1, l_2\) を尺取り法の要領で管理することができます。


アルゴリズム

右端 \(r\)\(0\) から \(N-1\) まで順番に進めながら、以下の処理を行います。

1. 条件 2(高さのバランス)の管理:ポインタ \(l_2\)

区間内の最大値・最小値を効率よく取得するために、スライド最大値・最小値deque を用いた手法)を使用します。

  • max_dq: 区間内の要素のインデックスを、値が降順になるように保持する両端キュー。
  • min_dq: 区間内の要素のインデックスを、値が昇順になるように保持する両端キュー。

新しい要素 \(B[r]\) を追加する際、キューの末尾から \(B[r]\) より「最大値/最小値として不適切な要素」を取り除いてから \(r\) を追加します。 その後、B[max_dq[0]] - B[min_dq[0]] > M(現在の最大値と最小値の差が \(M\) を超えている)である限り、左端 \(l_2\) を右に進めます。このとき、キューの先頭にあるインデックスが \(l_2\) より小さくなればキューから削除します。

2. 条件 1(品種の多様さ)の管理:ポインタ \(l_1\)

区間 \([l_1, r]\) に含まれる各品種の出現回数を配列 cnt で、異なる品種の数を変数 \(D\) で管理します。

  • \(A[r]\) を追加し、新しく登場した品種であれば \(D\)\(1\) 増やします。
  • \(D \times (r - l_1 + 1) > K\) である限り、左端 \(l_1\) を右に進めます。進める際、取り除く要素 \(A[l_1]\) の出現回数を \(1\) 減らし、出現回数が \(0\) になったら \(D\)\(1\) 減らします。

3. 答えの集計

\(r\) において、条件を満たす左端の最小値は \(\text{limit} = \max(l_1, l_2)\) です。 \(\text{limit} \le r\) であれば、区間の個数 \(r - \text{limit} + 1\) を答えに加算します。


計算量

  • 時間計算量: \(O(N)\)

    • 右端のポインタ \(r\)\(0\) から \(N-1\) まで \(1\) ずつ増加します。
    • 左端のポインタ \(l_1\)\(l_2\) も、それぞれ最大で \(N\) 回しか右に進みません。
    • deque への要素の追加・削除は、各要素について高々 \(1\) 回ずつしか行われません。
    • したがって、すべての操作が全体で \(O(N)\) で行われます。
  • 空間計算量: \(O(N)\)

    • 品種の出現回数を記録する配列 cnt(サイズ \(N+1\))と、最大値・最小値を管理する deque(最大サイズ \(N\))を使用するため、空間計算量は \(O(N)\) です。

実装のポイント

  • インデックスの管理: deque には値そのものではなく「インデックス」を格納します。これにより、左端 \(l_2\) が進んだときに、キューの先頭要素が現在の区間外(max_dq[0] < l2)になったかどうかを定数時間で判定できます。

  • オーバーフローへの注意: 条件 1 の判定式 \(D \times (r - l_1 + 1)\) は、最大で約 \(N^2 \approx 4 \times 10^{10}\) に達します。Python では自動的に多倍長整数として扱われますが、C++ などの他言語で実装する場合は、計算途中でオーバーフローを防ぐために 64 ビット整数型(long long など)を使用してください。

    ソースコード

import sys
from collections import deque

def solve():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    
    N = int(data[0])
    K = int(data[1])
    M = int(data[2])
    
    A = [int(x) for x in data[3:3+N]]
    B = [int(x) for x in data[3+N:3+2*N]]
    
    # 条件2 (高さのバランス) を管理するための deque とポインタ
    max_dq = deque()
    min_dq = deque()
    l2 = 0
    
    # 条件1 (品種の多様さ) を管理するための頻度配列とポインタ
    cnt = [0] * (N + 1)
    D = 0
    l1 = 0
    
    ans = 0
    for r in range(N):
        # 条件2の更新 (B[r] を追加)
        br = B[r]
        while max_dq and B[max_dq[-1]] <= br:
            max_dq.pop()
        max_dq.append(r)
        
        while min_dq and B[min_dq[-1]] >= br:
            min_dq.pop()
        min_dq.append(r)
        
        # 高さの差が M 以下になるまで l2 を進める
        while B[max_dq[0]] - B[min_dq[0]] > M:
            l2 += 1
            if max_dq[0] < l2:
                max_dq.popleft()
            if min_dq[0] < l2:
                min_dq.popleft()
                
        # 条件1の更新 (A[r] を追加)
        ar = A[r]
        if cnt[ar] == 0:
            D += 1
        cnt[ar] += 1
        
        # D * (r - l1 + 1) <= K を満たすまで l1 を進める
        while D * (r - l1 + 1) > K:
            al1 = A[l1]
            cnt[al1] -= 1
            if cnt[al1] == 0:
                D -= 1
            l1 += 1
            
        # 両方の条件を満たす左端の下限
        limit = l1 if l1 > l2 else l2
        if limit <= r:
            ans += r - limit + 1
            
    print(ans)

if __name__ == '__main__':
    solve()

この解説は gemini-3.5-flash-thinking によって生成されました。

posted:
last update: