[USACO08JAN] Artificial Lake G

GitHub跳转原题关系图返回列表

把平台序列建成最大笛卡尔树,递归计算每个子盆地先灌到根高度、再整体上涨的体积时间,从而求出各平台被淹没时刻。

OJ: luogu

题目 ID: P2897

难度:提高+/省选-

标签:单调栈笛卡尔树递归模拟思维

日期: 2026-06-20 23:16

题意

给出 N 个从左到右排列的平台,每个平台有宽度 W_i 和高度 H_i,且所有高度互不相同。

从全局最低的平台开始,以每分钟 1 单位体积的速度向湖中注水。

要求对每个平台输出:它的平台顶从哪个时刻开始,与水面的距离至少为 1,也就是它第一次被完全淹没的时刻。

思路

先看一个慢版对照:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 205;

int n;
long long w[MAXN], h[MAXN];
int left_son[MAXN], right_son[MAXN], parent_node[MAXN];
long long sub_width[MAXN];
long long ans[MAXN];

int build_tree(int l, int r) {
    if (l > r) {
        return 0;
    }

    int root = l;
    for (int i = l + 1; i <= r; i++) {
        if (h[i] > h[root]) {
            root = i;
        }
    }

    left_son[root] = build_tree(l, root - 1);
    right_son[root] = build_tree(root + 1, r);
    if (left_son[root] != 0) {
        parent_node[left_son[root]] = root;
    }
    if (right_son[root] != 0) {
        parent_node[right_son[root]] = root;
    }
    return root;
}

long long dfs_width(int u) {
    if (u == 0) {
        return 0;
    }
    sub_width[u] = w[u] + dfs_width(left_son[u]) + dfs_width(right_son[u]);
    return sub_width[u];
}

// 慢速版本仍然使用同样的递归水位模型,只是建树改成 O(n^2)。
long long flood_subtree(int u, int side, long long cap, long long start) {
    if (u == 0) {
        return 0;
    }

    long long first_cost = 0;
    long long second_cost = 0;
    long long reach_top_time = start;

    if (side == 0) {
        first_cost = flood_subtree(left_son[u], 0, h[u], start);
        reach_top_time = start + first_cost;
        second_cost = flood_subtree(right_son[u], 0, h[u], reach_top_time);
    } else {
        first_cost = flood_subtree(right_son[u], 1, h[u], start);
        reach_top_time = start + first_cost;
        second_cost = flood_subtree(left_son[u], 1, h[u], reach_top_time);
    }

    ans[u] = reach_top_time + second_cost + sub_width[u];
    return first_cost + second_cost + sub_width[u] * (cap - h[u]);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> w[i] >> h[i];
        left_son[i] = right_son[i] = parent_node[i] = 0;
        sub_width[i] = ans[i] = 0;
    }

    int root = build_tree(1, n);
    dfs_width(root);

    int source = 1;
    for (int i = 2; i <= n; i++) {
        if (h[i] < h[source]) {
            source = i;
        }
    }

    long long active_width = w[source];
    long long current_time = 0;
    ans[source] = active_width;

    int cur = source;
    while (parent_node[cur] != 0) {
        int p = parent_node[cur];

        current_time += active_width * (h[p] - h[cur]);

        int sibling = 0;
        long long sibling_cost = 0;
        if (left_son[p] == cur) {
            sibling = right_son[p];
            sibling_cost = flood_subtree(sibling, 0, h[p], current_time);
        } else {
            sibling = left_son[p];
            sibling_cost = flood_subtree(sibling, 1, h[p], current_time);
        }

        current_time += sibling_cost;
        active_width += w[p] + sub_width[sibling];
        ans[p] = current_time + active_width;

        cur = p;
    }

    for (int i = 1; i <= n; i++) {
        cout << ans[i] << '\n';
    }

    return 0;
}

brute.cpp 并不是逐分钟模拟,而是和正式解使用同一个递推模型,只不过它在每个区间里暴力找最高平台建树,所以只适合小数据验证。

这题真正的关键观察是:

  • 某个平台要被淹没,左右更低的平台区域必须先灌到它的高度
  • 所以一个区间里的最高平台,一定会比这个区间里的其它平台更晚被淹没

这正好对应一棵最大笛卡尔树

  • 中序遍历顺序等于原平台顺序
  • 父节点高度严格高于子节点

在这棵树里,一个节点的整棵子树就对应一个完整盆地区间。

递归灌水时,可以这样理解:

如果从某个子树的一侧开始灌水,那么过程一定是:

  1. 先把这一侧的较低部分灌到根节点高度
  2. 水第一次碰到根顶
  3. 再从根顶流向另一侧,把另一侧也灌到根高度
  4. 最后整棵子树作为一个整体继续上涨

于是根节点被淹没的时刻可以递推出来:

  • 先算“水第一次碰到根顶”的时刻
  • 再加上另一侧被灌到根高度所需时间
  • 再加上整棵子树整体上涨 1 所需时间

最后这一项,正好就是整棵子树的总宽度。

正式解的优化点在于:

  • 用单调栈在线性时间建出最大笛卡尔树

之后所有递推都在线性规模内完成。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int n;
long long w[MAXN], h[MAXN];
int left_son[MAXN], right_son[MAXN], parent_node[MAXN];
int stk[MAXN], top_idx;
long long sub_width[MAXN];
long long ans[MAXN];

void build_cartesian_tree() {
    top_idx = 0;

    for (int i = 1; i <= n; i++) {
        left_son[i] = right_son[i] = parent_node[i] = 0;
    }

    for (int i = 1; i <= n; i++) {
        int last = 0;
        while (top_idx > 0 && h[stk[top_idx]] < h[i]) {
            last = stk[top_idx];
            top_idx--;
        }
        if (top_idx > 0) {
            right_son[stk[top_idx]] = i;
            parent_node[i] = stk[top_idx];
        }
        if (last != 0) {
            left_son[i] = last;
            parent_node[last] = i;
        }
        stk[++top_idx] = i;
    }
}

long long dfs_width(int u) {
    if (u == 0) {
        return 0;
    }
    sub_width[u] = w[u] + dfs_width(left_son[u]) + dfs_width(right_son[u]);
    return sub_width[u];
}

// 在一个子树中灌水:
// side=0 表示水从这个子树的左边进入,side=1 表示从右边进入。
// cap 表示外侧“挡板”的高度,且保证 cap > h[u]。
// start 表示开始往这个子树里灌水的时刻。
// 返回把整个子树都灌到高度 cap 所需的总时间。
long long flood_subtree(int u, int side, long long cap, long long start) {
    if (u == 0) {
        return 0;
    }

    long long first_cost = 0;
    long long second_cost = 0;
    long long reach_top_time = start;

    if (side == 0) {
        // 从左边进来,要先把左子树灌到当前根的高度,才能第一次碰到根顶。
        first_cost = flood_subtree(left_son[u], 0, h[u], start);
        reach_top_time = start + first_cost;

        // 根顶被碰到后,水会从根顶继续流向右子树。
        second_cost = flood_subtree(right_son[u], 0, h[u], reach_top_time);
    } else {
        // 从右边进入时完全对称。
        first_cost = flood_subtree(right_son[u], 1, h[u], start);
        reach_top_time = start + first_cost;
        second_cost = flood_subtree(left_son[u], 1, h[u], reach_top_time);
    }

    // 当两侧都被灌到 h[u] 以后,整棵子树才会作为一个整体继续上升。
    ans[u] = reach_top_time + second_cost + sub_width[u];

    return first_cost + second_cost + sub_width[u] * (cap - h[u]);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> w[i] >> h[i];
    }

    build_cartesian_tree();

    int root = stk[1];
    int source = 1;
    for (int i = 2; i <= n; i++) {
        if (h[i] < h[source]) {
            source = i;
        }
    }

    dfs_width(root);

    long long active_width = w[source];
    long long current_time = 0;
    ans[source] = active_width;

    int cur = source;
    while (parent_node[cur] != 0) {
        int p = parent_node[cur];

        // 先把当前已经连通的水体整体抬到父节点的高度。
        current_time += active_width * (h[p] - h[cur]);

        int sibling = 0;
        long long sibling_cost = 0;

        if (left_son[p] == cur) {
            sibling = right_son[p];
            sibling_cost = flood_subtree(sibling, 0, h[p], current_time);
        } else {
            sibling = left_son[p];
            sibling_cost = flood_subtree(sibling, 1, h[p], current_time);
        }

        current_time += sibling_cost;
        active_width += w[p] + sub_width[sibling];

        ans[p] = current_time + active_width;
        cur = p;
    }

    for (int i = 1; i <= n; i++) {
        cout << ans[i] << '\n';
    }

    return 0;
}

复杂度

单调栈建树、统计子树宽度、递归计算答案都只会线性处理每个平台常数次。

所以总时间复杂度是:

O(N)O(N)

空间复杂度是:

O(N)O(N)

总结

这题最难的不是实现,而是把“灌水过程”重新看成一棵树上的递推关系。

一旦想到:

  • 区间最高点最后被淹没
  • 可以用最大笛卡尔树表示包含关系

后面的公式推导就顺了。