题解:洛谷 P2607 骑士

【题目来源】

洛谷:P2607 [ZJOI2008] 骑士

【题目描述】

Z 国的骑士团是一个很有势力的组织,帮会中汇聚了来自各地的精英。他们劫富济贫,惩恶扬善,受到社会各界的赞扬。

最近发生了一件可怕的事情,邪恶的 Y 国发动了一场针对 Z 国的侵略战争。战火绵延五百里,在和平环境中安逸了数百年的 Z 国又怎能抵挡的住 Y 国的军队。于是人们把所有的希望都寄托在了骑士团的身上,就像期待有一个真龙天子的降生,带领正义打败邪恶。

骑士团是肯定具有打败邪恶势力的能力的,但是骑士们互相之间往往有一些矛盾。每个骑士都有且仅有一个自己最厌恶的骑士(当然不是他自己),他是绝对不会与自己最厌恶的人一同出征的。

战火绵延,人民生灵涂炭,组织起一个骑士军团加入战斗刻不容缓!国王交给了你一个艰巨的任务,从所有的骑士中选出一个骑士军团,使得军团内没有矛盾的两人(不存在一个骑士与他最痛恨的人一同被选入骑士军团的情况),并且,使得这支骑士军团最具有战斗力。

为了描述战斗力,我们将骑士按照 \(1\)\(n\) 编号,给每名骑士一个战斗力的估计,一个军团的战斗力为所有骑士的战斗力总和。

【输入】

第一行包含一个整数 \(n\),描述骑士团的人数。

接下来 \(n\) 行,每行两个整数,按顺序描述每一名骑士的战斗力和他最痛恨的骑士。

【输出】

应输出一行,包含一个整数,表示你所选出的骑士军团的战斗力。

【输入样例】

3
10 2
20 3
30 1

【输出样例】

30

【核心思想】

  1. 问题分析:有 \(n\) 个骑士,每个骑士有一个战斗力 \(w_i\) 和一个唯一的厌恶对象 \(h_i\)\(h_i \neq i\))。要求选出一个骑士子集,使得不存在一对骑士 \(x, y\) 满足 \(x\) 厌恶 \(y\) 且二者同时被选(等价于有向边 \(x \to y\) 的两个端点不能同时被选中),且使总战斗力最大。由于每个点出度均为 1,整个图由若干个基环树(每个连通块恰有一个环)组成。问题转化为:在基环树上求最大权独立集(相邻点不能同时选),其中每个点有权值 \(w_i\)

  2. 算法选择

    • 基环树 + 树形 DP(最大权独立集):基环树的结构可看作一个环上挂着若干棵树。处理方法是先找出环,然后枚举断环:在环上选择一条边断开,将基环树变为普通树,做两次树形 DP(分别强制不选环上相邻的两个端点),取最大值。
    • 具体实现:使用无向图(反向建边也可,但无向图更通用),通过 DFS 找出环并标记环边。对每个基环树,分别以环上一条边的两个端点 \(r_1, r_2\) 作为根,进行树形 DP(跳过环边),计算不选根时的最大独立集,两次结果取最大值,累加到总答案。
  3. 关键步骤

    • 建图:将每对 \((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\)),取最大值作为该基环树的贡献,累加到答案。
    • 输出:总战斗力最大值。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个节点和边在找环和 DP 中各被访问一次。
    • 空间复杂度:\(O(n)\),存储邻接表和 DP 数组。
  5. 基环树 + 树形 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
posted @ 2026-09-02 15:35  团爸讲算法  阅读(14)  评论(0)    收藏  举报