P2419 [USACO08JAN] Cow Contest S
题目描述
$ N (1 ≤ N ≤ 100) $ cows, conveniently numbered $ 1 ~ N $ , are participating in a programming contest. As we all know, some cows code better than others. Each cow has a certain constant skill rating that is unique among the competitors.
The contest is conducted in several head-to-head rounds, each between two cows. If cow $ A $ has a greater skill level than cow $ B (1 ≤ A ≤ N; 1 ≤ B ≤ N; A ≠ B) $, then cow $ A $ will always beat cow $ B $ .
Farmer John is trying to rank the cows by skill level. Given a list the results of $ M (1 ≤ M ≤ 4,500) $ two-cow rounds, determine the number of cows whose ranks can be precisely determined from the results. It is guaranteed that the results of the rounds will not be contradictory.
FJ的 N N N( 1 ≤ N ≤ 100 1 \leq N \leq 100 1≤N≤100)头奶牛们最近参加了场程序设计竞赛。在赛场上,奶牛们按 1 , 2 , ⋯ , N 1, 2, \cdots, N 1,2,⋯,N 依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为 A A A 的奶牛的编程能力强于编号为 B B B 的奶牛 ( 1 ≤ A , B ≤ N 1 \leq A, B \leq N 1≤A,B≤N, A ≠ B A \neq B A=B),那么她们的对决中,编号为 A A A 的奶牛总是能胜出。 FJ 想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M M M( 1 ≤ M ≤ 4 , 500 1 \leq M \leq 4,500 1≤M≤4,500)轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。
输入格式
第一行两个用空格隔开的整数 N , M N, M N,M。
第 2 ∼ M + 1 2\sim M + 1 2∼M+1 行,每行为两个用空格隔开的整数 A , B A, B A,B ,描述了参加某一轮比赛的奶牛的编号,以及结果(每行的第一个数的奶牛为胜者)。
输出格式
输出一行一个整数,表示排名可以确定的奶牛的数目。
输入输出样例 #1
输入 #1
5 5
4 3
4 2
3 2
1 2
2 5
输出 #1
2
说明/提示
样例解释:
编号为 2 2 2 的奶牛输给了编号为 1 , 3 , 4 1, 3, 4 1,3,4 的奶牛,也就是说她的水平比这 3 3 3 头奶牛都差。而编号为 5 5 5 的奶牛又输在了她的手下,也就是说,她的水平比编号为 5 5 5 的奶牛强一些。于是,编号为 2 2 2 的奶牛的排名必然为第 4 4 4,编号为 5 5 5 的奶牛的水平必然最差。其他 3 3 3 头奶牛的排名仍无法确定。
思路:
如果要确定排名那得与其余的节点有联系才能确定排名,也就是说这一个点得与其余的点连通才能有联系。
说到连通第一反应应该是并查集,还有Floyd求闭包问题
Floyd的前身应该是DFS,可通过暴力DFS的方法求解
1.统计能战胜i的奶牛数量
2.统计i能战胜的奶牛数量
3.如果满足两者之和为除自己以外的所有点的数量,就说明该点与其余点有联系
DFS:
#include <iostream>
#include <climits>
#include <limits>
#include <vector>
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef std::pair<int, int> PII;
#define rep(i, n) for(int i = 0; i < n; i++)
#define Rep(i, len, n) for(int i = len; i < n; i++)
const int INF = std::numeric_limits<int>::max();
int N, M;
std::vector<std::vector<int>> e;
std::vector<bool> vis;
void DFS(int u) {
vis[u] = true;
for(const int& v : e[u]) if(!vis[v]) DFS(v);
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
std::cin >> N >> M;
e.resize(N + 1);
rep(i, M) {
int a, b;
std::cin >> a >> b;
e[a].push_back(b);
}
int ans = 0;
Rep(i, 1, N + 1) {
int ij = 0;
// 统计能战胜i的奶牛数量
Rep(j, 1, N + 1) {
if(i == j) continue;
vis.assign(N + 1, false);
DFS(j);
if(vis[i]) ij++; // 这个 i 是否被战胜
}
// 统计i能战胜的奶牛数量
vis.assign(N + 1, false);
int ji = 0;
DFS(i);
Rep(j, 1, N + 1) if(i != j && vis[j]) ji++;
if(ji + ij == N - 1) ans++;
}
std::cout << ans << '\n';
return 0;
}
发现DFS复杂度太高,就想用dp优化成Floyd(闭包问题):
如果说 我从 i 到 j 是连通的那便不需要进行操作
如果说 我从 i 到 j 是不连通的,那遍找一个中转点 k 看看是否 i -> k 和 k -> j 都连通,也就是 i -> k -> j。
Floyd:
#include <iostream>
#include <climits>
#include <limits>
#include <vector>
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef std::pair<int, int> PII;
#define rep(i, n) for(int i = 0; i < n; i++)
#define Rep(i, len, n) for(int i = len; i < n; i++)
const int INF = std::numeric_limits<int>::max();
int N, M, maxn = 0;
std::vector<std::vector<int>> f;
inline void Floyd() {
Rep(k, 1, N + 1) {
Rep(i, 1, N + 1) {
Rep(j, 1, N + 1) {
f[i][j] |= f[i][k] & f[k][j];
}
}
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
std::cin >> N >> M;
f.resize(N + 1, std::vector<int>(N + 1, 0));
Rep(i, 1, N + 1) f[i][i] = 0;
rep(i, M) {
int a, b;
std::cin >> a >> b;
f[a][b] = 1;
}
Floyd();
int ans = 0;
Rep(i, 1, N + 1) {
int flag = 1;
// i -> j || j -> i 有一个成立则为真,如果存在该条件为假的,那么flag为假
Rep(j, 1, N + 1) if(i != j) flag &= (f[i][j] | f[j][i]);
ans += flag;
}
std::cout << ans << '\n';
return 0;
}
但是Floyd标准方法求闭包问题复杂度还是到 O ( n 3 ) O(n^3) O(n3)还是不尽人意,在想又没有更优的方式进行计算。我们发现 如果 i 能到 j ,那么 j 能到的点 i 都能到,所以连通性就是与 j 连通的点都与 i 连通。
用 b i t s e t bitset bitset 进行存储,bitset每位只占1个字节,大大减少了空间复杂度,bitset是存储的二进制,直接把 b i t s e t ( i ) bitset(i) bitset(i) 和 b i t s e t ( j ) bitset(j) bitset(j) 异或于,更新 i 能通过的点。
外层循环遍历 j j j ,用 j j j 作为中转点,用 j j j 能到达的点去更新起点 i i i 能到达的点。
为什么外层循环是 j j j 不是 i i i ?
因为 j j j 作为中转点的话因为是终点,会不断更新起点 i i i 的值, j j j 不断变大,每次用的都是更新后的 i i i 的值。
如果说外层循环是 i i i 的话, j j j 每次增加作为中转点的话, j > i j > i j>i 的部分都是用的老值,不会及时更新。
闭包问题(Floyd + bitset优化):
#include <iostream>
#include <climits>
#include <limits>
#include <bitset>
#include <vector>
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef std::pair<int, int> PII;
#define rep(i, n) for(int i = 0; i < n; i++)
#define Rep(i, len, n) for(int i = len; i < n; i++)
const int INF = std::numeric_limits<int>::max();
const int MAXN = 110;
int N, M;
std::vector<std::bitset<MAXN>> cow;
inline void Floyd() {
Rep(j, 1, N + 1) {
Rep(i, 1, N + 1) {
if (cow[i][j]) cow[i] |= cow[j]; // i -> j j能到的点i全都能到
}
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
std::cin >> N >> M;
cow.resize(N + 1);
rep(i, M) {
int a, b;
std::cin >> a >> b;
cow[a][b] = 1;
}
Floyd();
int ans = 0;
Rep(i, 1, N + 1) {
int flag = 1;
Rep(j, 1, N + 1) if(i != j) flag &= (cow[i][j] | cow[j][i]);
ans += flag;
}
std::cout << ans << '\n';
return 0;
}
并查集+拓扑排序:
这个思路拓扑排序起到了什么作用?
判断图中是否存在环:如果一个有向图能够成功进行拓扑排序,那么该图是有向无环图(DAG);如果在拓扑排序过程中发现存在入度始终不为 0 的节点,即无法完成拓扑排序,则说明图中存在环。在代码中,通过检查拓扑排序完成后所有节点的入度是否都为 0 来判断图是否有环
确定节点之间的胜负关系:在拓扑排序的过程中,通过遍历每个节点及其出边,可以确定节点之间的直接胜负关系。例如,当从队列中取出节点u,并遍历其邻接节点v时,将vis[u][v]设置为true,表示u战胜了v。同时,还会更新与u有胜负关系的其他节点与v的胜负关系,即如果k能战胜u,那么k也能战胜v。这样,通过拓扑排序可以完整地构建出整个图中节点之间的胜负关系矩阵vis,为后续确定每个节点是否能确定排名提供依据。
AC code(并查集 + 拓扑):
#include <iostream>
#include <climits>
#include <limits>
#include <vector>
#include <queue>
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef std::pair<int, int> PII;
#define rep(i, n) for(int i = 0; i < n; i++)
#define Rep(i, len, n) for(int i = len; i < n; i++)
const int INF = std::numeric_limits<int>::max();
int N, M;
std::vector<int> fa, sz, d;
std::vector<std::vector<int>> e;
std::vector<std::vector<bool>> vis;
std::queue<int> q;
inline void Init() {
Rep(i, 1, N + 1) fa[i] = i, sz[i] = 1;
}
int Findest(int x) {
return x == fa[x] ? x : fa[x] = Findest(fa[x]);
}
inline void Union(int x, int y) {
int fx = Findest(x), fy = Findest(y);
if(fx == fy) return ;
if(sz[fx] > sz[fy]) std::swap(fx, fy);
fa[fx] = fy;
sz[fy] += sz[fx];
}
bool TopoSort() {
Rep(i, 1, N + 1) if(!d[i]) q.push(i);
while(!q.empty()) {
int u = q.front();
q.pop();
for(const int& v : e[u]) { // u win v lose
vis[u][v] = true;
Rep(k, 1, N + 1) if(vis[k][u]) vis[k][v] = true; // 如果 k赢u 那么k也能赢v
if(--d[v] == 0) q.push(v);
}
}
// 是否成环
Rep(i, 1, N + 1) if(d[i] != 0) return false;
return true;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
std::cin >> N >> M;
fa.resize(N + 1);
sz.resize(N + 1);
e.resize(N + 1);
vis.resize(N + 1, std::vector<bool> (N + 1, false));
d.resize(N + 1, 0);
Init();
rep(i, M) {
int a, b;
std::cin >> a >> b;
e[a].push_back(b);
d[b]++;
Union(a, b);
}
if(!TopoSort()) {
std::cout << 0 << '\n';
return 0;
}
int ans = 0;
Rep(i, 1, N + 1) {
int cnt = 0;
int fx = Findest(i);
Rep(j, 1, N + 1) {
if(i == j) continue;
int fy = Findest(j);
if(fx == fy && (vis[i][j] || vis[j][i])) cnt++; // 在一个集合说明i与j有关系
}
if(cnt == sz[fx] - 1) ans++; // 如果这个点跟剩下的点都有关系的话就说明能确定排名
}
std::cout << ans << '\n';
return 0;
}
/*
*/