公式

F - Second Gap 解説 by en_translator


Naive DP (Dynamic Programming) solution

Let us construct a permutation by repeatedly prepending an element.

Suppose we are now inserting \(P_i\) to \((P_{i+1}, \dots, P_{N})\). Here, we only care the relative magnitude of the values. (The idea is similar to Insertion DP.)

Let \(\{a, b\}\) denote the indices of the maximum and second maximum values before the insertion.

Depending on the magnitude of \(P_i\) among \((P_i, P_{i+1}, \dots, P_N)\), it becomes:

  • \(\{i, a\}\) if \(P_i\) is the largest;

  • \(\{a, i\}\) if \(P_i\) is the second largest;

  • \(\{a,b\}\) otherwise.

By maintaining them as a DP key and checking if the condition of \(D_i\) is satisfied on each transition step, we obtain a polynomial-time solution.

Reducing keys

In fact, we may perform DP without maintaining \(b\).

First, if \(P_i\) becomes largest or second largest, \(b\) is irrelevant.

For the other case, it remains \(\{a, b\}\), so the condition is automatically satisfied only when \(D_i = D_{i+1}\); again, \(b\) does not matter.

Therefore, we do not have to memorize \(b\) at all. Let us reorganize the condition and rewrite the DP.

The DP

Let \(dp[i][a] =\) the number of (relative magnitudes of) \((P_i, \dots, P_N)\) whose maximum value is \(P_a\).

The initial values are \(dp[n-1][n-1] = dp[n-1][n] = 1\).

The table can be filled in descending order of \(i\), according to the following transitions:

  • If \(P_i\) becomes largest

    The position \(a\) of the largest element before the insertion is \(|a - i| = D_i, a > i\), so \(a = i + D_i\).
    Thus, the transition is \(dp[i][i] += dp[i+1][i + D_i]\).

  • If \(P_i\) becomes second largest

    The position of the largest element before the insertion is \(a = i + D_i\).
    Thus, the transition is \(dp[i][i+D_i] += dp[i+1][i+D_i]\).

  • Otherwise

    There are \((N-1-i)\) cases that fall into this pattern.
    As described before, if \(D_i = D_{i+1}\), we may transition as
    \(dp[i][j] += dp[i+1][j]\) for all \(j\).

Optimization

Here, the transition costs \(O(N)\) time for each \(i\).

However, the final transition is merely multiplying a constant, and all other transitions is \(O(1)\).
Therefore, we can perform the DP in-place, and manage the “current constant factor” outside the DP table. This way, the DP runs in a total of \(O(N)\) time. (Please refer to the sample code for details.)

Alternatively, one may easily implement it using a dual segment tree or a lazy segment tree.

Sample code

For simplicity, we use the dictionary type, and spend \(O(\log MOD)\) time to compute an inverse.

from collections import defaultdict

mod = 998244353

n = int(input())
d = list(map(int, input().split()))

dp = defaultdict(int)
dp[n-1] = 1
dp[n-2] = 1
coef = 1  # Manages the global factor

for i in reversed(range(0, n - 2)):
    j = i + d[i] # 直前の最大値の位置
    v = dp[j]
    if d[i] == d[i + 1]:
        coef = coef * (n - i - 2) % mod # Transition for case 3
        add = v * pow(n - i - 2, -1, mod) % mod # Correct for the global factor
        dp[j] = (dp[j] + add) % mod # Transition for case 2
        dp[i] = (dp[i] + add) % mod # Transition for case 1
    else:
        dp.clear() # Transition for case 3; clear elements because this is in-place DP
        dp[j] = v # Transition for case 2
        dp[i] = v # Transition for case 1

ans = sum(dp.values()) % mod
print(ans * coef % mod)

This is a solution using a lazy segment tree. Note that we only need to apply element-wise addition.

from atcoder.lazysegtree import LazySegTree

mod = 998244353

n = int(input())
d = list(map(int, input().split()))

def add(x, y): return (x + y) % mod
def mul(x, y): return x * y % mod

dp = LazySegTree(add, 0, mul, mul, 1, [0] * (n - 2) + [1] * 2)

for i in reversed(range(0, n - 2)):
    j = i + d[i]
    v = dp.get(j)
    if d[i] == d[i+1]:
        dp.apply(0, n, n - i - 2)
    else:
        dp.apply(0, n, 0)
    dp.set(j, (dp.get(j) + v) % mod)
    dp.set(i, (dp.get(i) + v) % mod)

print(dp.all_prod())

投稿日時:
最終更新: