Official

E - Distribute Bunnies Editorial by Nyaan


この問題は色々なアプローチが考えられそうです。しかし、貪欲法や DP などのアルゴリズムではどうやっても上手くいきません。
問題をうまくグラフの問題として捉え直すことが今回の問題のポイントです。
いくつかの解法があるのでそれぞれ説明します。

解法 1

座標 \(x\) を頂点 \(x\) に対応させたグラフを考えます。そして、各ウサギ \(i\) について、ウサギが動ける頂点間を辺で結びます。つまり、頂点 \(X_i - R_i\) と頂点 \(X_i + R_i\) を辺で結びます。
このようなグラフにおいて問題の答えを表現すると次の問題になります:

  • 各辺について隣接する頂点のどちらかに色を塗ります。(同じ頂点に複数回色を塗っても良い) 色を 1 回以上塗られた頂点の個数を最大化してください。

この問題を解くことを考えましょう。この問題はグラフの連結成分ごとに独立に解くことができるので、簡単のためグラフが \(n\) 頂点 \(m\) 辺の連結グラフである場合を解くことにします。
考察を進めると、グラフが木である場合とそうでない場合に分けて考えればよいことがわかります。場合分けして詳しく説明します。

1. グラフが木である (すなわち \(n=m+1\) である) 場合

グラフが木である場合、連結成分に含まれる辺の本数は \(m=n-1\) 本なので頂点は高々 \(n-1\) 個しか塗ることが出来ません。 また、グラフのどれか 1 頂点を根とみなしてグラフを根付き木とみなして、各辺について子の側の頂点を塗ることにすると必ず \(n-1\) 個塗ることが出来ます。(わからない人は絵を書いてみるとよいでしょう) よってこのケースでは \(n-1\) 個が最大で、これは常に達成できます。

2. グラフが木でない (すなわち \(m \geq n\) である) 場合

グラフが木である場合、連結成分に含まれる頂点の個数は \(n\) 個なので頂点は高々 \(n\) 個しか塗ることが出来ません。 また、以下の手順で \(n\) 個塗ることが必ず達成できます。

  • \(G\) の全域木を 1 個取り \(T\) とする。
  • \(G\) に含まれて \(T\) に含まれない辺が必ず存在するが、そのうち \(1\) 本を取る。その辺は \(u-v\) 間を結ぶとする。
  • \(T\) を頂点 \(v\) を根とする根付き木とみなして、各辺について子の側の頂点を塗る。グラフの頂点は頂点 \(v\) 以外が塗られた状態になる。
  • 先に選んだ \(u-v\) 辺を用いて頂点 \(v\) を塗る。

よってこのケースでは \(n\) 個が最大で、これは常に達成できます。

以上より、連結成分が木である場合と木でない場合のそれぞれにおいて答えを求められるようになりました。よって今回の問題は以下の手順で解くことが出来ます。

  • 上に説明した通りにグラフを構築する。
  • 答えを管理する変数を \(\mathrm{ans}\) とする。はじめ \(\mathrm{ans} = 0\) である。
  • グラフの連結成分のうち辺を含むものを列挙する。各連結成分について木かどうかを判定して、木である場合は \((連結成分の頂点数) - 1\) を、木でない場合は \((連結成分の頂点数)\)\(\mathrm{ans}\) に加算する。
  • 最終的な \(\mathrm{ans}\) の値が答えとなる。

計算量はグラフを std::map などの連想配列を用いて管理すると \(\mathrm{O}(N \log N)\) になり、これは十分高速です。

解法 2

こちらの解法は ネットワークフロー問題 (フローとよく略称されます) という高度な問題に帰着する解法ですが、フローはアルゴリズムの教科書に必ず載っている典型的な問題なので、幅広い視野を身に着けるためにもこの問題を解くレベル帯の方は履修することを勧めます。

次に説明する二部グラフを考えます。

  • 各ウサギ \(i\) に対応する頂点を左側頂点とする。
  • 各座標 \(x\) に対応する頂点を右側頂点とする。
  • ウサギ \(i\) が座標 \(x\) に移動可能なときに左側の頂点 \(i\) と右側の頂点 \(x\) の間に辺を張る。

このようなグラフにおいて問題の答えを表現すると次の問題になります:

  • 左側の頂点 \(i\) と右側の頂点 \(x\) を結ぶ辺の集合であって複数の辺に覆われた頂点が存在しないものの最大サイズは?

このように辺の集合であって複数の辺に覆われた頂点が存在しないもののことを マッチング と呼びます。今回はマッチングの最大サイズを求める問題で、これは 最大マッチング のサイズを計算する問題と言えます。
一般のグラフにおける最大マッチングの計算は困難ですが(多項式時間で計算できることは知られていてまれに出題されます、ABC412G 解説 などを参照してください)、二部グラフの最大マッチングは最大流を用いて(頂点数を \(n\)、辺数を \(m\) として) \(\mathrm{O}(n \sqrt{m})\) で計算できることが知られています。(Hopcroft-Karp アルゴリズムというより効率の良いアルゴリズムも存在します。) よって ACL の最大流ライブラリを用いれば今回の問題を \(\mathrm{O}(N \sqrt{N})\) で解くことが出来て、これは十分高速です。

  • 解法 1 の実装例 (C++)
#include <iostream>
#include <map>
#include <set>
#include <utility>
#include <vector>
using namespace std;

int main() {
  cin.tie(0)->sync_with_stdio(0);
  int N;
  cin >> N;
  vector<int> X(N), R(N);
  for (int i = 0; i < N; i++) cin >> X[i] >> R[i];

  map<int, vector<pair<int, int>>> g;
  for (int i = 0; i < N; i++) {
    int xpr = X[i] + R[i];
    int xmr = X[i] - R[i];
    g[xpr].emplace_back(xmr, i);
    g[xmr].emplace_back(xpr, i);
  }

  set<int> visited;
  auto dfs = [&](auto rc, int c, int edge_num) -> pair<int, int> {
    visited.insert(c);
    int num = 1;
    int is_tree = true;
    for (auto& [d, e] : g[c]) {
      if (e == edge_num) continue;
      if (visited.count(d)) {
        is_tree = false;
      } else {
        auto [n, f] = rc(rc, d, e);
        num += n;
        is_tree &= f;
      }
    }
    return make_pair(num, is_tree);
  };

  int ans = 0;
  for (auto& [key, _] : g) {
    if (visited.count(key)) continue;
    auto [num, is_tree] = dfs(dfs, key, -1);
    ans += is_tree ? num - 1 : num;
  }
  cout << ans << "\n";
}
  • 解法 2 の実装例(C++)
#include <iostream>
#include <set>
#include <utility>
#include <vector>
using namespace std;
#include "atcoder/maxflow.hpp"

int main() {
  cin.tie(0)->sync_with_stdio(0);
  int N;
  cin >> N;
  vector<int> X(N), R(N);
  for (int i = 0; i < N; i++) cin >> X[i] >> R[i];

  set<int> st;
  for (int i = 0; i < N; i++) {
    int xpr = X[i] + R[i];
    int xmr = X[i] - R[i];
    st.insert(xpr), st.insert(xmr);
  }

  vector<int> coords;
  for (auto& s : st) coords.push_back(s);
  int M = coords.size();

  int source = N + M;
  int sink = source + 1;
  atcoder::mf_graph<int> g(sink + 1);

  for (int i = 0; i < N; i++) {
    g.add_edge(source, i, 1);
  }
  for (int i = 0; i < N; i++) {
    int xpr = X[i] + R[i];
    int xmr = X[i] - R[i];
    xpr = lower_bound(begin(coords), end(coords), xpr) - begin(coords);
    xmr = lower_bound(begin(coords), end(coords), xmr) - begin(coords);
    g.add_edge(i, N + xpr, 1);
    g.add_edge(i, N + xmr, 1);
  }
  for (int j = 0; j < M; j++) {
    g.add_edge(N + j, sink, 1);
  }

  cout << g.flow(source, sink) << "\n";
}

posted:
last update: