[bzoj 1758] [Wc2010]重建计划

如果莫名地TLE了可以看一看本文. 数据经过加强后, 以前许多AC代码会超时.
新站没被百度收录, 所以在这里复制一份, 希望能帮到您.
https://chrt.github.io/2017/04/09/bzoj1758-rebuild/

一棵n个结点, 边带权的无根树, 求包含[L,U]条边的简单路径的最大平均权值 (权值和/边数), 保留3位小数. (n≤10^5, 1≤L≤U≤n-1, 边权≤10^6)

虽然AC花费了比想象中更多的功夫, 但我认为数据加强后这是一道不错的题.

这是一个0-1分数规划, 考虑二分一下. 问题简化为, 判断是否存在一条简单路径, 使得

(v(e1)m)+(v(e2)m)++(v(ek)m)0 (k[L,U])

其中 m 是二分的答案.

路径相关问题点分治是个不错的选择. 遍历重心的每一棵子树, 设当前结点v的深度为 d(v) , 对边权重新赋值后到重心的距离为 l(v) , 需要判断是否存在另一棵子树中的某点 u , 使得

Ld(v)d(u)Ud(v)l(u)l(v)

对于所有深度在 [Ld(v),Ud(v)] 的点, 只用考虑距离的数值最大的那个.

这是RMQ问题, 线段树? 两个log好像不太可行……但还是尝试了一下. 本机开O2 3~4s出解, 但是交上去TLE (也许是因为后面将要说的其他原因). 事实上我写了两遍……手敲编译指令, 打错了, 把源代码覆盖了……神奇的是, 之前不开O2无法出解, 重写之后快了不少 (极限数据11s左右).

二分的那个log大概搞不掉了, 大概要换掉线段树. 发现查询的区间有一个特点 - 长度基本是 UL . 能不能借此优化到均摊 O(1) 呢?

看了题解, 才发觉这就是滑动窗口问题, 单调队列即可搞定. 分别维护之前所有子树和当前子树的 深度-长度 数组, 做完滑动窗口后合并两个数组即可.

仍然TLE. 以为是常数问题, 参照题解, 做了一些诸如把二分套到点分治里面的优化, 但是无济于事. 卡了一天……大概新加的那组数据有玄机? 复制了神犇以前的AC代码, 结果也TLE了.

想到有个Discuss页面, 看到有人跟我有相同的疑惑. 谜底揭晓: 单调队列做滑动窗口的线性复杂度, 自变量是待查询的数组大小. 点分治保证递归的深度为 O(lgn) , 但不保证分出来的子树的大小, 深度相近. 所以可以设计这样的数据: 第一棵子树的大小和深度 (从1开始标号) 为n/2, 其余(n/2-1)棵子树大小和深度为1; 每次点分治的时间复杂度退化为 O(n2lgn) . 将子树按深度从小到大排序再做滑动窗口即可解决这个问题.

正确地实现, 时间复杂度为 O(lgUnlgn) .

#include <bits/stdc++.h>
#define rep(i, a, b) for (int i = (a), i##_end = (b); i < i##_end; ++i)
#define per(i, a, b) for (int i = (a), i##_end = (b); i >= i##_end; --i)

using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int N = 1e5;
const double inf = 1e16;
struct Edge {
    int v, w, nxt;
} E[N*2];
int adj[N + 1];

bool g[N + 1];
ii subtree[N];
int depth[N], top, sz[N + 1];
double a[N], b[N];
int tot_a, tot_b;
int L, U;

int get_centroid(int u, int p, int n);
void counting_sort(int k[], ii v[], int m, int n);

void cal_b(int u, int p, double m, int dep, double len)
{
    if (dep > U) return;

    if (tot_b < dep+1) {
        b[dep] = len;
        tot_b = dep+1;
    } else
        b[dep] = max(b[dep], len);

    for (int i = adj[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if (g[v] || v == p) continue;
        cal_b(v, u, m, dep + 1, len + E[i].w - m);
    }
}

void cal_a()
{
    rep (i, 1, tot_a)
        a[i] = max(a[i], b[i]);
    rep (i, tot_a, tot_b)
        a[i] = b[i];
    tot_a = tot_b;
}

bool check()
{
    static int Q[N];
    int front = 0, rear = 0, j = 0;
    per (i, tot_b-1, 0) { // [L-i, U-i]
        while (j < tot_a && j <= U-i) {
            while (rear > front && a[Q[rear-1]] <= a[j]) --rear;
            Q[rear++] = j++;
        }
        while (front < rear && Q[front] < L-i) ++front;
        if (front < rear && a[Q[front]] + b[i] >= 0)
            return true;
    }
    return false;
}

bool judge(int x, double m)
{
    tot_a = 1;
    rep (i, 0, top) {
        int y = subtree[i].first;

        tot_b = 1;
        cal_b(y, x, m, 1, subtree[i].second - m);

        if (check()) return true;
        cal_a();
    }
    return false;
}

int cal_depth(int u, int p)
{
    int d = -1;

    for (int i = adj[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if (!g[v] && v != p)
            d = max(d, cal_depth(v, u));
    }

    return d+1;
}

void solve(int u, int n, double& l, double _r)
{
    if (n-1 < L) return;

    int x = get_centroid(u, 0, n), maxd = 0;
    g[x] = true;

    top = 0;
    for (int i = adj[x]; i; i = E[i].nxt) {
        int v = E[i].v;
        if (!g[v]) {
            subtree[top] = ii(v, E[i].w);
            maxd = max(maxd, depth[top++] = min(U, cal_depth(v, u)+1));
        }
    }

    counting_sort(depth, subtree, maxd + 1, top);

    double r = _r;
    while (r-l >= 1e-4) {
        double m = (l+r)/2;
        if (judge(x, m)) l = m;
        else r = m;
    }

    for (int i = adj[x]; i; i = E[i].nxt) {
        int y = E[i].v;
        if (!g[y]) solve(y, sz[y]>sz[x] ? n-sz[x] : sz[y], l, _r);
    }
}

inline void add_edge(int u, int v, int w)
{
    static int ptr = 1;
    E[ptr] = (Edge){v, w, adj[u]};
    adj[u] = ptr++;
}

int main()
{
    int n;
    double l = inf, r = 0;
    scanf("%d%d%d", &n, &L, &U);

    rep (i, 0, n-1) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add_edge(u, v, w);
        add_edge(v, u, w);
        l = min(l, (double)w);
        r = max(r, (double)w);
    }

    solve(1, n, l, r);

    printf("%.3f\n", l);
    return 0;
}

int get_centroid(int u, int p, int n)
{
    int mx = 0, x = 0;
    sz[u] = 1;
    for (int i = adj[u]; i; i = E[i].nxt) {
        int v = E[i].v, ret;
        if (g[v] || v == p) continue;
        if ((ret = get_centroid(v, u, n))) x = ret;
        sz[u] += sz[v];
        mx = max(mx, sz[v]);
    }
    return x ? x : (max(mx, n-sz[u]) <= n/2 ? u : 0);
}

void counting_sort(int k[], ii v[], int m, int n)
{
    static int a[N];
    static ii b[N];
    fill_n(a, m, 0);
    rep (i, 0, n) ++a[k[i]];
    rep (i, 1, m) a[i] += a[i-1];
    per (i, n-1, 0) b[--a[k[i]]] = v[i];
    copy(b, b+n, v);
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: bzoj作为一个计算机竞赛的在线评测系统,不仅可以提供大量的题目供程序员练习和学习,还可以帮助程序员提升算法和编程能力。为了更好地利用bzoj进行题目的学习和刷题,制定一个bzoj做题计划是非常有必要的。 首先,我们需要合理安排时间,每天留出一定的时间来做bzoj的题目。可以根据自己的时间安排,每天挑选适量的题目进行解答。可以先从难度较低的题目开始,逐渐提高难度,这样既能巩固基础知识,又能挑战自己的思维能力。 其次,要有一个计划和目标。可以规划一个每周或每月的题目数量目标,以及每个阶段要学习和掌握的算法知识。可以根据bzoj的题目分类,如动态规划、图论、贪心算法等,结合自己的实际情况,有针对性地选择题目进行学习。 此外,要充分利用bzoj提供的资源。bzoj网站上有很多高质量的题解和优秀的解题代码,可以参考和学习。还有相关的讨论区,可以与其他程序员交流和讨论,共同进步。 最后,要坚持并保持思考。做题不是单纯为了刷数量,更重要的是学会思考和总结。遇到难题时,要有耐心,多思考,多尝试不同的解法。即使不能一次性解出来,也要学会思考和分析解题过程,以及可能出现的错误和优化。 总之,bzoj做题计划的关键在于合理安排时间、制定目标、利用资源、坚持思考。通过有计划的刷题,可以提高算法和编程能力,并培养解决问题的思维习惯,在计算机竞赛中取得更好的成绩。 ### 回答2: bzoj做题计划是指在bzoj这个在线测评系统上制定一套学习和刷题的计划,并且将计划记录在excel表格中。该计划主要包括以下几个方面的内容。 首先是学习目标的设定。通过分析自己的水平和知识缺口,可以设定一个合理的目标,比如每天解决一定数量的题目或者提高特定的算法掌握程度。 其次是题目选择的策略。在excel表格中可以记录下自己选择的题目编号、题目类型和难度等信息。可以根据题目的类型和难度来安排每天的刷题计划,确保自己可以逐步提高技巧和解题能力。 然后是学习进度的记录和管理。将每天的完成情况记录在excel表格中,可以清晰地看到自己的学习进度和任务完成情况。可以使用图表等功能来对学习进度进行可视化展示,更好地管理自己的学习计划。 同时,可以在excel表格的备注栏中记录下每道题目的解题思路、关键和需要复习的知识等信息。这样可以方便自己回顾和总结,巩固所学的知识。 最后,可以将excel表格与其他相关资料进行整合,比如算法教材、题目解析和学习笔记等。这样可以形成一个完整的学习档案,方便自己进行系统的学习和复习。 总之,bzoj做题计划excel的制定和记录可以帮助我们更加有条理和高效地进行学习和刷题。通过合理安排学习目标和题目选择策略,记录学习进度和思路,并整合其他学习资料,我们可以提高自己的解题能力,并在bzoj上取得更好的成绩。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值