牛客网-2018 美团 CodeM 编程大赛-初赛 A 轮-3-城市漫游

ACM模版

描述


题解

这个题出的十分好,让我回忆起了遗忘已久的一些东西。

首先,题意是给定一个树, n n 个结点, n 1 条边,每条边有时间花费 zi z i ,给定若干组起点 S S T ,要求从 S S T 时,必须每条边都经过至少 li l i 次,问最少花费。

这里,我们首先很容易想到的是应该和 S S T 的最短路径(这里的最短路径不考虑边的重复次数)有关,并且题意中说到 min(li)>0 m i n ( l i ) > 0 ,也就是数据保证每条边都必须经过一次,所以我们可以将所有边分为两种,一种是在最短路径上的,一种是不在最短路径上的。对于前者,如果他在最短路径上,我们很容易实现每条边经过恰好 li l i 次;而对于后者,则不能,因为我们在离开最短路径去其他路径上时必须重新返回到最短路径上,所以对于这些不在最短路径上的边时,我们必然经过偶数次,那么当 li%2==0 l i % 2 == 0 时,没有什么疑问,那就是经过 li l i 次,而当 li%2==1 l i % 2 == 1 时,我们需要经过它 li+1 l i + 1 次才行。

说到这里,其实已经很简单了,只剩下一个问题,就是如何求 S S T 最短路径的长度,因为这是一棵树,所以最短路径唯一,我们可以通过 LCA L C A 来求,公式为 S S 到根的距离加上 T 到根的距离减去二倍的 LCA(S,T) L C A ( S , T ) 到根的距离。

此时,两个部分的全部考虑完毕,已经能够完美 AC A C 了,不过这里有一个小小的技巧,我们可以首先将所有的路径都考虑为不在最短路径上,所以都考虑为偶数次,然后建树时,将所有 li%2==1 l i % 2 == 1 的路径的花费置为 costi − c o s t i ,这样再去求最短路径上的代价,相加即可,完美抵消最短路径上多走的那些路。

这里稍微扩展一下,如果某些路径的花费是可变的,那么在根据公式求最短路径上的花费时,就要用到树链剖分 + + 树状数组了。

这里我的代码写的略微复杂了,原本只需要 L C A 算法即可, 这里我用到了树链剖分和树状数组,多此一举了,完全没有必要,因为这个题路径的花费是不可变的(在以前代码上改的代码,所以懒得修改了)。

代码

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>

#define lson rt << 1
#define rson lson | 1

using namespace std;

typedef long long ll;

const int MAXN = 1e5 + 10;
const int MOD = 1e9 + 7;

struct node
{
    int id;
    int to;
    ll cost;
} Q;

struct nn
{
    int l, r, mn;
} PP[MAXN << 4];

int n, cnt;
int x[MAXN], y[MAXN];
int wx[MAXN][2];
int pre[MAXN << 2];
int front_edge[MAXN][2];
ll z[MAXN], tree[MAXN << 2];
bool vis[MAXN << 1];
vector<node> vn[MAXN];

void add_edge(int x_, int y_, ll z_, int id)
{
    Q.cost = z_;
    Q.id = id;
    Q.to = y_;
    vn[x_].push_back(Q);
    Q.to = x_;
    vn[y_].push_back(Q);
}

int lowit(int x)
{
    return x & (-x);
}

void add(int a, ll b)
{
    for (; a <= n; a += lowit(a))
    {
        tree[a] += b;
    }
}

ll query(int rt)
{
    ll ans = 0;
    for (; rt; rt -= lowit(rt))
    {
        ans += tree[rt];
        ans %= MOD;
    }

    return ans;
}

void create(int l, int r, int rt)
{
    PP[rt].l = l;
    PP[rt].r = r;

    if (l == r)
    {
        PP[rt].mn = wx[pre[l]][0];
        return ;
    }

    int m = (l + r) >> 1;
    create(l, m, lson);
    create(m + 1, r, rson);
    PP[rt].mn = min(PP[lson].mn, PP[rson].mn);
}

int LCA(int l, int r, int rt)
{
    if (l == PP[rt].l && r == PP[rt].r)
    {
        return PP[rt].mn;
    }

    int m = (PP[rt].l + PP[rt].r) >> 1;
    if (l > m)
    {
        return LCA(l, r, rson);
    }
    else if (r <= m)
    {
        return LCA(l, r, lson);
    }
    else
    {
        return min(LCA(l, m, lson), LCA(m + 1, r, rson));
    }
}

void dfs(int rt)
{
    vis[rt] = false;
    wx[rt][0] = ++cnt;
    pre[cnt] = rt;

    for (int i = 0; i < vn[rt].size(); i++)
    {
        if (vis[vn[rt][i].to])
        {
            front_edge[vn[rt][i].id][0] = cnt;
            add(cnt, vn[rt][i].cost);

            dfs(vn[rt][i].to);

            front_edge[vn[rt][i].id][1] = cnt;
            add(cnt, -vn[rt][i].cost);
            wx[rt][1] = ++cnt;
            pre[cnt] = rt;
        }
    }

    wx[rt][1] = cnt;
    pre[cnt] = rt;
}

ll m;

int main()
{
    scanf("%d", &n);

    ll sum = 0, l;
    for (int i = 1; i < n; i++)
    {
        scanf("%d%d%lld%lld", &x[i], &y[i], &z[i], &l);

        // 先每条边跑偶数次,且保证至少 l 次
        if (l % 2 == 0)
        {
            sum += z[i] * l;
            sum %= MOD;
            l = 1;
        }
        else
        {
            sum += z[i] * (l + 1);
            sum %= MOD;
            l = -1;
        }
        z[i] *= l;                      // 对于奇数的情况多跑了一次

        add_edge(x[i], y[i], z[i], i);  // 初始建树
    }

    int S, T, tmp;
    scanf("%lld", &m);
    // n == 1 时结果均为 0
    if (n == 1)
    {
        while (m--)
        {
            scanf("%d%d", &S, &T);
            printf("0\n");
        }

        return 0;
    }

    memset(vis, true, sizeof(vis));
    memset(tree, 0, sizeof(tree));
    n = 2 * n - 2;
    cnt = 0;

    dfs(1);
    create(1, n, 1);

    while (m--)
    {
        scanf("%d%d", &S, &T);

        S = wx[S][0];
        T = wx[T][0];
        if (S < T)
        {
            tmp = LCA(S, T, 1);
        }
        else
        {
            tmp = LCA(T, S, 1);
        }

        ll ans = query(S - 1) - 2 * query(tmp - 1) + query(T - 1);
        ans += sum;
        ans %= MOD;
        ans += MOD;
        ans %= MOD;
        printf("%lld\n", ans);
    }

    return 0;
}
全部评论

相关推荐

HR_丸山彩同学:你的项目描述里,系统设计讲了很多:MemCube是什么、三级存储架构怎么设计、四种遗忘策略分别是什么。这些面试的时候讲没问题,但简历上不需要这么细。 简历要突出的是影响力,不是实现细节。面试官看简历的时候想知道的是「这个项目有多大价值」,不是「这个项目具体怎么实现的」。实现细节是面试时候聊的 怎么改:技术细节可以精简为一句「采用三级存储架构+四种遗忘策略」,把省出来的篇幅用来写影响力。比如:项目有没有开源?有没有写成技术博客?有没有被别人使用过? 校园经历没有任何信息量,任何人都可以写这句话,写了等于没写。更关键的是,你投的是技术岗,校园活动经历本来就不是加分项。如果非要写,必须写出具体的数字和成果。如果你没有这些数字,那就老老实实删掉 「端到端耗时缩减30-40%」要给出确切数字和绝对值。从1000ms降到600ms是降了40%,从100ms降到60ms也是降了40%,但这两个含义完全不一样。其他也是,涉及到数据,准备好证据,口径统一,面试会问 「熟练」「熟悉」「了解」混在一起用,读起来很乱。而且「了解前端需求」最好改成「具备前后端协作经验」
点赞 评论 收藏
分享
bg双非本科,方向是嵌入式。这次秋招一共拿到了&nbsp;8&nbsp;个&nbsp;offer,最高年包&nbsp;40w,中间也有一段在海康的实习经历,还有几次国家级竞赛。写这篇不是想证明什么,只是想把自己走过的这条路,尽量讲清楚一点,给同样背景的人一个参考。一、我一开始也很迷茫刚决定走嵌入式的时候,其实并没有一个特别清晰的规划。网上的信息很零散,有人说一定要懂底层,有人说项目更重要,也有人建议直接转方向。很多时候都是在怀疑:1.自己这种背景到底有没有机会2.现在学的东西到底有没有用3.是不是已经开始晚了这些问题,我当时一个都没答案。二、现在回头看,我主要做对了这几件事第一,方向尽早确定,但不把自己锁死。我比较早就确定了嵌入式这个大方向,但具体做哪一块,是在项目、竞赛和实习中慢慢调整的,而不是一开始就给自己下结论。第二,用项目和竞赛去“证明能力”,而不是堆技术名词。我不会刻意追求学得多全面,而是确保自己参与的每个项目,都能讲清楚:我负责了什么、遇到了什么问题、最后是怎么解决的。第三,尽早接触真实的工程环境。在海康实习的那段时间,对我触动挺大的。我开始意识到,企业更看重的是代码结构、逻辑清晰度,以及你能不能把事情说清楚,而不只是会不会某个知识点。第四,把秋招当成一个需要长期迭代的过程。简历不是一次写完的,面试表现也不是一次就到位的。我会在每次面试后复盘哪些问题没答好,再针对性补。三、我踩过的一些坑现在看也挺典型的:1.一开始在底层细节上纠结太久,投入产出比不高2.做过项目,但前期不会总结,导致面试表达吃亏3.早期有点害怕面试,准备不充分就去投这些弯路走过之后,才慢慢找到节奏。四、给和我背景相似的人一点建议如果你也是双非,准备走嵌入式,我觉得有几件事挺重要的:1.不用等“准备得差不多了”再投2.项目一定要能讲清楚,而不是做完就算3.不要只盯着技术,多关注表达和逻辑很多时候,差的不是能力,而是呈现方式。五、写在最后这篇总结不是标准答案,只是我个人的一次复盘。后面我会陆续把自己在嵌入式学习、竞赛、实习和秋招中的一些真实经验拆开来讲,希望能对后来的人有点帮助。如果你正好也在这条路上,希望你能少走一点弯路。
x_y_z1:蹲个后续
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务