【题解】P1453 - 城市环路
题目大意
给定一个包含 \(n\) 个点、\(n\) 条边的单圈图和实数 \(k\)。已知每个结点有其点权 \(p_i\),结点 \(i\) 的贡献为 \(p_i \times k\)。现在要求在该图中选出若干结点,使得这些结点中没有被一条边直接连接的两个结点并且它们的贡献之和最大。
\(1 \leq n \leq 10^5, 1 \leq p_i \leq 10^4, 0 \leq k \leq 10^4\)
解题思路
从题目中的 单圈图 以及 \(n\) 个点、\(n\) 条边 可知题目给出的图是一棵 基环树。基环树定义为一个包含 \(n\) 个点和 \(n\) 条边并且没有重边和自环的图,或者是在一棵大小为 \(n\) 的树中插入一条未出现的边所产生的带环图。它的特点是图中包含且仅包含一个环。
基环树可以根据边大致分类,其中特殊的基环树主要分为三种:
-
基环树上的所有边都是 无向边,此时基环树为 基环无向树
-
基环树上的所有边都是 出边,即对于任意结点 \(i\),与结点 \(i\) 相连的边总是由结点 \(i\) 连向任意其他结点,此时基环树为 基环外向树
-
基环树上的所有边都是 入边,即对于任意结点 \(i\),与结点 \(i\) 相连的边总是由任意其他结点连向结点 \(i\),此时基环树为 基环内向树
对于这道题而言,我们可以采用 二次 \(dp\) 法 来解决。我们可以用并查集找出基环树上的环的任意一条边,然后将这条边断开。将这条边断开以后,基环树会变成一棵普通的树。设断边的两端点分别为 \(u, v\),此时在断边后产生的新树分别以 \(u, v\) 为根结点进行树形 \(dp\)。这样问题就变成了在新树上求最大独立集。
整体的 \(dp\) 思路类似于 P1352。设 \(dp_{u, 0}\) 表示 不选中结点 \(u\) 时,在结点 \(u\) 的子树内按规则选中结点产生的最大点权和;\(dp_{u, 1}\) 表示 选中结点 \(u\) 时,在结点 \(u\) 的子树内按规则选中结点产生的最大点权和。显然当不选中结点 \(u\) 时,它的子结点可以选中或不选中,因此 \(dp_{u, 0} = \sum\limits \max(dp_{v, 0}, dp_{v, 1}), (u, v) \in E\)。当选中结点 \(u\) 时,它的所有子结点都不能被选中,所以 \(dp_{u, 1} = p_u + \sum\limits dp_{v, 0}, (u, v) \in E\)。
证明:因为已经断开结点 \(u, v\) 之间的连边,所以在产生的新树上不会存在环。此时在新树上求最大独立集,满足除 \(u, v\) 外一定不会同时选中一条边的两个端点。此时最终状态分别取 \(dp_{s, 0}\) 和 \(dp_{t, 0}\),则 \(s\) 一定不会贡献 \(dp_{s, 0}\),\(t\) 此时可以任意处理,\(dp_{t, 0}\) 同理。故而最终答案满足不会同时选中同一条边的两个端点。
具体找环可以使用 \(dfs\)、拓扑排序或者并查集等。这里提供一种用并查集来维护的方法:在输入无向图的时候维护点之间的连通性,当输入的边 \((u, v)\) 两端点属于相同连通块的时候,说明 \((u, v)\) 在环上。此时不添加这条边,最终形成的树相当于完整的基环树删除了这一条边。顺便记录一下这条边的端点作为 \(dp\) 的根结点即可。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
struct node
{
int to, nxt;
} edge[maxm];
int n, cnt, ans;
int head[maxn], p[maxn];
int fa[maxn], dp[maxn][2];
double k;
void add_edge(int u, int v)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
void dfs(int u, int fa)
{
dp[u][0] = 0;
dp[u][1] = p[u];
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (v != fa)
{
dfs(v, u);
dp[u][0] += max(dp[v][1], dp[v][0]);
dp[u][1] += dp[v][0];
}
}
}
int get(int x)
{
if (fa[x] == x)
return x;
return fa[x] = get(fa[x]);
}
void merge(int x, int y)
{
x = get(x);
y = get(y);
if (x != y)
fa[y] = x;
}
int main()
{
int u, v, s, t;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &p[i]);
fa[i] = i;
}
for (int i = 1; i <= n; i++)
{
scanf("%d%d", &u, &v);
u++, v++;
if (get(u) == get(v))
{
s = u, t = v;
continue;
}
merge(u, v);
add_edge(u, v);
add_edge(v, u);
}
scanf("%lf", &k);
dfs(s, 0);
ans = max(ans, dp[s][0]);
dfs(t, 0);
ans = max(ans, dp[t][0]);
printf("%.1lf\n", ans * k);
return 0;
}