問題
頂点の木があり、頂点は と、辺は と番号をつけられている。 また、辺 (ただし、 )は頂点 と頂点 を結んでいる。
を満たすすべての整数組 に対して、以下の値の総和を求めよ。
- 頂点 から出発し、番号が 以上 以下である辺のみを 本以上辿って到達できる頂点の個数。
入力
まず最初の1行目には、整数 が与えられる。
次の 行のうち 行目には が順に与えられる。
条件
- (ただし、 )
- 与えられるグラフは木
出力
答えとなる数値を1行で出力すること。
解法
単純に、すべての考えられうる に対して、頂点 から辿り着ける頂点を数え上げていく、という方法が考えられます。 しかし、 をforループ等で決め、 を決めた後にも辺の本数が 本あることより、この方法では 程度の計算量がかかってしまうと考えられます。
この問題では最終的には「すべての の組に対して、頂点 から 以上 以下の番号の辺を通って辿り着ける頂点の個数の総数」を求めます。 これは、「ある頂点 に対して、頂点 から 以上 以下の番号の辺を通って辿り着けるような、 の個数の総数」と等しいです。
すなわち、「辺の番号をもとにして、辿り着ける頂点の個数の総数」を求める問題を、「頂点をもとにして、辺の番号として満たされるものの個数の総数」を求める問題として言い換えることができます。
また、今回の問題で与えられるグラフは木構造であるため、頂点 から頂点 に辿り着く方法は一意に定まります。
ここで、頂点 でないある頂点 に対して、辿り着くために通る辺の番号の最小値を 、最大値を とします。 ( は ではないので、 です。)
頂点 に辿り着く方法が一意に定まることから、この は一意に定まります。
従って、問題の に頂点 が含まれるための条件は かつ となります。
以上から、頂点 が含まれるような の組み合わせの総数は であると言えます。
ここで、この については、頂点 から与えられたグラフ通りに辺を辿っていくことにより、求めることができます。
具体的には、既に頂点 に到達しているとして、辺 によって頂点 に辿り着けるとき、
と更新していくことができます。
また、頂点 については、必ず頂点 から出発することから、辺を辿らずとも必ず到達することができます。 そのため、どのような に対しても必ず頂点 は含まれます。
であるため、 のときに の個数は 個となることから、 の総数は \begin{align} \sum^{n - 1}_{i = 1} i = \frac{1}{2} n(n - 1) \end{align} であると言えます。
以上から、求める個数については、頂点 の場合とそれ以外の場合を考えて、 \begin{align} \frac{1}{2} n (n - 1) + \sum^{n}_{i = 2} {a}_{i} (n - {b}_{i}) \end{align} となります。
計算量については、各頂点の の値を求める際に、辺を順番に辿っていく箇所に が必要になります。 従って、これは実行時間制限に十分に間に合うと言えます。
ソースコード
まず、頂点 以外の頂点について、値 を求める部分を関数 dfs()
として以下のように実装しました。
vector<Pil> graph[int(2e5 + 5)]; // graph[i] は {次の頂点, 辺の番号} が入っている pair<int, ll> Pll range[int(2e5 + 5)]; // range[i] は {a[i], b[i]} の値が入っている pair<ll, ll> bool visited[int(2e5 + 5)]; // すでにその頂点を辿ったかどうかを表す bool 値 void dfs(int start) { // start は始点となる頂点が入る(この問題では常に start = 1) visited[start] = true; // 始点は探索済とする queue<int> que; // 次に探索を行う頂点の入る queue que.push(start); while (!que.empty()) { int now = que.front(); que.pop(); for (Pil next : graph[now]) { int next_node = next.first; // 次に辿るであろう頂点の番号 ll next_edge = next.second; // その頂点に辿り着くことのできる辺の番号 if (visited[next_node]) { // もし次の頂点が探索済なら、このループは行わない continue; } visited[next_node] = true; // 次の頂点を探索済とする que.push(next_node); // 次の頂点を queue に入れる range[next_node].first = min(range[now].first, next_edge); // a[i] の値を next_edge を用いて更新 range[next_node].second = max(range[now].second, next_edge); // b[i] の値を next_edge を用いて更新 } } return; }
以上の dfs()
が実装されている状態で、main()
関数の中に答えを出力する部分を以下のように実装しました。
ll n; vector<Pil> graph[int(2e5 + 5)]; Pll range[int(2e5 + 5)]; int main() { // 値の入力 cin >> n; for (int i = 1; i <= n - 1; i++) { int u, v; cin >> u >> v; // グラフに値を入れていく graph[u].push_back({v, i}); graph[v].push_back({u, i}); } range[1] = {n - 1, 1}; // dfs 用に、仮で a[1] = n - 1, b[1] = 1 としておく(後の計算には使用しない) dfs(1); // dfs を行う ll ans = (n - 1) * n / 2; // 頂点 1 の分を予め計算する for (int i = 2; i <= n; i++) { ans += range[i].first * (n - range[i].second); // 頂点 2 以降の部分について ans に加えていく } cout << ans << endl; // 答えの出力 return 0; }