看板娘加载较慢请耐心等待qwq~~~

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;
}

posted @ 2019-10-19 00:25  椎名·六花  阅读(87)  评论(0编辑  收藏  举报