問題
$N$ 頭の牛が番号順に一直線に並ぶ。牛 $i$ の位置を整数 $x_i$ とし、$x_1\le x_2\le\dots\le x_N$ でなければならない(同じ位置に複数頭いてよい)。さらに次の条件がある。
- 仲良しの組 $ML$ 個: 牛 $a,b$($a<b$)は距離 $d$ 以内、すなわち $x_b-x_a\le d$
- 仲の悪い組 $MD$ 個: 牛 $a,b$($a<b$)は距離 $d$ 以上、すなわち $x_b-x_a\ge d$
すべての条件を満たす配置のうち、$x_N-x_1$ の最大値を出力せよ。条件を満たす配置が存在しなければ -1、$x_N-x_1$ をいくらでも大きくできるなら -2 を出力せよ。
入力形式
N ML MD
a_1 b_1 d_1 (仲良しの組 ML 行)
...
a_1' b_1' d_1' (仲の悪い組 MD 行)
...
制約
| # | 条件 |
|---|---|
| 1 | $2\le N\le 1000$ |
| 2 | $0\le ML, MD\le 5000$ |
| 3 | $1\le a<b\le N$ |
| 4 | $0\le d\le 10^6$ |
| 5 | 入力はすべて整数 |
入出力例
入力例1
4 2 1
1 3 10
2 4 20
2 3 3出力例1
27($x_3\le x_1+10$、$x_4\le x_2+20$、$x_2\le x_3-3$ から $x_4\le x_1+27$。$x=(0,7,10,27)$ で達成)
入力例2
3 1 2
2 3 1
2 3 4
2 3 2出力例2
-1($x_3-x_2\le1$ と $x_3-x_2\ge4$ が矛盾。牛 1 からはこの矛盾に辿り着けない ので、牛 1 を始点にした Bellman–Ford だけだと -2 と誤答する)
入力例3
3 1 0
1 2 5出力例3
-2(牛 3 を縛る条件が無いので $x_3$ はいくらでも右に置ける)
背景と出典
「牛ゲー」は秋葉・岩田・北川『プログラミングコンテストチャレンジブック 第 2 版』(2012)2-5 節の例題「Layout」(POJ 3169)に由来する通称で、差分制約系(difference constraints)を最短路に帰着する定番。差分制約と最短路の対応は Cormen ら『Introduction to Algorithms』の「Difference constraints and shortest paths」節でも扱われる。本セッションでは両書を参照しておらず、正しさは下の検証ログ(全探索と Floyd–Warshall との照合)を根拠にしている。
本問は原題に「牛 1 から届かない矛盾」を検出する要件を明示的に入れた(入力例 2)。
概念図:不等式のグラフと最短路・負閉路
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$x_b\le x_a+d$ は、最短路の距離がみたす三角不等式 $\mathrm{dist}(b)\le\mathrm{dist}(a)+w(a,b)$ と同じ形。すべての条件を「$x_v-x_u\le c$」の形にそろえてみよう。
ヒント2(アプローチ)
- 仲良し $x_b-x_a\le d$ → 辺 $a\to b$、重み $d$
- 仲の悪い $x_b-x_a\ge d\iff x_a-x_b\le -d$ → 辺 $b\to a$、重み $-d$
- 並び順 $x_i\le x_{i+1}\iff x_i-x_{i+1}\le0$ → 辺 $i+1\to i$、重み $0$
頂点 1 からの最短距離 $\mathrm{dist}(N)$ が $x_N-x_1$ の最大値。
ヒント3(誘導)
Bellman–Ford を 2 回使う。
1. 全頂点の距離を 0 で始めて $N$ 周緩和し、$N$ 周目も更新があれば負閉路=解なし(-1)。これはどこにある負閉路でも見つける
2. 頂点 1 だけ 0、他を $\infty$ で始めて最短路。$\mathrm{dist}(N)=\infty$ なら -2、そうでなければ $\mathrm{dist}(N)$
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
INF = float("inf")
def bellman_ford(n, edges, dist):
"""dist を初期値として緩和を最大 n 周。n 周目にまだ更新されたら負閉路あり(False を返す)"""
for it in range(n):
updated = False
for u, v, w in edges:
du = dist[u]
if du != INF and du + w < dist[v]:
dist[v] = du + w
updated = True
if not updated:
return True
return False
def main():
data = sys.stdin.buffer.read().split()
n, ml, md = int(data[0]), int(data[1]), int(data[2])
edges = []
p = 3
for _ in range(ml): # x_b - x_a <= d → 辺 a→b 重み d
a, b, d = int(data[p]) - 1, int(data[p + 1]) - 1, int(data[p + 2]); p += 3
edges.append((a, b, d))
for _ in range(md): # x_b - x_a >= d ⇔ x_a - x_b <= -d → 辺 b→a 重み -d
a, b, d = int(data[p]) - 1, int(data[p + 1]) - 1, int(data[p + 2]); p += 3
edges.append((b, a, -d))
for i in range(n - 1): # x_i <= x_{i+1} ⇔ x_i - x_{i+1} <= 0 → 辺 i+1→i 重み 0
edges.append((i + 1, i, 0))
# 1) 解があるか: 全頂点を距離 0 で始める(仮想の始点から重み 0 の辺を全頂点へ張ったのと同じ)
if not bellman_ford(n, edges, [0] * n):
print(-1)
return
# 2) 最大の x_N - x_1 = 頂点 1 から頂点 N への最短距離
dist = [INF] * n
dist[0] = 0
bellman_ford(n, edges, dist) # 1) で負閉路が無いと分かっているので必ず収束する
print(-2 if dist[n - 1] == INF else dist[n - 1])
main()
計算量: $O(N\cdot(ML+MD+N))$。実測(Python 3.14.7、M1 Mac)で $N=1000$、$ML=MD=5000$、Bellman–Ford がほぼ $N$ 周回るように辺を逆順に並べた最悪ケースが 1.83 s、負閉路ありのケースが 0.84 s。
Step-by-Step 解説
「$x_v\le x_u+c$」を辺 $u\to v$(重み $c$)に対応させる。最短路の距離は全辺で $\mathrm{dist}(v)\le\mathrm{dist}(u)+c$ を満たすので、最短距離はそのまま不等式の解 になる。
頂点 1 から $N$ への任意の経路 $1=v_0\to v_1\to\dots\to v_k=N$ に沿って不等式を足すと $x_N-x_1\le(\text{経路の重み})$。すべての経路について成り立つので $x_N-x_1\le\mathrm{dist}(N)$。一方 $x_i=\mathrm{dist}(i)$(届かない頂点は後で述べる)とおけば等号が達成される。よって最大値=最短距離。
閉路 $v_0\to\dots\to v_0$ に沿って不等式を足すと $0\le(\text{閉路の重み})$。重みが負なら矛盾で、解が無い。逆に負閉路が無ければ、全頂点を距離 0 から始めた Bellman–Ford の結果が解になる(仮想の始点 $s$ から全頂点へ重み 0 の辺を張ったのと同じ)。
よくある実装は「頂点 1 から Bellman–Ford を回し、$N$ 周目に更新があれば -1」。しかし頂点 1 から届かない場所の負閉路は $\infty$ のまま緩和されず、見逃される。入力例 2 がそれで、-1 が正しいのに -2 を出す。
本解は 1 回目の Bellman–Ford を全頂点 0 で始める ことで、どこにある負閉路も見つける。全探索との照合で、頂点 1 始点だけの版は 300 ケース中 10 ケースを誤った(実測。10 件とも -1 を -2 と答えた)。
負閉路が無く、頂点 1 から $N$ に届かないなら、頂点 1 から届く頂点の集合 $R$ に対して $R$ の外の頂点をまとめて $+t$ ずらしても、$R$ から外へ出る辺が無いので条件は壊れない(外から $R$ へ入る辺 $u\to v$ は $x_v\le x_u+c$ で、$x_u$ が増えるだけなので保たれる)。$t\to\infty$ で $x_N-x_1\to\infty$。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 頂点 1 始点の Bellman–Ford 1 回 | $O(NM)$ | 不採用 | 届かない負閉路を見逃す(300 ケース中 10 ケース誤答、実測)。多くの解説がこの形 |
| Floyd–Warshall | $O(N^3)$ | 検算用のみ | $N=1000$ で $10^9$。$N\le30$ の検算には使った(300 ケース一致) |
| SPFA(キュー付き Bellman–Ford) | 平均は速いが最悪 $O(NM)$ | 不採用 | 最悪ケースを作られると遅い点は同じで、負閉路判定に「各頂点がキューに入った回数」の管理が要り、誤りやすい |
| ポテンシャル付き Union-Find | ほぼ $O(M)$ | 不採用 | 等式 $x_b-x_a=d$ しか扱えない(Day067 Q3)。不等式には使えない |
| 線形計画ソルバ | — | 不採用 | 差分制約は最短路で厳密に解けるので不要。整数解も自動的に得られる |
| Bellman–Ford 2 回(本解) | $O(NM)$ | 採用 | 全探索 300 ケース・Floyd–Warshall 300 ケースと全件一致 |
前提が変わったら:
- $x_N-x_1$ の最小値 を求めるなら、$x_1-x_N\le$ の最短路、すなわち頂点 $N$ から頂点 1 への最短距離 $\mathrm{dist}_N(1)$ を使い、答えは $-\mathrm{dist}_N(1)$
- 重みが非負だけ(仲の悪い組が無い)なら Dijkstra で $O(M\log N)$ に落とせる。ただし本問は $-d$ の辺があるので使えない
- 制約が $N=10^5$ 規模 なら $O(NM)$ は重い。負の辺が「並び順」の $0$ 辺と少数の $-d$ 辺だけ、などの構造があれば Johnson 法(ポテンシャルで Dijkstra 化)を検討する
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 頂点 1 始点の Bellman–Ford だけで負閉路判定 | 届かない負閉路は $\infty$ のまま | 1 回目は全頂点 0 で始める(入力例 2。300 中 10 ケース誤答) |
| 仲の悪い組を辺 $a\to b$ 重み $-d$ にする | 不等式の向きの取り違え | $x_a-x_b\le-d$ なので辺 $b\to a$ |
| 並び順 $x_i\le x_{i+1}$ を入れ忘れる | 問題文の「番号順に並ぶ」を読み落とす | 辺 $i+1\to i$ 重み 0 |
INF + w で比較して inf 同士の緩和が起きる | Python の float('inf') は inf + (-3) = inf で問題ないが、10**18 などを INF にすると負の辺で値が下がる | du != INF を先に確かめる |
次のステップ
- 発展1: 解の 1 つ(各 $x_i$)も出力する。1 回目の Bellman–Ford の距離をそのまま使えば、全条件を満たす配置になる
- 発展2: 条件が「$x_b-x_a\le d$ または …」のように OR を含むと NP 困難になりうる。2 択ずつなら 2-SAT と組み合わせる型を調べる
- 次回予告: 焦点帯 M1(DP・ゲーム)を続ける
検証ログ(本セッションで実行した数値)
- 全探索との一致(実測): $N\le4$、各組 3 個以下、$d\le4$ のランダム 300 ケース(内訳: 解なし 101・無限大 122・有限値 77)で、プログラム全体(標準入出力)の出力が「$x_1=0$、$x_i\in[0,40]$ の全整数配置」の探索と全件一致(最大値が 20 以上なら無限大と判定。有限なら最大値は高々 $3\times4=12$ なので区別できる)
- Floyd–Warshall との一致(実測): $N\le30$、仲良し 40 個以下・仲の悪い組 40 個以下の 300 ケース(解なし 260・無限大 23・有限値 17)と、仲の悪い組 6 個以下の 300 ケース(解なし 169・無限大 61・有限値 70)で全件一致
- 頂点 1 始点だけの版(実測): 全探索の 300 ケース中 10 ケースで誤答。入力例 2 はそのうちの 1 つ(
-1が正しいのに-2) - 実行時間(実測): $N=1000$、$ML=MD=5000$ で最悪順の鎖 1.83 s(出力 999)、負閉路あり 0.84 s(出力 -1)。$ML=MD=10^4$ にすると最悪 2.72 s だったので制約を 5000 にした
- 扱っていない範囲: 蟻本・CLRS の原文との照合、SPFA の実装と比較