牆上海報 題解|APCS 2022 年 1 月高級 第 4 題


前置知識

這題是「對答案二分搜」的代表題,下面的講解直接把這些當成已知,沒讀過的先讀;同一個主題兩邊都有的,對照著讀會更快上手:

  • 對答案二分搜:NTUCPC Guide〈對答案二分搜〉——它的例題就是本題(ZeroJudge h084),「貼得完的高度是連在一起的一段」這個單調性講得很清楚;吳邦一《AP325》4.2.4「外掛二分搜」(教材 PDF 第 126~128 頁,P-4-9 基地台)是同一招:先寫「給定答案做不做得到」的判斷函式,再對答案二分。
  • 貪心的交換論證:NTUCPC Guide〈貪心法 I〉——判斷函式裡「能貼就立刻貼」為什麼不吃虧,用的是同一種說理。

簡潔題意

\(n\) 塊寬 \(1\) 的木板排成一面牆,高度 \(h_1, \ldots, h_n\)。\(k\) 張海報寬度 \(w_1, \ldots, w_k\),要依序由左到右貼、不重疊(可以留空),每張佔連續的木板,全部貼在同一個高度 \(H\),而且海報佔到的每塊木板都要 \(\ge H\)。求最大的 \(H\)(\(1 \le n \le 2 \times 10^5\),\(1 \le k \le \min(n, 5000)\),\(1 \le h_i \le 10^9\),\(\sum w_i \le n\))。

依正確通過的測試資料筆數給分,其中:

  • 子題組 1(\(20\) 分):\(n \le 100\),\(k = 1\)。
  • 子題組 2(\(40\) 分):\(k = 1\)。
  • 子題組 3(\(40\) 分):無額外限制。

先拿下 20 分:一張海報,每個位置都試

只有一張寬 \(w\) 的海報,貼在木板 \(s\) 到 \(s + w - 1\) 時最高只能貼到這 \(w\) 塊裡最矮的那塊;每個 \(s\) 都算一次最矮的、取最大。\(O(nw)\),子題組 1 的 \(n \le 100\) 瞬間。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, k;
    cin >> n >> k;                                   // 這個子題組 k = 1
    vector<int> h(n);
    for (int &x : h) cin >> x;
    int w;
    cin >> w;
    int best = 0;
    for (int start = 0; start + w <= n; start++) {   // 海報從第 start 塊木板開始貼
        int lowest = h[start];                       // 這 w 塊木板裡最矮的那塊
        for (int i = start; i < start + w; i++) lowest = min(lowest, h[i]);
        best = max(best, lowest);
    }
    cout << best << '\n';
    return 0;
}

從 20 分到 100 分:先會判斷「高度 \(H\) 貼不貼得完」,再對 \(H\) 二分搜

直接找「最大的 \(H\)」不好下手,換成好回答的問題:給定 \(H\),所有海報貼得完嗎? 高度 \(\ge H\) 的木板算「夠高」,從左到右掃,數目前連續幾塊夠高(run),遇到太矮的歸零;run 一湊滿目前這張海報的寬度就立刻貼上、run 歸零從下一塊重數。能貼就貼為什麼不吃虧:任何一種貼法,把每張海報依序往左推到最早能貼的位置,不會撞到前一張、也不會讓後面的海報更難貼,所以「最早能貼就貼」貼得完的海報數最多。

再看 \(H\) 本身:\(H\) 貼得完,所有比 \(H\) 低的高度也一定貼得完(夠高的木板只會變多);\(H\) 貼不完,更高的也貼不完。貼得完的高度是從 \(1\) 開始連在一起的一段,要的是這一段的右端——對 \(H\) 二分搜。圖 1 是範例 2:高度 \(5\) 貼得完、\(6\) 貼不完。

圖 1:範例 2 的十塊木板,高度 5 時三張海報從左到右湊滿就貼、貼得完;高度 6 時夠高的木板連不成兩塊,第一張就貼不上
#include <bits/stdc++.h>
using namespace std;

int n, k;
vector<int> h, w;

bool canPost(int height) {                           // 全部海報都貼在高度 height,貼得完嗎
    int placed = 0, run = 0;                         // run:目前連續幾塊木板的高度 >= height
    for (int i = 0; i < n && placed < k; i++) {
        if (h[i] >= height) run++;
        else run = 0;                                // 太矮的木板把連續段切斷
        if (run == w[placed]) {                      // 湊滿目前這張海報的寬度:立刻貼上
            placed++;
            run = 0;                                 // 下一張從下一塊木板重新數
        }
    }
    return placed == k;
}

int main() {
    cin >> n >> k;
    h.resize(n);
    w.resize(k);
    for (int &x : h) cin >> x;
    for (int &x : w) cin >> x;
    int lo = 1, hi = *max_element(h.begin(), h.end());   // 高度 1 一定貼得完、超過最高木板一定不行
    while (lo < hi) {
        int mid = lo + (hi - lo + 1) / 2;            // 偏右取中點:找的是「最大的可行高度」
        if (canPost(mid)) lo = mid;                  // mid 貼得完:答案至少是 mid
        else hi = mid - 1;                           // 貼不完:答案比 mid 小
    }
    cout << lo << '\n';
    return 0;
}

判斷函式 \(O(n)\),二分搜 \(\log_2 10^9 \approx 30\) 次,總共 \(O(n \log \max h)\),\(n = 2 \times 10^5\) 本站實測 \(0.12\) 秒。找「最大的可行值」時中點要偏右取(lo + (hi - lo + 1) / 2):lo 只會往上移到可行的 mid,若中點偏左,hi = lo + 1 時 mid 永遠等於 lo、迴圈停不下來。

測過再交:範例沒有順序與重疊的陷阱

兩個範例就算把海報寬度排序(範例 2 變成 1 2 2)答案也不變,也沒有「一段夠高的木板差一點就能塞兩張」的局面。沒蓋到:最小輸入、兩張海報不能重疊、順序不能調換、海報之間可以留空也可以相連、剛好等於 \(H\) 的木板也算夠高、\(h\) 頂到 \(10^9\)。

輸入 正確輸出 這一筆在測什麼
1 1 / 5 / 1 5 最小輸入
5 2 / 9 9 9 1 1 / 2 2 1 三塊高木板塞不下兩張寬 \(2\) 的:貼完一張沒把 run 歸零會讓海報重疊、印 9
6 2 / 9 9 1 9 9 9 / 3 2 1 寬 \(3\) 的要先貼:把寬度排序後先貼寬 \(2\) 的會印 9
4 2 / 5 3 5 3 / 1 1 5 海報之間可以隔著矮木板
4 2 / 7 7 7 7 / 2 2 7 海報可以緊貼、\(\sum w = n\)
2 1 / 3 3 / 2 3 木板高度剛好等於海報高度也算夠高:用 > 判斷會印 2
3 1 / 1000000000 1000000000 1000000000 / 3 1000000000 答案頂到 \(10^9\),二分搜的上界要夠大
用程式造:\(n = 2 \times 10^5\) 全部 \(10^9\)、\(k = 5000\) 張寬 \(40\) 1000000000 最大規模,\(\sum w = n\) 剛好貼滿

八筆都親眼看過正確,這題就穩了。

常犯錯誤
  • 貼完一張海報沒把 run 歸零(寫成 if (run >= w[placed]) placed++;):下一張會疊在上一張用過的木板上,兩個範例都過、本站測資掛 \(2\) 筆;自測表第 2 列印 9。
  • 先把寬度排序:海報必須照輸入順序貼,兩個範例排序後答案剛好不變;自測表第 3 列印 9,本站測資掛 \(1\) 筆。
  • 遇到太矮的木板沒把 run 歸零:不連續的高木板被當成連續的一段,範例 1 印 5、範例 2 印 5。
  • 用 h[i] > height 判斷夠不夠高:剛好等於也可以貼,範例 1 印 2、範例 2 印 4。
  • 中點用 (lo + hi) / 2 配 lo = mid:hi = lo + 1 時 mid 一直是 lo,兩個範例都跑不完(超時)。