Official

E - Art Gallery on Graph Editorial by Nyaan


まず、問題文に登場する「警備されている頂点」の条件、

  • 頂点 \(v\) と頂点 \(p_i\)​ の距離が \(h_i\) 以下であるような警備員 \(i\) が少なくとも 1 人存在する。

を理解しやすい設定に言い換えてみましょう。すると、次のようになります。

  • 警備員は今いる頂点から隣接する頂点に体力を \(1\) 消費して移動することができるとする。また、警備員は体力が \(0\) になると動けなくなる。
    このとき、「警備されている頂点」とは、 \(v\) にたどり着ける警備員が存在するような頂点のことを言う。

このように問題を適切に言い換えると見通しが良くなります。
\(d_i\) を「頂点 \(i\) に辿り着いた警備員の体力の最大値(ただし誰も警備員がたどり着けない場合は \(-1\))」と定義します。\(d_1, d_2, \dots, d_N\) を全て計算できればこの問題を解くことができます。
\(d_1, d_2, \dots, d_N\) は実は以下のアルゴリズムで計算できます。

  • \(x_1, x_2, \dots, x_N\)\(d_i\) の暫定値を保存する配列とする。\(x_i\)\(p_i\) に警備員がいる場合はその警備員の体力で、そうでない場合は \(-1\) で初期化されている。
  • 次の手順を \(N\) 回行い、\(d_{i}\) の値を 1 頂点ずつ確定させていく。
    • まだ \(d_i\) が確定していない頂点のうち、\(x_i\) の値が最も大きい頂点を \(v\) とする。
    • まず、\(d_v = x_v\) として、\(d_v\) の値を確定済みにする。
    • そして、\(v\) に隣接する頂点 \(u\) について以下の操作を行う。
      • \(u\)\(d_u\) が確定していない頂点である場合、\(x_u\)\(\max(x_u, x_v - 1)\) に置き換える。

正当性は帰納法を利用すれば証明できます。(詳細は略しますが、ダイクストラ法の正当性の証明と同様の方法で証明することができるので、わからない方はまずダイクストラ法を勉強してみましょう。)

このアルゴリズムは、ナイーブな実装をすると \(x_i\) 最大の要素を発見するのに \(\mathrm{O}(N)\) かかるので \(\mathrm{O}(N^2)\) の時間計算量になってしまいます。そこで、\(x_i\) 最大の要素の管理にヒープやセグメント木などのデータ構造を利用することで、計算量を \(\mathrm{O}((N + M) \log (N + M))\) に落とすことができます。これは AC するのに十分高速です。 (グラフの辺重みが全て 1 なのを利用してアルゴリズムに工夫を加えることで計算量を \(\mathrm{O}(N + M)\) に落とすこともできます。)
ところで、この問題は実は最短路問題の正負を反転させた構造を持っています。また、上記のアルゴリズムはダイクストラ法の正負を反転させたようなアルゴリズムになっています。このような事実を利用して、適切にグラフを構築して最短路問題に帰着させるという別解も考えられます。

  • 実装例(C++)
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;

int main() {
  cin.tie(nullptr);
  ios::sync_with_stdio(false);

  int N, M, K;
  cin >> N >> M >> K;
  vector<vector<int>> g(N);
  for (int i = 0; i < M; i++) {
    int a, b;
    cin >> a >> b, --a, --b;
    g[a].push_back(b), g[b].push_back(a);
  }
  vector<int> p(K), h(K);
  for (int i = 0; i < K; i++) cin >> p[i] >> h[i], p[i]--;

  vector<int> d(N, -1);
  priority_queue<pair<int, int>> Q;

  auto add = [&](int v, int x) {
    if (d[v] < x) Q.emplace(d[v] = x, v);
  };
  for (int i = 0; i < K; i++) add(p[i], h[i]);
  while (Q.size()) {
    auto [x, v] = Q.top();
    Q.pop();
    if (d[v] != x) continue;
    for (auto& u : g[v]) add(u, d[v] - 1);
  }
  vector<int> ans;
  for (int i = 0; i < N; i++)
    if (d[i] >= 0) ans.push_back(i + 1);

  cout << ans.size() << "\n";
  for (int i = 0; i < (int)ans.size(); i++)
    cout << ans[i] << " \n"[i + 1 == (int)ans.size()];
}

posted:
last update: