[R15F] 树上炸弹
- 难度 普及+/提高
- 时限 3s
- 空限 512m
- 树动态规划树上差分倍增
数据规模:,,。 的数据 。
思路
朴素做法是对每个炸弹从 出发 BFS/DFS,给所有满足 的节点 的答案加 ,复杂度 ,只能拿 。注意到 很小,这是关键突破口。
把树转为以节点 为根的有根树。记 为 向根方向走 步到达的节点(走不到根则记为 )。分析在节点 放一个范围 的炸弹的影响:被波及的节点是从 出发、距离不超过 的所有点。把这些点按「离根最近的公共祖先」归类,可以拆成至多 段沿祖先链的「子树内、距某祖先不超过某值」的集合。
具体地,沿 向上枚举步数 (设 ):
- 当 : 子树内距 不超过 的节点全部被炸,即 。
- 当 且 : 子树内距 不超过 的节点全部被炸,但这会重复计入已经处理过的 那一支(即 子树)。所以先 ,再用树上差分抵消:令 子树内距它不超过 的节点少被炸一次,即 。
- 当 且 :只剩祖先 自己被炸,即 。
这里 的含义是「 子树内、距 不超过 的节点统一加上的标记值」。每个炸弹只需沿父链向上走至多 步,因此处理所有炸弹是 。
标记定义完后,做一次自根向下的 DFS 推导真实贡献:对每个节点 ,有 ()。这条转移的含义是:父节点处「距父不超过 」的标记,传播到 处正好对应「距 不超过 」。按 BFS 序(父先于子)顺序遍历即可,无需递归。
最后每个节点 的答案就是 。总复杂度 。
仓颉实现
import std.env.*
import std.convert.*
main(): Int64 {
let reader = getStdIn()
let line1 = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = line1[0]
let m = line1[1]
let nn = n
// 邻接表 (1-indexed), 用 CSR 式数组节省内存
var head = Array<Int64>(nn + 2, { _ => -1 })
var toArr = Array<Int64>(2 * nn, { _ => 0 })
var nxt = Array<Int64>(2 * nn, { _ => -1 })
var ecnt = 0
func addEdge(u: Int64, v: Int64): Unit {
toArr[ecnt] = v
nxt[ecnt] = head[u]
head[u] = Int64(ecnt)
ecnt++
}
var i = 0
while (i < n - 1) {
let e = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
addEdge(e[0], e[1])
addEdge(e[1], e[0])
i++
}
// BFS 建立有根树, parent[1]=0; order 记录 BFS 顺序 (父先于子)
let parent = Array<Int64>(nn + 1, { _ => 0 })
let order = Array<Int64>(nn, { _ => 0 })
var ohead = 0
var otail = 0
order[otail] = 1
otail++
parent[1] = 0
while (ohead < otail) {
let u = order[ohead]
ohead++
var e = head[u]
while (e != -1) {
let v = toArr[e]
if (v != parent[u]) {
parent[v] = u
order[otail] = v
otail++
}
e = nxt[e]
}
}
// f[x][i], i in 0..50, 共 51 列. 扁平存储: f[x*W + i]
// 用 Int32 减小内存 (5e5 * 51 * 4 ≈ 102MB)
let W = 51
let stride = Int64(W)
let f = Array<Int32>((nn + 1) * W, { _ => 0 })
// 处理每个炸弹: 沿父链向上走, 用差分抵消子树内重复部分
var bi = 0
while (bi < m) {
let parts = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let p = parts[0]
let w = parts[1]
var cur = p
var rem = w
var bc = Int64(cur) * stride + rem
f[bc] = f[bc] + 1
while (rem > 0) {
let child = cur
cur = parent[cur]
rem -= 1
if (cur == 0) {
break
}
if (rem > 0) {
let baseCur = Int64(cur) * stride
let baseChild = Int64(child) * stride
let p1 = baseCur + rem
f[p1] = f[p1] + 1
let p2 = baseChild + rem - 1
f[p2] = f[p2] - 1
} else {
let baseCur = Int64(cur) * stride
f[baseCur] = f[baseCur] + 1
break
}
}
bi++
}
// 自顶向下传播: f[x][i] += f[parent][i+1], i in 0..49
// order 是 BFS 顺序, 顺序遍历即父先于子
var oi = 1 // 跳过根 1
while (oi < nn) {
let x = order[oi]
let par = parent[x]
var offX = Int64(x) * stride
var offP = Int64(par) * stride
var ii = 0
while (ii < 50) {
f[offX] = f[offX] + f[offP + 1]
offX += 1
offP += 1
ii++
}
oi++
}
// ans[x] = sum_i f[x][i]
let sb = StringBuilder()
var xi = 1
while (xi <= nn) {
var s = Int64(0)
var off = Int64(xi) * stride
var ii = 0
while (ii < W) {
s += Int64(f[off])
off += 1
ii++
}
sb.append(s)
if (xi < nn) {
sb.append(" ")
}
xi++
}
println(sb)
return 0
}
要点
-
拆炸弹影响为祖先链上的若干子树集合:一个范围 的炸弹在 处爆炸,等价于沿 的祖先链 ,在每个仍存在的祖先 处给「 子树内距 不超过 」加一。这正是 能被利用的地方:每个炸弹只走至多 步父链。
-
树上差分抵消重复:祖先 处加的范围会把已经处理过的 那一支重复计入,所以在 处对「距它不超过 」的范围减一。这样每个炸弹只产生 次单点修改,不需要真的去遍历子树。
-
状态定义与推导:令 表示「 子树内距 不超过 的节点统一加的标记」。推导阶段自顶向下做 (父处距父 ,对应子处距子 ),最后每个节点的答案为 。
-
用 BFS 序替代递归: 达到 ,递归 DFS 有栈溢出风险,改用 BFS 序数组,父先于子顺序遍历完成自顶向下的传播。
-
内存优化: 数组规模 ,用
Int32并扁平化为一维(f[x*stride+i])存储,约 MB;邻接表也用head/to/nxt三个一维数组实现,避免ArrayList开销。