Official

C - 道路の段差補修 / Road Bump Repair Editorial by MMNMM


ブロック \(j\) より後ろ、ブロック \(i\) より前にある \(i-j-1\) 個のブロックを好きに置き換えたあとの、これらのブロックの間にある危険箇所の数を \(f(i,j)\) とします。 \(f(i,j)\) の値について考えます。

\(|D _ j-D _ i|\le T\times(i-j)\) のとき、かつそのときに限り、適切に間のブロックを交換することでこの間の危険箇所を \(0\) 箇所にすることができます。

証明

ブロック \(j\) からブロック \(i\) までの危険箇所を \(0\) 個にすることができるとき、つまり \(|D _ k-D _ {k+1}|\le T\) が \(k=j,j+1,\ldots,i-1\) に対して成り立っているとき、この不等式の両辺をそれぞれ足し合わせて \(\displaystyle\sum _ {k=j} ^ {i-1}|D _ k-D _ {k+1}|\le T\times(i-j)\) が成り立ちます。 三角不等式より \(\displaystyle|D _ j-D _ i|\le\sum _ {k=j} ^ {i-1}|D _ k-D _ {k+1}|\) が成り立つので、\(|D _ j-D _ i|\le T\times(i-j)\) が示されました。

逆に、\(|D _ j-D _ i|\le T\times(i-j)\) が成り立つなら、\(D _ k=\left\lfloor\dfrac{D _ i\times(k-j)+D _ j\times(i-k)}{i-j}\right\rfloor\ (j\lt k\lt i)\) などとすることでブロック \(j\) とブロック \(i\) の間の危険箇所を \(0\) 箇所にすることができます。

上の議論から、\(|D _ j-D _ i|\gt T\times(i-j)\) のときブロック \(j\) とブロック \(i\) の間の危険箇所を \(0\) 箇所にすることはできません。 交換したブロックの高さをすべて \(D _ j\) にすることで危険箇所をたかだか \(1\) 箇所にすることができるので、\[f(i,j)=\begin{cases}0&(|D _ j-D _ i|\le T\times(i-j))\\1&(|D _ j-D _ i|\gt T\times(i-j))\end{cases}\] となることがわかりました。

\(\operatorname{dp}[i][k]\coloneqq\) ブロック \(i\) までのブロックのうち \(k\) 個以下を交換し、ブロック \(i\) を交換しないときの、ブロック \(i\) までに含まれる危険箇所の数の最小値 \((1\le i\le N,0\le k\le K)\) とします。 求める値は \(\displaystyle\min _ {0\le k\le K}\operatorname{dp}[N-k][K-k]\) です(最後の \(k\) 個を適切に交換することで、ブロック \(N-k\) 以降の危険箇所をなくすことができます)。

直前の交換しなかったブロックがどれかについて場合分けをすることで、\(i\) の昇順に DP テーブルを埋めていくことができます。 具体的には、上の \(f(i,j)\) を使って次のように計算することができます。

\[\operatorname{dp}[i][k]=\begin{cases}0&(k\ge i-1)\\\displaystyle\min _ {1\le j\le i,0\le k-i+j+1}\bigl\lbrace\operatorname{dp}[j][k-i+j+1]+f(i,j)\bigr\rbrace&(k\lt i-1)\end{cases}\]

あとは、これに従って計算することで \(\operatorname{dp}[i][k]\) の値をすべて求めることができます。

時間計算量は \(O(NK ^ 2)\) などとなります。

実装例は以下のようになります。 番兵を両端に置くことで場合分けを減らすことができます。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int N, T, K;
    cin >> N >> T >> K;
    if (N == K) { // N = K のとき、すべてを交換することができる
        cout << 0 << endl;
        return 0;
    }

    vector<int> D(N);
    for (int& d : D) {
        cin >> d;
    }
    // 番兵を置いておく
    D.emplace(begin(D), -100000);
    D.emplace_back(100000);

    vector dp(N + 2, vector(K + 1, N + 2));
    for (int k = 0; k <= K; ++k) {
        dp[0][k] = 0;
    }
    for (int i = 1; i < N + 2; ++i) {
        for (int j = 0; j < i; ++j) { // 直前の交換しなかったブロックを全部調べて
            for (int k = i - j - 1; k <= K; ++k) {
                // DP テーブルを更新
                dp[i][k] = min(dp[i][k], dp[j][k - i + j + 1] + (abs(D[i] - D[j]) > T * (i - j)));
            }
        }
    }

    // 番兵の寄与を引いて出力
    cout << dp.back().back() - 2 << endl;
    return 0;
}
N, T, K = map(int, input().split())

if N == K: # N = K のとき、すべてを交換することができる
    print(0)
    exit()

# 番兵を置いておく
D = [-100000] + list(map(int, input().split())) + [100000]

dp = [[N + 2 for j in range(K + 1)] for i in range(N + 2)]
for k in range(K + 1):
    dp[0][k] = 0
for i in range(1, N + 2):
    for j in range(i): # 直前の交換しなかったブロックを全部調べて
        for k in range(i - j - 1, K + 1):
            # DP テーブルを更新
            dp[i][k] = min(dp[i][k], dp[j][k - i + j + 1] + (abs(D[i] - D[j]) > T * (i - j)))

# 番兵の寄与を引いて出力
print(dp[-1][-1] - 2)

posted:
last update: