洛谷P9755 [CSP-S 2023] 种树【题解】【二分+贪心】

P9755 [CSP-S 2023] 种树【题解】

题意简述

给定一棵树。每个结点对应一个地块,每天选择一个 未种树 并且 与某个已种树的地块直接邻接(即通过单条道路连接) 的地块种下一棵高度为 000 米的树,若所有地块已种树,无需操作。第一天只能在根节点种树。对于在 iii 地块的树,从种下的当天开始,每天都会生长 max(bi+x×ci,1)max(b_i + x \times c_i,1)max(bi+x×ci,1) 米,特别注意:xix_ixi 是整个任务已经开始的天数,不是种下这棵树的天数。要使得每棵树的高度 ≥ai\ge a_iai ,最少需要多少天?

思路

二分答案。

可以发现一个性质:已种树地块的扩展方向是固定的,必须从根节点向下扩展(如果一个结点已经有树,意味着它到根节点的路径上每个点都已经有树,不必再考虑)。

N≤1e5N \le 1e5N1e5 ,暴力枚举每次种哪个结点肯定是不行的。

25pts25pts25pts 的特殊性质 AAAci=0c_i = 0ci=0 ,换句话说,第几天种下这棵树对它生长没有任何影响,每天要么生长 bib_ibi ,要么生长 111

10pts10pts10pts 的特殊性质 BBB :链,很好做。

15pts15pts15pts 的特殊性质 CCC :除根节点每个结点最多只有一个孩子。

15pts15pts15pts 的特殊性质 DDD :菊花图。

这题如果在赛时,想不出正解应当冲暴力+特殊性质的 80pts80pts80pts ,也是挺划算的。

可以发现一个性质:每个地块每天可以生长的高度是固定的。每个 xxx 对应唯一高度,不为任何因素所干扰。所以是否可以提前预处理出这个东西?但是天数太多了,存不下。

二分的值域是 [n,1e9][n,1e9][n,1e9] 。数学推导一下?

对于每个结点,可以求出一个分界点日期 x0x_0x0 ,此前(后)每天生长取函数值,此后(前)每天生长取 111 。有何意义?设当前二分的答案是 midmidmid ,要判定当前局面是否可行,可以处理出在 midmidmid 天内要长到 aia_iai最晚 要在哪一天种下。对于每个地块:

c>0c > 0c>0 ,则在 $x_0 = \frac{1-b}{c} $ 天之后,每天生长 b+x×cb+x \times cb+x×c 米,每多一天,每天多生长 ccc 米。

不对, bi≥1b_i \ge 1bi1 ,只要 ci>0c_i > 0ci>0 ,每天都取函数值。在第 xix_ixi 天种下,最终生长总高度即为 (mid−xi+1)b+(∑i=ximidi)c(mid - x_i + 1)b + (\sum_{i = x_i}^{mid}i)c(midxi+1)b+(i=ximidi)c ,其中 (∑i=ximidi)c(\sum_{i = x_i}^{mid}i)c(i=ximidi)c 可以简化为 12(xi+mid)(mid−xi+1)c\frac{1}{2}(x_i + mid)(mid - x_i + 1)c21(xi+mid)(midxi+1)c

换句话说,这个过程也可以二分?

大致思路好像有了,通过各种乱搞求出每个地块最晚种树的时间点,再用某种搜索来判定能否满足在每个地块的时间限制以前遍历到它(即在此种下树)。先验证这种方法可行性。

在当前 midmidmid 下,假设现已知道每个地块 iii 最晚要在第 xix_ixi 天被遍历。换句话说,用 dfndfndfn 记录被遍历到的时间,判断是否存在一种遍历树的方式,使得 dfni≤xidfn_i \le x_idfnixi 。这个操作怎么实现?感觉我这个思路是正解,问题就在乎如何完成上述操作。

首先深度超了肯定不行,怎么遍历都遍历不到。

贪心?xix_ixi 越小的点优先级越大,第一次必然是从根节点往外扩展。不妨以样例为例试着扩展一下?

动态规划???有没有可能通过动态规划来判定?不妨考虑这样设计状态。

fif_ifi 表示在树中 iii 号结点最早可以在······不对,这样只考虑单个结点,不考虑别的结点肯定不行。

时间紧迫,问问 AIAIAI

其实想到这里,菊花图部分分已经可以做了。吗?

看了眼题解,果然我的思路是正解。而且看到题解中处理方法很妙。

修改定义:设 txt_xtx 表示 xxx 号地块最晚种树时间,将 ttt 数组升序排序,从头遍历:对于当前 iii 地块已经被种树,跳过;否则向上攀爬到 最近的已经种树 的 祖先,将祖先到该结点的所有地块按序种上树。这样的贪心为什么正确?显然没有被种的地块 ttt 一定更大,它们可以延后考虑,那我把它们提前种了也无妨,而当前结点是迫在眉睫的,所以从祖先种到当前结点。卧槽,太妙了。

先前遗留两个问题没考虑:

c=0c = 0c=0 时,$t_x = \left \lceil \frac{a_x}{b_x} \right \rceil $

c<0c < 0c<0 时,在 x0≤⌊1−bc⌋x_0 \le \left \lfloor \frac{1 - b}{c} \right \rfloorx0c1b 时,取函数值,反之取 111 。求出这个转折点就行。

综上所述,重新理一下思路

思路整理

本题要求最少天数,单调性是显然的。(后续让 deepseekdeepseekdeepseek 证明)

接下来,二分答案 midmidmid,考虑非平方复杂度判断 midmidmid 是否可行:观察数据可以发现,每个地块每天的生长高度不被其他因素影响,所以可以利用这个性质,快速求解出在 midmidmid 范围内,使得指定地块的树长到指定高度的最晚种树时间 txt_xtx,这个过程的思路也很自然。求解 txt_xtx 的过程利用一次函数单调性二分即可。

最后,现已知道每个地块最晚种树时间 txt_xtx ,如何线性判定能否满足每个点的需求?考虑贪心:由于 txt_xtx 越小越紧迫,所以按 txt_xtx 升序排序,对于当前遍历到的结点,如果已经被种,直接跳过;如果没有,跳到 最近的、已被种植的祖先节点 ,将祖先结点到该结点的路径上所有结点依次种树。为什么是对的?首先,txt_xtx 更大的结点不会干扰当前结点的结果,在先前过程中如果把它种了更好。其次,正如我观察到的性质,一个结点要种树,其到根节点的路径上必然都已经被种,这是个必要条件,于是必须将祖先节点到该结点路径上所有结点种树,且这一过程种的每棵树都是必要的,不会产生冗余,浪费时间。

因此,本题的思路已经明朗,时间复杂度为 O(log⁡v×(nlog⁡n+n))O(\log v \times (n \log n + n))O(logv×(nlogn+n)) ,其实复杂度这个东西挺难精确求出的,均摊下来,最终复杂度为 O(nlog⁡nlog⁡v)O(n \log n \log v)O(nlognlogv)

代码实现的时候,二分求解 txtxtx 的过程写的不对,暂时没调出来。下午继续。

重新思考:

1.ci>0c_i > 0ci>0

此时每天生长高度都取函数值 bi+x×cib_i + x \times c_ibi+x×ci ,最后一天生长高度为 bi+mid×cib_i + mid \times c_ibi+mid×ci ,设在第 x0x_0x0 天种下,则总生长高度即为 ((bi+x0×ci)+(bi+mid×ci))×(mid−x0+1)÷2((b_i + x_0 \times c_i) + (b_i + mid \times c_i)) \times (mid - x_0 + 1) \div 2((bi+x0×ci)+(bi+mid×ci))×(midx0+1)÷2

2.ci<0c_i < 0ci<0

求出分界点 divdivdiv ,使得 [1,div][1,div][1,div] 取函数值生长,(div,mid](div,mid](div,mid]111

$b_i + x \times c_i \ge 1 \to $

x×ci≥1−bi→x \times c_i \ge 1 - b_i \tox×ci1bi

x≤⌊1−bici⌋x \le \left \lfloor \frac{1 - b_i}{c_i} \right \rfloorxci1bi

代码实现

调了 infinfinf 次。前后历时 555ACACAC

几个问题:

1.本题数据规模极大,总和生长高度远超 long longlong \space longlong long ,正解应当使用 int128int128int128

2.二次重构代码的时候出现了弱智错误,如遍历 txtxtx 数组的时候应当用 vis[pos]vis[pos]vis[pos] 而不是 vis[i]vis[i]vis[i] ,调了半天没调出来。

3.边界条件处理不严谨,包括但不限于 divdivdivci==0c_i == 0ci==0 时向上取整的问题,还有处理 ci<0c_i < 0ci<0divdivdiv 是否在 [1,val][1,val][1,val] 区间内的细节问题。

总而言之,本题细节很多,想实现好需要足够细心。

代码太丑陋,放 AIAIAI 整理过的吧。整体框架不变。

#include <bits/stdc++.h>

using namespace std;

typedef __int128 ii;
const int N = 1e5 + 100;
int n;
int fa[N], req[N];
bool vis[N];
struct node1 {
    ii a, b, c;
} arr[N];
struct node2 {
    ii t;
    int id;
} tx[N];
vector<int> gra[N];

void read(ii &res) {
    res = 0;
    bool neg = false;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') neg = true;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        res = res * 10 + (ch - '0');
        ch = getchar();
    }
    if (neg) res = -res;
}

void dfs(int u, int f) {
    fa[u] = f;
    for (auto v : gra[u]) {
        if (v == f) continue;
        dfs(v, u);
    }
}

bool cmp(node2 x, node2 y) {
    return x.t < y.t;
}

bool check(int val) {
    for (int i = 1; i <= n; i++) {
        vis[i] = false;
        tx[i].id = i;
        ii ai = arr[i].a;
        ii bi = arr[i].b;
        ii ci = arr[i].c;

        if (ci > 0) {
            int l = 1, r = val, mid, ans = -1;
            while (l <= r) {
                mid = (l + r) / 2;
                ii sum = ((bi + mid * ci) + (bi + val * ci)) * (val - mid + 1) / 2;
                if (sum >= ai) {
                    ans = mid;
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
            if (ans == -1) return false;
            tx[i].t = ans;
        } else if (ci == 0) {
            ii t = (ai + bi - 1) / bi; // 正确向上取整
            if (t > val) return false;
            tx[i].t = val - t + 1;
        } else {
            // 计算线性增长部分结束的天数
            ii div = (1 - bi) / ci;
            if ((1 - bi) % ci != 0 && (1 - bi) > 0) div++; // 处理余数,确保div是最后一个满足b_i +x*c_i >=1的x
            int l = 1, r = val, mid, ans = -1;
            while (l <= r) {
                mid = (l + r) / 2;
                ii sum = 0;
                if (mid <= div) {
                    ii end = min(div, (ii)val);
                    ii cnt = end - mid + 1;
                    ii a1 = bi + mid * ci;
                    ii an = bi + end * ci;
                    sum = (a1 + an) * cnt / 2;
                    sum += max((ii)0, val - end); // 剩余天数每天增长1
                } else {
                    sum = val - mid + 1; // 全部增长为1
                }
                if (sum >= ai) {
                    ans = mid;
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
            if (ans == -1) return false;
            tx[i].t = ans;
        }
        req[i] = tx[i].t;
    }

    stack<int> sta;
    sort(tx + 1, tx + n + 1, cmp);
    vis[0] = true; // 虚拟根节点已访问
    int times = 0;
    for (int i = 1; i <= n; i++) {
        int pos = tx[i].id;
        if (vis[pos]) continue;
        // 向上查找直到已访问的祖先
        while (!vis[pos]) {
            sta.push(pos);
            pos = fa[pos];
        }
        // 按顺序种树
        while (!sta.empty()) {
            pos = sta.top();
            sta.pop();
            times++;
            if (times > req[pos]) {
                return false;
            }
            vis[pos] = true;
        }
    }
    return true;
}

void solve() {
    int l = 1, r = 1e9, mid, ans = -1;
    while (l <= r) {
        mid = (l + r) / 2;
        if (check(mid)) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    printf("%d\n", ans);
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        read(arr[i].a);
        read(arr[i].b);
        read(arr[i].c);
    }
    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        gra[u].push_back(v);
        gra[v].push_back(u);
    }
    dfs(1, 0);
    solve();
    return 0;
}

总结

本题耗时长的关键原因:

1.代码中出现变量引用错误的弱智错误。

2.向上下取整运算的不严谨。

3.边界条件考虑不全。

好题。综合考察了二分与贪心。做的过程中明显感觉贪心过程独立思考难以想出,后续要加强贪心的练习。 CNOICNOICNOI 近几年考的贪心有点意思。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值