C - 水の分配 / Distribution of Water Editorial by admin
Claude 4.6 Opus (Thinking)概要
水路で繋がった貯水池のグループ(連結成分)ごとに水を最適に再分配し、全貯水池の水量の最大値と最小値の差を最小化する問題です。
考察
重要な気づき①:連結成分内では自由に再分配できる
水路で直接つながっている貯水池間で 1 リットルずつ移動できるため、操作を繰り返せば、同じ連結成分内のどの貯水池間でも水を運べます。
例えば、貯水池 A → B → C と水路が繋がっていれば、A から B に 1 リットル送り、次に B から C に 1 リットル送ることで、実質的に A から C へ水を移動できます。
重要な気づき②:各貯水池は最低 1 リットル必要
初期水量はすべて \(P_i \geq 1\) なので、連結成分のサイズ \(k\)、総水量 \(S\) に対して \(S \geq k\) が成り立ちます。よって \(\lfloor S/k \rfloor \geq 1\) となり、均等に分配しても制約を満たせます。
重要な気づき③:連結成分内の最適分配
サイズ \(k\)、総水量 \(S\) の連結成分では、水量を可能な限り均等にするのが最適です: - \(S \mod k = 0\) の場合:全ノードが \(S/k\) リットル - \(S \mod k \neq 0\) の場合:\((k - S\%k)\) 個のノードが \(\lfloor S/k \rfloor\)、\((S\%k)\) 個のノードが \(\lceil S/k \rceil\)
重要な気づき④:答えはグローバルに決まる
全連結成分について、それぞれの達成可能な最小値・最大値を求め、全体の最大値 − 全体の最小値が答えです。異なる連結成分間では水を移動できないため、各成分は独立に最適化します。
アルゴリズム
- Union-Find で連結成分を求める
- 各連結成分について、ノード数 \(k\) と総水量 \(S = \sum P_i\) を計算する
- 各連結成分の最小値 \(\lfloor S/k \rfloor\) と最大値 \(\lceil S/k \rceil\) を求める
- 全連結成分にわたるグローバルな最大値とグローバルな最小値の差を出力する
具体例: \(N=4\), \(P = [10, 1, 5, 6]\) で、辺が (1,3), (3,4) の場合 - 連結成分 {1,3,4}:\(S=21, k=3\), 各ノード \(7\) リットル → min=7, max=7 - 連結成分 {2}:\(S=1, k=1\) → min=1, max=1 - 答え:\(7 - 1 = 6\)
計算量
- 時間計算量: \(O(N \cdot \alpha(N) + M \cdot \alpha(N))\)(\(\alpha\) はアッカーマン逆関数、実質 \(O(N + M)\))
- 空間計算量: \(O(N + M)\)
実装のポイント
Union-Find に経路圧縮とランクによる統合を用いることで高速化
連結成分ごとの集計には
defaultdict(list)を使い、各ノードの水量をグループ化総水量 \(S\) が最大 \(N \times 10^9 = 2 \times 10^{14}\) 程度になりうるため、Python の任意精度整数が活きる(C++ なら
long longが必要)孤立ノード(水路に接続していない貯水池)はサイズ 1 の連結成分として扱われ、水量は変更不可能
ソースコード
import sys
from collections import defaultdict
def main():
input_data = sys.stdin.buffer.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
M = int(input_data[idx]); idx += 1
P = [0] * N
for i in range(N):
P[i] = int(input_data[idx]); idx += 1
# Union-Find
parent = list(range(N))
rank = [0] * N
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx == ry:
return
if rank[rx] < rank[ry]:
rx, ry = ry, rx
parent[ry] = rx
if rank[rx] == rank[ry]:
rank[rx] += 1
for j in range(M):
u = int(input_data[idx]) - 1; idx += 1
v = int(input_data[idx]) - 1; idx += 1
union(u, v)
# Group pools by connected component
components = defaultdict(list)
for i in range(N):
components[find(i)].append(P[i])
# For each connected component, we can redistribute water freely
# (as long as every pool has >= 1 at all times, but since we move 1 liter at a time
# through connected paths, we can achieve any distribution where sum is preserved
# and each pool >= 1).
#
# For a component with k pools and total sum S:
# Each pool gets at least 1. Remaining S - k to distribute.
# To minimize max - min across ALL components, we need to think globally.
#
# Within a component of size k and sum S:
# floor_val = S // k, ceil_val = floor_val + 1
# num_ceil = S % k, num_floor = k - num_ceil
# So min in component = floor_val (if num_floor > 0, else ceil_val)
# and max in component = ceil_val (if num_ceil > 0, else floor_val)
# But we need floor_val >= 1 (since each pool >= 1).
# Since S >= k (each pool starts with >= 1), floor_val >= 1. Always satisfied.
#
# For a single-node component, the value is fixed at P[i].
#
# The global answer is global_max - global_min over all components' achievable values.
#
# Each component contributes a min value and a max value.
# The global min is the minimum of all component mins.
# The global max is the maximum of all component maxes.
global_min = float('inf')
global_max = float('-inf')
for root, pools in components.items():
k = len(pools)
S = sum(pools)
floor_val = S // k
remainder = S % k
if remainder == 0:
# All pools get floor_val
comp_min = floor_val
comp_max = floor_val
else:
# Some get floor_val, some get floor_val + 1
comp_min = floor_val
comp_max = floor_val + 1
global_min = min(global_min, comp_min)
global_max = max(global_max, comp_max)
print(global_max - global_min)
if __name__ == '__main__':
main()
この解説は claude4.6opus-thinking によって生成されました。
posted:
last update: