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