题解:洛谷 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
【核心思想】
-
问题分析:\(N\) 头奶牛,每头有愿望单(\(1\) 到 \(N\) 的排列)。初始 \(i\) 号礼物给 \(i\) 号奶牛。重新分配要求每头奶牛得到的礼物不比原来差(即在其愿望单中位置 \(\leq\) 原礼物位置)。求每头奶牛能拿到的最好礼物。本质上是强连通分量 + 偏好图分析问题:将偏好关系建模为有向图,在强连通分量内可循环交换,每头奶牛可获得同 SCC 内它最喜欢的礼物。
-
算法选择:
- 偏好图构建:奶牛 \(i\) 的愿望单中,排在 \(i\) 之前的礼物 \(t\) 表示 \(i\) 更喜欢 \(t\),建立边 \(i \to t\)
- 强连通分量(SCC)分解:正向 DFS 和反向 DFS 求交集得到 SCC
- 最佳后继选择:在每个 SCC 内,每个节点选择其出边中仍在 SCC 内的第一个节点作为最佳后继
-
关键步骤:
- 读取数据:读入 \(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,继续处理剩余节点
- 取节点 \(a\),正向 DFS 得
- 初始化
- 输出:
best[i]存在则输出,否则输出 \(i\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N^3)\),每次 SCC 分解 \(O(N^2)\),最多 \(N\) 个 SCC
- 空间复杂度:\(O(N^2)\),邻接表存储
-
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
浙公网安备 33010602011771号