zoukankan      html  css  js  c++  java
  • Luogu4381 BZOJ1791[IOI2008]Island

    Luogu4381 BZOJ1791[IOI2008]Island

    本篇博客的程序在洛谷以及darkBZOJ通过,但是使用的是DFS,在BZOJ可能导致爆栈。(不过BZOJ爆炸了我也没办法测)

    题面

    你准备浏览一个公园,该公园由 (N) 个岛屿组成,当地管理部门从每个岛屿 (i) 出发向另外一个岛屿建了一座长度为 (L_i) 的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:

    • 可以自行挑选一个岛开始游览。
    • 任何一个岛都不能游览一次以上。
    • 无论任何时间,你都可以由当前所在的岛 (S) 去另一个从未到过的岛 (D)。从 (S)(D) 有如下方法:
      • 步行:仅当两个岛之间有一座桥时才有可能。对于这种情况,桥的长度会累加到你步行的总距离中。
      • 渡船:你可以选择这种方法,仅当没有任何桥和以前使用过的渡船的组合可以由 (S) 走到 (D) (当检查是否可到达时,你应该考虑所有的路径,包括经过你曾游览过的那些岛)。

    注意,你不必游览所有的岛,也可能无法走完所有的桥。
    请你编写一个程序,给定 (N) 座桥以及它们的长度,按照上述的规则,计算你可以走过的桥的长度之和的最大值。

    通过对题面的分析,我们发现给出的图是一个基环树森林。((n)个点(n)条边不保证图联通)
    因此我们的答案便是每棵基环树上长度最大的简单路径。这个路径就是基环树的直径。
    这个问题就被转化成了求基环树的直径。通过分析我们发现基环树的直径有两种情况:

    • 位于基环树上某一棵树上。
    • 基环树上两颗树的链加上他们之间环上的路径。

    求第一种情况,(DP)求即可。
    对于第二种情况,我们把环断成链进行操作。


    具体细节:

    1.找环
    这道题找环不能记(father),因为会有这个样子的环:
    这个
    如果记(father)这个环就跑不到了。
    我使用了直接标记边的方法以防止跑到反向边。
    在建边时题目给出的正向边的id都是偶数,反向边都是奇数,这样就不会跑反向边。

        for (register int i = head[x]; i; i = e[i].nxt)
        {
            if (i & 1)
                continue;
        }
    

    我们发现题目原本是建立双向道路这样无法遍历整个图,但我们跑环不需要遍历啊,所以并查集维护一下就好了。

    class FIND
    {
    public:
        int fa[MAXN];
        inline int find(int x)
        {
            return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
        }
        inline void merge(int x, int y)
        {
            x = find(x);
            y = find(y);
            if (x != y)
                fa[y] = x;
        }
        inline void init()
        {
            for (register int i = 1; i <= n; ++i)
                fa[i] = i;
        }
    } fd;
    
    if (fd.find(i) == i)
    {
        now = i;
        rd[0].push_back(now);
        top = 0;
        v[0] = 0;
        dfs_round(i);
    } 
    

    2.一棵树的直径
    使用DP法求树的直径,同时维护树的深度。

    void dp(int x)
    {
        v[x] = 1;
        for (register int i = head[x]; i; i = e[i].nxt)
        {
            register int y = e[i].ver;
            if (v[y] || rond[y])
                continue;
            dp(y);
            d[now] = max(d[now], f[x] + f[y] + e[i].edge);
            f[x] = max(f[x], f[y] + e[i].edge);
            l[now] = max(l[now], f[x]);
        }
    }
    

    3.基环树的直径
    第一种在一棵树上的情况已经求出,现在计算链加路径的情况。
    环上问题使用DP求解将环断成链并复制成两倍。
    使用(L[i])表示以(i)为根的树的最长链长度,(f[i])表示(i)到断开的点在环上的路径长度。
    枚举右端点(r),当左端点是(l)时的答案(ans = max(ans,L[r] + L[l] + f[r] - f[l]))
    也就是说,我们要维护出在合法范围内,(L[i] - f[i])最小的点。
    枚举右端点,使用单调递增的单调队列进行维护左端点。
    详见环路运输

    • 队头状态不合法,出队
    • 当前队头元素便是最优左端点,更新答案
    • 如果队尾元素大于当前元素,弹队尾
    • 将当前元素入队
        for (register int i = 1; i <= len; ++i)
            g[i] = g[i + len] = l[s[i]];
        ans = 0;
        for (register int i = 1; i < len * 2; ++i)
        {
            while (q.size() && i - q.front() >= len)
                q.pop_front();
            if (q.size())
            {
                ans = max(ans, f[i] - f[q.front()] + g[i] + g[q.front()]);
            }
            while (q.size() && g[i] - f[i] > g[q.back()] - f[q.back()])
                q.pop_back();
            q.push_back(i);
        }
    

    大体就是这样,具体的在代码里有注释。
    另外特别注意一点,由于每次dfs只是一个连通块,并不会遍历整张图,所以并没有必要dfs一遍就清空vis数组,这样会导致TLE,而是应该当前图被完全遍历之后清空vis数组。

    放代码

    #include <bits/stdc++.h>
    using namespace std;
    //快读
    namespace fdata
    {
    inline char nextchar()
    {
        static const int BS = 1 << 21;
        static char buf[BS], *st, *ed;
        if (st == ed)
            ed = buf + fread(st = buf, 1, BS, stdin);
        return st == ed ? -1 : *st++;
    }
    template <typename Y>
    inline void poread(Y &ret)
    {
        ret = 0;
        char ch;
        while (!isdigit(ch = nextchar()))
            ;
        do
            ret = ret * 10 + ch - '0';
        while (isdigit(ch = nextchar()));
    }
    #undef nextcar
    } // namespace fdata
    using fdata::poread;
    const int MAXN = 1e6 + 10;
    int n;
    struct node
    {
        int nxt, ver, edge;
    } e[MAXN << 1];
    int head[MAXN], tot = 1;
    inline void add(const int &x, const int &y, const int &z)
    {
        e[++tot].ver = y;
        e[tot].edge = z;
        e[tot].nxt = head[x];
        head[x] = tot;
    }
    //并查集 没错我封装了
    class FIND
    {
    public:
        int fa[MAXN];
        inline int find(int x)
        {
            return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
        }
        inline void merge(int x, int y)
        {
            x = find(x);
            y = find(y);
            if (x != y)
                fa[y] = x;
        }
        inline void init()
        {
            for (register int i = 1; i <= n; ++i)
                fa[i] = i;
        }
    } fd;
    #define int long long
    //双端队列 没错我自己封装的
    class QUE
    {
    private:
        long long q[MAXN << 1];
        int l = 1, r;
    
    public:
        inline bool size()
        {
            return r >= l;
        }
        inline void init()
        {
            l = 1, r = 0;
        }
        inline void push_back(const long long x)
        {
            q[++r] = x;
        }
        inline long long back()
        {
            return q[r];
        }
        inline void pop_back()
        {
            --r;
        }
        inline void pop_front()
        {
            ++l;
        }
        inline long long front()
        {
            return q[l];
        }
    } q;
    bool v[MAXN << 1];
    int sta[MAXN], top;
    int rond[MAXN], now;
    //使用vector记录环上的点
    vector<int> rd[MAXN];
    //找环
    inline bool dfs_round(int x)
    {
        //入栈
        sta[++top] = x;
        v[x] = 1;
        for (register int i = head[x]; i; i = e[i].nxt)
        {
            //判断是不是反向边
            if (i & 1)
                continue;
            register int y = e[i].ver;
            //当前点跑到过  已经跑完了这个环 栈中到当前点点之间的点就是环上的点
            if (v[y])
            {
                register int z = 0;
                do
                {
                    z = sta[top--];
                    assert(z);
                    rd[now].push_back(z);
                    rond[z] = now;
                } while (z != y);
                return 1;
            }
            if (dfs_round(y))
                return 1;
        }
        sta[top--] = 0;
        return 0;
    }
    long long f[MAXN << 1], s[MAXN << 1], d[MAXN], l[MAXN << 1];
    long long g[MAXN << 1];
    //求树的直径
    void dp(int x)
    {
        v[x] = 1;
        for (register int i = head[x]; i; i = e[i].nxt)
        {
            register int y = e[i].ver;
            if (v[y] || rond[y])
                continue;
            dp(y);
            d[now] = max(d[now], f[x] + f[y] + e[i].edge);
            f[x] = max(f[x], f[y] + e[i].edge);
            l[now] = max(l[now], f[x]);
        }
    }
    long long ans;
    int len;
    //跑环
    inline void dfs(int x, int nw)
    {
        for (register int i = head[x]; i; i = e[i].nxt)
        {
            register int y = e[i].ver;
            if (v[i] || !rond[y])
                continue;
            v[i] = v[i ^ 1] = 1;
            //边的长度
            f[nw] = e[i].edge;
            //环上的点,顺序记录便于DP
            s[nw] = y;
            dfs(y, nw + 1);
        }
    }
    /*计算答案*/
    inline long long sov(const int &now)
    {
        int len = rd[now].size();
        s[1] = rd[now][0];
        dfs(rd[now][0], 2);
        q.init();
        //断环成链并复制
        for (register int i = 2; i <= len; ++i)
            f[i + len + 1 - 1] = f[i];
        //求前缀和,O(1)查询区间和
        for (register int i = 1; i <= len * 2; ++i)
            f[i] = f[i - 1] + f[i];
        //将环上每棵树的深度信息复制
        for (register int i = 1; i <= len; ++i)
            g[i] = g[i + len] = l[s[i]];
        ans = 0;
        //DP求第二种情况
        for (register int i = 1; i < len * 2; ++i)
        {
            while (q.size() && i - q.front() >= len)
                q.pop_front();
            if (q.size())
            {
                ans = max(ans, f[i] - f[q.front()] + g[i] + g[q.front()]);
            }
            while (q.size() && g[i] - f[i] > g[q.back()] - f[q.back()])
                q.pop_back();
            q.push_back(i);
        }
        //第一种情况,直接取max
        for (register vector<int>::iterator it = rd[now].begin(); it != rd[now].end(); ++it)
        {
            ans = max(ans, d[*it]);
        }
        return ans;
    }
    signed main()
    {
        poread(n);
        fd.init();
        for (register int i = 1, y, z; i <= n; ++i)
        {
            //建图 维护连通性
            poread(y), poread(z);
            add(i, y, z);
            add(y, i, z);
            fd.merge(i, y);
        }
        for (register int i = 1; i <= n; ++i)
        {
            //遍历每一个连通块 求环
            if (fd.find(i) == i)
            {
                now = i;
                rd[0].push_back(now);
                top = 0;
                v[0] = 0;
                dfs_round(i);
            }
        }
        //整张图完全遍历 清空vis
        memset(v, 0, sizeof(v));
        //对每一个环上的点为根的树求直径和深度
        for (vector<int>::iterator it = rd[0].begin(); it != rd[0].end(); ++it)
        {
            for (register vector<int>::iterator jt = rd[*it].begin(); jt != rd[*it].end(); ++jt)
            {
                now = *jt;
                dp(*jt);
            }
        }
        memset(v, 0, sizeof(v));
        long long ANs = 0;
        //对每一个连通块求解并统计答案
        for (vector<int>::iterator it = rd[0].begin(); it != rd[0].end(); ++it)
        {
            ANs += sov(*it);
        }
        cout << ANs << endl;
        return 0;
    }
    
    
  • 相关阅读:
    5.2 spring5源码--spring AOP源码分析三---切面源码分析
    5.2 spring5源码--spring AOP源码分析二--切面的配置方式
    在Dubbo中使用Zookeeper入门案例
    Dubbo直连方式改造
    Dubbo直连方式
    16.3.3 对矢量可执行的其它操作
    16.3.2 可对矢量(vector)执行的操作
    16.3 标准模板库
    16.2.2 有关智能指针的注意事项
    16.2.1 使用智能指针
  • 原文地址:https://www.cnblogs.com/Shiina-Rikka/p/11701492.html
Copyright © 2011-2022 走看看