E - フェンスの塗装 / Painting the Fence Editorial by admin Editorial for printing
Gemini 3.0 Flash (Thinking)概要
\(N\) 枚の板に対して \(M\) 回の区間塗装指示が出されます。このうち連続する \(K\) 回の指示を無視したとき、最終的に塗装されている板の枚数を最大化する問題です。板の総数 \(N\) が非常に大きいため、座標圧縮と効率的な区間管理が鍵となります。
考察
1. 座標圧縮の必要性
板の枚数 \(N\) は最大 \(10^9\) と非常に大きいため、板を 1 枚ずつ管理することは不可能です。一方で、指示の数 \(M\) は \(2 \times 10^5\) と比較的小さいため、指示の端点(\(L_i\) と \(R_i+1\))に注目する座標圧縮を用いることで、管理すべき区間の数を \(O(M)\) の規模に抑えることができます。
2. 板が「未塗装」になる条件
ある区間が最終的に「未塗装」になるのは、「その区間を塗るはずだったすべての指示が、無視する \(K\) 回の中に含まれている」ときだけです。
各圧縮後の区間について、それをカバーする指示のインデックスの集合を \(I\) とします。無視する指示の範囲を \([s, s+K-1]\) とすると、その区間が未塗装になる条件は以下の通りです: - \(I\) に含まれる最小のインデックスを \(\min(I)\)、最大のインデックスを \(\max(I)\) としたとき、 - \(s \leq \min(I)\) かつ \(\max(I) \leq s+K-1\)
これを \(s\) について整理すると、以下のようになります: - \(\max(I) - K + 1 \leq s \leq \min(I)\)
この条件を満たす \(s\) の範囲において、その区間の長さ分だけ「塗装される板の総数」が減少することになります。
3. 各区間の最小・最大インデックスの取得
各区間をカバーする指示の最小・最大インデックスを効率よく求める必要があります。これは、セグメント木を用いた区間更新(Range Chmin / Range Chmax)を行うことで、各リーフ(最小単位の区間)に対して \(O(\log M)\) または一括処理で求めることができます。
アルゴリズム
- 座標圧縮: すべての指示の左端 \(L_i\) と右端の次 \(R_i+1\) を集めてソートし、重複を除きます。これにより、数直線が最大 \(2M-1\) 個の小区間に分割されます。
- 区間情報の収集:
セグメント木(または遅延評価付きの構造)を準備し、各指示 \(i\) について、それがカバーする小区間に対して「インデックス \(i\)」で更新を行います。
tree_min: その区間を覆う最小のインデックスを保持tree_max: その区間を覆う最大のインデックスを保持
- 減少量の計算:
各小区間について、もし一度でも塗られるなら(\(\min(I) \neq \infty\)):
- 条件 \(\max(I) - K + 1 \leq s \leq \min(I)\) を満たす \(s\) の範囲を求めます。
- この \(s\) の範囲に対して、その区間の長さ分だけ「塗装枚数の減少量」を加算します。これはいもす法(差分配列)を用いることで効率的に行えます。
- 最適解の選択: 全指示を実行した場合の塗装枚数の合計から、いもす法で計算した「減少量」の最小値を引いたものが答えとなります。
計算量
- 時間計算量: \(O(M \log M)\)
- 座標圧縮のためのソート: \(O(M \log M)\)
- セグメント木への \(M\) 回の区間更新: \(O(M \log M)\)
- いもす法による集計: \(O(M)\)
- 空間計算量: \(O(M)\)
- 座標圧縮後の配列、セグメント木、差分配列などで \(O(M)\) のメモリを使用します。
実装のポイント
半開区間の利用: 板 \(L_i\) から \(R_i\) までを塗るという指示を、座標圧縮上では \([L_i, R_i+1)\) という半開区間として扱うと、区間の長さの計算(\(coords[j+1] - coords[j]\))が簡潔になります。
セグメント木の簡略化: 今回は「最後に各リーフの値が分かれば良い」ため、完全な遅延伝搬セグメント木を実装しなくても、各ノードに値を保持しておき、最後に根から葉へ値を伝搬させる(
push_down)だけで十分です。\(s\) の範囲: 無視する開始位置 \(s\) は \(1 \leq s \leq M-K+1\) の範囲を動くことに注意してください。助変数 \(s\) の範囲外にならないよう
max(1, ...)やmin(M-K+1, ...)でクランプする必要があります。ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/**
* Problem: Fence Painting
* We need to maximize the number of painted boards after ignoring exactly K consecutive instructions.
* N can be up to 10^9, so coordinate compression is necessary.
* M is up to 2 * 10^5, which allows for O(M log M) solutions.
*/
const int INF = 1e9 + 7;
int main() {
// Optimize input and output
ios_base::sync_with_stdio(false);
cin.tie(NULL);
long long N;
int M, K;
if (!(cin >> N >> M >> K)) return 0;
// Store instructions and collect all relevant coordinates for compression
vector<pair<int, int>> instructions(M);
vector<int> coords;
coords.reserve(2 * M);
for (int i = 0; i < M; ++i) {
cin >> instructions[i].first >> instructions[i].second;
// The instructions cover boards from L_i to R_i inclusive.
// This corresponds to the interval [L_i, R_i + 1) in coordinate space.
coords.push_back(instructions[i].first);
coords.push_back(instructions[i].second + 1);
}
// Coordinate compression
sort(coords.begin(), coords.end());
coords.erase(unique(coords.begin(), coords.end()), coords.end());
int P = (int)coords.size();
int num_intervals = P - 1;
// If there are no intervals, no boards are painted.
if (num_intervals <= 0) {
cout << 0 << endl;
return 0;
}
// Segment trees to store the minimum and maximum instruction index covering each interval.
// We only need the final leaf values after all updates, so we can push down lazy updates at the end.
vector<int> tree_min(4 * num_intervals, INF);
vector<int> tree_max(4 * num_intervals, -INF);
// Range update function for segment tree (minimum)
auto update_min = [&](auto self, int node, int start, int end, int l, int r, int val) -> void {
if (start > end || start > r || end < l) return;
if (start >= l && end <= r) {
tree_min[node] = min(tree_min[node], val);
return;
}
int mid = start + (end - start) / 2;
self(self, 2 * node, start, mid, l, r, val);
self(self, 2 * node + 1, mid + 1, end, l, r, val);
};
// Range update function for segment tree (maximum)
auto update_max = [&](auto self, int node, int start, int end, int l, int r, int val) -> void {
if (start > end || start > r || end < l) return;
if (start >= l && end <= r) {
tree_max[node] = max(tree_max[node], val);
return;
}
int mid = start + (end - start) / 2;
self(self, 2 * node, start, mid, l, r, val);
self(self, 2 * node + 1, mid + 1, end, l, r, val);
};
// Process each instruction and update the range of intervals it covers.
for (int i = 0; i < M; ++i) {
int l_idx = lower_bound(coords.begin(), coords.end(), instructions[i].first) - coords.begin();
int r_idx = lower_bound(coords.begin(), coords.end(), instructions[i].second + 1) - coords.begin() - 1;
if (l_idx <= r_idx) {
update_min(update_min, 1, 0, num_intervals - 1, l_idx, r_idx, i + 1);
update_max(update_max, 1, 0, num_intervals - 1, l_idx, r_idx, i + 1);
}
}
// Leaf arrays to store the final min and max index for each interval.
vector<int> min_idx_leaf(num_intervals, INF);
vector<int> max_idx_leaf(num_intervals, -INF);
// Push down the updates from the segment tree to the leaves.
auto push_down = [&](auto self, int node, int start, int end, int cur_min, int cur_max) -> void {
cur_min = min(cur_min, tree_min[node]);
cur_max = max(cur_max, tree_max[node]);
if (start == end) {
min_idx_leaf[start] = cur_min;
max_idx_leaf[start] = cur_max;
return;
}
int mid = start + (end - start) / 2;
self(self, 2 * node, start, mid, cur_min, cur_max);
self(self, 2 * node + 1, mid + 1, end, cur_min, cur_max);
};
push_down(push_down, 1, 0, num_intervals - 1, INF, -INF);
long long total_painted = 0;
// Difference array to calculate the reduction in painted area for each possible window s.
vector<long long> reduction_diff(M + 2, 0);
for (int j = 0; j < num_intervals; ++j) {
if (min_idx_leaf[j] != INF) {
long long width = (long long)coords[j + 1] - coords[j];
total_painted += width;
// An interval becomes unpainted if ALL instructions covering it are ignored.
// This happens if the ignored window [s, s + K - 1] contains both the
// minimum and maximum instruction indices that cover the interval.
int l_s = max(1, max_idx_leaf[j] - K + 1);
int r_s = min(M - K + 1, min_idx_leaf[j]);
if (l_s <= r_s) {
reduction_diff[l_s] += width;
reduction_diff[r_s + 1] -= width;
}
}
}
// Find the window s that results in the minimum reduction of painted boards.
long long min_reduction = -1;
long long current_reduction = 0;
for (int s = 1; s <= M - K + 1; ++s) {
current_reduction += reduction_diff[s];
if (min_reduction == -1 || current_reduction < min_reduction) {
min_reduction = current_reduction;
}
}
// If no window was processed, reduction is 0.
if (min_reduction == -1) min_reduction = 0;
// The maximum number of painted boards is the initial total minus the minimum reduction.
cout << total_painted - min_reduction << endl;
return 0;
}
この解説は gemini-3-flash-thinking によって生成されました。
posted:
last update: