题解:洛谷 P2607 骑士
【题目来源】
【题目描述】
Z 国的骑士团是一个很有势力的组织,帮会中汇聚了来自各地的精英。他们劫富济贫,惩恶扬善,受到社会各界的赞扬。
最近发生了一件可怕的事情,邪恶的 Y 国发动了一场针对 Z 国的侵略战争。战火绵延五百里,在和平环境中安逸了数百年的 Z 国又怎能抵挡的住 Y 国的军队。于是人们把所有的希望都寄托在了骑士团的身上,就像期待有一个真龙天子的降生,带领正义打败邪恶。
骑士团是肯定具有打败邪恶势力的能力的,但是骑士们互相之间往往有一些矛盾。每个骑士都有且仅有一个自己最厌恶的骑士(当然不是他自己),他是绝对不会与自己最厌恶的人一同出征的。
战火绵延,人民生灵涂炭,组织起一个骑士军团加入战斗刻不容缓!国王交给了你一个艰巨的任务,从所有的骑士中选出一个骑士军团,使得军团内没有矛盾的两人(不存在一个骑士与他最痛恨的人一同被选入骑士军团的情况),并且,使得这支骑士军团最具有战斗力。
为了描述战斗力,我们将骑士按照 \(1\) 至 \(n\) 编号,给每名骑士一个战斗力的估计,一个军团的战斗力为所有骑士的战斗力总和。
【输入】
第一行包含一个整数 \(n\),描述骑士团的人数。
接下来 \(n\) 行,每行两个整数,按顺序描述每一名骑士的战斗力和他最痛恨的骑士。
【输出】
应输出一行,包含一个整数,表示你所选出的骑士军团的战斗力。
【输入样例】
3
10 2
20 3
30 1
【输出样例】
30
【核心思想】
-
问题分析:有 \(n\) 个骑士,每个骑士有一个战斗力 \(w_i\) 和一个唯一的厌恶对象 \(h_i\)(\(h_i \neq i\))。要求选出一个骑士子集,使得不存在一对骑士 \(x, y\) 满足 \(x\) 厌恶 \(y\) 且二者同时被选(等价于有向边 \(x \to y\) 的两个端点不能同时被选中),且使总战斗力最大。由于每个点出度均为 1,整个图由若干个基环树(每个连通块恰有一个环)组成。问题转化为:在基环树上求最大权独立集(相邻点不能同时选),其中每个点有权值 \(w_i\)。
-
算法选择:
- 基环树 + 树形 DP(最大权独立集):基环树的结构可看作一个环上挂着若干棵树。处理方法是先找出环,然后枚举断环:在环上选择一条边断开,将基环树变为普通树,做两次树形 DP(分别强制不选环上相邻的两个端点),取最大值。
- 具体实现:使用无向图(反向建边也可,但无向图更通用),通过 DFS 找出环并标记环边。对每个基环树,分别以环上一条边的两个端点 \(r_1, r_2\) 作为根,进行树形 DP(跳过环边),计算不选根时的最大独立集,两次结果取最大值,累加到总答案。
-
关键步骤:
- 建图:将每对 \((u, v)\) 视为无向边(\(u\) 憎恨 \(v\),则 \(u\) 和 \(v\) 不能同时选,故边为无向约束)。读入每个骑士的战斗力 \(w[i]\) 和憎恨对象 \(to\),添加双向边。
- 找环:从每个未访问节点出发,DFS 遍历基环树,通过记录父边避免走回头路,当遇到已访问节点时,说明找到环,记录环上两个相邻端点 \(r_1, r_2\) 并标记环边。
- 树形 DP(最大权独立集):设计状态 \(f[u][0/1]\),\(f[u][0]\) 表示不选节点 \(u\) 时,以 \(u\) 为根的子树的最大权值和;\(f[u][1]\) 表示选 \(u\) 时的最大权值和。转移:
- \(f[u][0] = \sum_{v \in son(u)} \max(f[v][0], f[v][1])\)(子节点可选可不选)
- \(f[u][1] = w[u] + \sum_{v \in son(u)} f[v][0]\)(子节点必须不选)
- 断环处理:对于找到的环,分别强制不选 \(r_1\) 和 \(r_2\)(相当于断开环上边 \(r_1 \leftrightarrow r_2\)),执行两次树形 DP(跳过环边和父边),得到 \(res_1\)(不选 \(r_1\))和 \(res_2\)(不选 \(r_2\)),取最大值作为该基环树的贡献,累加到答案。
- 输出:总战斗力最大值。
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个节点和边在找环和 DP 中各被访问一次。
- 空间复杂度:\(O(n)\),存储邻接表和 DP 数组。
-
基环树 + 树形 DP 的核心思想:
- 基环树结构:每个点出度为 1 的有向图,每个弱连通分量恰有一个环,环上每个点挂着一棵(或多棵)有根树。环的存在使得不能直接套用普通树上的 DP,因为环上相邻点有冲突。
- 断环枚举:通过强制不选环上某条边的一个端点,等效于删除这条边,将环变成树,从而可使用标准树形 DP。枚举两种断法,取最优值。
- 最大权独立集:在树上,选点不能相邻,是经典 DP 问题,利用状态 \(0/1\) 转移即可。
- 适用场景:适用于基环树上的带权独立集、最大权闭合图等类似问题,核心是“环上枚举断边 + 树形 DP”的套路。
【算法标签】
提高 #基环树
【代码详解】
// 有向图
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1000005;
int n, sum; // n: 骑士人数, sum: 最大战斗力总和
vector<int> g[N]; // 邻接表存图(每个骑士痛恨的人作为父节点,反向建边)
int r1, r2, u, w[N]; // r1, r2: 环上相邻两点, u: 临时输入, w[i]: 骑士i的战斗力
bool vis[N]; // 标记节点是否已被访问(用于找环)
int f[N][2]; // f[u][0]: u不选时的子树最大和, f[u][1]: u选时的子树最大和
// 从 u 出发,寻找基环树上的环(找到一条反向边即环)
void find(int u, int rt)
{
vis[u] = 1;
for (int v : g[u])
{
if (v == rt) // 找到反向边,记录环上的两个端点
{
r1 = u;
r2 = v;
return;
}
if (vis[v])
continue;
find(v, rt);
}
}
// 树形 DP,计算以 u 为根的子树(不经过 rt)的最大独立集
int dfs(int u, int rt)
{
f[u][0] = 0;
f[u][1] = w[u];
for (int v : g[u])
{
if (v == rt)
continue;
dfs(v, rt);
f[u][0] += max(f[v][0], f[v][1]); // u 不选,则子节点可选可不选
f[u][1] += f[v][0]; // u 选,则子节点不能选
}
return f[u][0]; // 返回不选 u 的情况,用于断开环
}
signed main()
{
scanf("%d", &n);
for (int v = 1; v <= n; v++)
{
scanf("%d%d", &w[v], &u);
g[u].push_back(v); // 反向建边:u 痛恨 v,则 v 是 u 的儿子
}
for (int i = 1; i <= n; i++)
{
if (!vis[i]) // 每个连通块(基环树)处理一次
{
r1 = r2 = 0;
find(i, i); // 找出该基环树上的环
if (r1) // 如果找到了环
{
// 分别强制不选 r1 或 r2,取最大值
int res1 = dfs(r1, r1); // 不选 r1(断开环)
int res2 = dfs(r2, r2); // 不选 r2(断开环)
sum += max(res1, res2);
}
}
}
printf("%lld\n", sum);
return 0;
}
// 无向图
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1000005, M = N * 2; // N: 最大节点数, M: 最大边数(两倍)
int n, sum; // n: 骑士人数, sum: 最终最大战斗力总和
int h[N], e[M], ne[M], idx; // 链式前向星邻接表
int r1, r2, u, w[N]; // r1,r2: 环上两个端点, u: 临时输入, w[i]: 骑士i的战斗力
bool vis[N]; // vis[i]: 节点i是否已访问 (用于找环和DFS)
int be[M]; // be[i]=1 表示边i属于环上的边, 需要跳过
int f[N][2]; // f[u][0]: u不选时的子树最大和, f[u][1]: u选时的子树最大和
// 添加一条无向边 a-b
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
// 从节点 u 出发, 用DFS寻找基环树上的环
// ine: 进入 u 的边的编号 (用于避免原路返回)
// 如果找到环, 记录环端点 r1,r2, 并标记环边, 返回 true
bool find(int u, int ine)
{
vis[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
// 跳过反向边 (即回父节点的边)
if (i == (ine ^ 1))
continue;
int v = e[i];
if (!vis[v])
{
// 继续深搜
if (find(v, i))
return true;
}
else
{
// 遇到已访问节点, 说明找到了环
r1 = u;
r2 = v;
be[i] = 1; // 标记这条环边
be[i ^ 1] = 1; // 标记反向边
return true;
}
}
return false;
}
// 树形DP, 计算以 u 为根的子树 (跳过环边和父边) 的最大独立集
// ine: 进入 u 的边的编号, 用于防止走回头路
// 返回不选 u 时的最大和 f[u][0] (用于断开环时取最大值)
int dfs(int u, int ine)
{
vis[u] = true;
f[u][0] = 0; // u 不选, 初始为0
f[u][1] = w[u]; // u 选, 初始为自身战斗力
for (int i = h[u]; i != -1; i = ne[i])
{
// 跳过父边 (反向边) 以及环边
if (i == (ine ^ 1) || be[i])
continue;
int v = e[i];
dfs(v, i); // 递归处理子节点
// 如果 u 不选, 子节点可选可不选, 取最大值
f[u][0] += max(f[v][0], f[v][1]);
// 如果 u 选, 子节点不能选
f[u][1] += f[v][0];
}
return f[u][0]; // 返回不选 u 的情况, 用于断开环
}
signed main()
{
scanf("%d", &n);
memset(h, -1, sizeof(h)); // 初始化邻接表头指针
// 读入每个骑士的战斗力及其最厌恶的骑士, 建无向图
for (int v = 1; v <= n; v++)
{
scanf("%d%d", &w[v], &u);
add(u, v);
add(v, u);
}
// 遍历所有节点, 处理每个连通块 (基环树)
for (int i = 1; i <= n; i++)
{
if (!vis[i])
{
r1 = r2 = 0;
find(i, -1); // 寻找该基环树上的环
if (r1) // 如果找到了环
{
// 分别强制不选 r1 或 r2, 相当于断开环上的一条边
int res1 = dfs(r1, 0); // 不选 r1, 计算该连通块的最大独立集
int res2 = dfs(r2, 0); // 不选 r2, 计算该连通块的最大独立集
sum += max(res1, res2); // 取两者较大值, 累加到总答案
}
}
}
printf("%lld", sum);
return 0;
}
【运行结果】
3
10 2
20 3
30 1
30
浙公网安备 33010602011771号