题解:洛谷 P8186 Redistributing Gifts

【题目来源】

洛谷:P8186 [USACO22FEB] Redistributing Gifts S - 洛谷

【题目描述】

FJ 有 \(N\) 个礼物给他的 \(N\) 头奶牛,这 \(N\) 个礼物和 \(N\) 头奶牛都分别按顺序被标记为从 \(1\)\(N\) 的整数。每头奶牛都有一个愿望单,记录着一个含有 \(N\) 个礼物的排列。比起在愿望单中出现更晚的礼物,奶牛更喜欢先出现在愿望单中的礼物。

因为 FJ 太懒了,他直接把 \(i\) 号礼物分配给了 \(i\) 号奶牛。现在,奶牛们聚在了一起,决定重新分配礼物,以便在重新分配后,每头奶牛都能得到跟原来一样,或是它更喜欢的礼物。

对于每个 \(i\)\(1\le i\le N\)),计算出重新分配后,\(i\) 号奶牛可能拿到的最好的礼物(这个奶牛经过重新分配后能拿到的最喜欢的礼物)。

【输入】

第一行输入 \(N\)。之后 \(N\) 行每行包含一个奶牛的愿望单。保证这 \(N\) 行都是从 \(1\)\(N\) 的排列,即每一行都不按顺序不重不漏地包含 \(1\)\(N\) 中的每个整数。

【输出】

输出 \(N\) 行,第 \(i\) 行输出重新分配后 \(i\) 号奶牛可能得到的最好礼物。

【输入样例】

4
1 2 3 4
1 3 2 4
1 2 3 4
1 2 3 4

【输出样例】

1
3
2
4

【核心思想】

  1. 问题分析\(N\) 头奶牛,每头有愿望单(\(1\)\(N\) 的排列)。初始 \(i\) 号礼物给 \(i\) 号奶牛。重新分配要求每头奶牛得到的礼物不比原来差(即在其愿望单中位置 \(\leq\) 原礼物位置)。求每头奶牛能拿到的最好礼物。本质上是强连通分量 + 偏好图分析问题:将偏好关系建模为有向图,在强连通分量内可循环交换,每头奶牛可获得同 SCC 内它最喜欢的礼物。

  2. 算法选择

    • 偏好图构建:奶牛 \(i\) 的愿望单中,排在 \(i\) 之前的礼物 \(t\) 表示 \(i\) 更喜欢 \(t\),建立边 \(i \to t\)
    • 强连通分量(SCC)分解:正向 DFS 和反向 DFS 求交集得到 SCC
    • 最佳后继选择:在每个 SCC 内,每个节点选择其出边中仍在 SCC 内的第一个节点作为最佳后继
  3. 关键步骤

    • 读取数据:读入 \(N\)\(N\) 个愿望单
    • 建图:对每个奶牛 \(i\),读入愿望单,遇到 \(i\) 之前(不含 \(i\))的所有礼物 \(t\) 建立边 \(i \to t\),同时建立反向边 \(t \to i\)
    • SCC 分解
      • 初始化 whole 为所有节点集合
      • whole 非空:
        • 取节点 \(a\),正向 DFS 得 ndfs,反向 DFS 得 ndfsr
        • 交集 r1 = ndfs ∩ ndfsr 即为一个 SCC
        • 对每个 \(vex \in r1\),遍历其出边,第一个仍在 \(r1\) 内的邻接点即为 best[vex]
        • whole = whole - r1,继续处理剩余节点
    • 输出best[i] 存在则输出,否则输出 \(i\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^3)\),每次 SCC 分解 \(O(N^2)\),最多 \(N\) 个 SCC
    • 空间复杂度:\(O(N^2)\),邻接表存储
  5. SCC + 偏好图的核心思想

    • 偏好关系的传递性:若 \(i\) 喜欢 \(j\)\(j\) 喜欢 \(k\),则通过交换链 \(i\) 可能获得 \(k\)
    • 强连通分量的等价性:SCC 内的节点互相可达,意味着礼物可在 SCC 内任意流通,每头奶牛可获得 SCC 内它最喜欢的礼物
    • 最佳后继的选择:在 SCC 内按愿望单顺序找第一个可达节点,即为该奶牛能获得的最好礼物
    • 自环的排除:愿望单中遇到自身时停止建边,因为"不比原来差"包含原礼物本身
    • 适用于偏好匹配、资源重分配、图分解类问题

【算法标签】

普及+ #强连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 501;  // 最大节点数

int best[MAXN];  // 存储每个节点的最佳后继节点
vector<vector<int>> net(MAXN);  // 正向邻接表
vector<vector<int>> netr(MAXN);  // 反向邻接表
int n;  // 节点总数

/**
 * 深度优先搜索(DFS)实现
 * @param start 起始节点
 * @param rset 存储可达节点的集合
 * @param lnet 使用的邻接表(正向或反向)
 */
void goDFS(int start, set<int>& rset, vector<vector<int>>& lnet)
{
    vector<bool> vis(n + 1);  // 访问标记数组
    stack<int> s;  // DFS栈
    s.push(start);  // 起始节点入栈
    int a;  // 当前处理的节点

    while (!s.empty())
    {
        a = s.top();  // 获取栈顶节点
        rset.insert(a);  // 将当前节点加入可达集合
        s.pop();  // 弹出栈顶

        // 遍历当前节点的所有邻接节点
        for (int b : lnet[a])
        {
            if (!vis[b])  // 如果邻接节点未被访问
            {
                s.push(b);  // 邻接节点入栈
                vis[b] = true;  // 标记为已访问
            }
        }
    }
}

/**
 * 主处理函数,计算每个节点的最佳后继
 */
void goall()
{
    set<int> whole, ndfs, ndfsr, r1, r2;  // 各种辅助集合

    // 初始化包含所有节点的集合
    for (int i = 1; i <= n; i++)
    {
        whole.insert(i);
    }

    // 处理所有强连通分量
    while (!whole.empty())
    {
        int a = *whole.begin();  // 获取集合中的第一个节点

        // 计算正向和反向可达集合
        goDFS(a, ndfs, net);
        goDFS(a, ndfsr, netr);

        // 计算强连通分量(正向和反向可达的交集)
        set_intersection(ndfs.begin(), ndfs.end(),
                        ndfsr.begin(), ndfsr.end(),
                        inserter(r1, r1.begin()));

        // 处理强连通分量中的每个节点
        for (auto vex : r1)
        {
            // 寻找当前节点的最佳后继(在同一个强连通分量中)
            for (auto adj : net[vex])
            {
                if (r1.count(adj) > 0)
                {
                    best[vex] = adj;  // 记录最佳后继
                    break;
                }
            }
        }

        // 从总集合中移除已处理的强连通分量
        set_difference(whole.begin(), whole.end(),
                      r1.begin(), r1.end(),
                      inserter(r2, r2.begin()));
        whole = r2;  // 更新剩余待处理节点

        // 清空临时集合
        ndfs.clear();
        ndfsr.clear();
        r1.clear();
        r2.clear();
    }
}

int main()
{
    cin >> n;  // 输入节点总数

    // 构建邻接表
    for (int i = 1; i <= n; i++)
    {
        bool nodo = false;  // 标记是否遇到自环
        for (int j = 1; j <= n; j++)
        {
            int t;
            cin >> t;  // 输入邻接关系
            if (t == i) nodo = true;  // 检查自环

            // 构建正向和反向邻接表(忽略自环后的边)
            if (!nodo)
            {
                net[i].push_back(t);
                netr[t].push_back(i);
            }
        }
    }

    goall();  // 计算最佳后继

    // 输出结果
    for (int i = 1; i <= n; i++)
    {
        if (best[i] > 0)
            cout << best[i] << endl;  // 输出最佳后继
        else
            cout << i << endl;  // 无后继则输出自身
    }

    return 0;
}

【运行结果】

4
1 2 3 4
1 3 2 4
1 2 3 4
1 2 3 4
1
3
2
4
posted @ 2026-08-17 22:48  团爸讲算法  阅读(10)  评论(0)    收藏  举报