[解题报告] AGC016E Poor Turkeys
题意
\(n\) 只火鸡 \((n \le 400)\),\(m\) 个人,每个人都有两个指定的火鸡 \(x_i, y_i\),这 \(m\) 个人会依次进行以下操作。
- 若 \(x_i, y_i\) 都存活,则随机杀死其中的一只。
- 若只有一个存活,则杀死存活的那只。
- 若都死亡,则不进行操作。
问有多少对 \((i,j)\) 满足 \(i\) 和 \(j\) 有可能一起存活到最后时刻。
思路
这是一道妙妙题。
我的解法
假设 \(x\) 最后要存活下来,看看会影响到哪些点。
显然,对于 \((x,i)\),\(i\) 一定得死在 \((x,i)\) 这条边被操作的时刻。
再考虑 \(i\) 对其他点的影响,发现它对其他点的影响是和它的死亡时刻有关的。
所以我们设 \(d[i]\) 为点 \(i\) 的死亡时刻,\(t(x,i)\) 为边 \((x,i)\) 被操作的时刻。
分类讨论一下,
- 若 \(d[x] > t(x, i)\),则 \(d[i] = t(x,i)\)。
- 若 \(d[x] \le t(x,i)\),则 \(d[i] \le t(x,i)\)。
这样跑一遍后,若出现了矛盾,则表示 \(x\) 不能存活到最后时刻,否则它可以和那些 \(d[i]\) 没有限制的点一起存活到最后。
具体写法的话,我们 \(bfs\),只走第一类边,然后以这些点为起点去跑 \(Dijkstra\),找出 \(lim[i]\),看是否有矛盾即可。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#define mkp make_pair
#define fi first
#define se second
using namespace std;
const int _ = 4e2 + 7;
const int __ = 2e5 + 7;
int n, m, d[_], lim[_];
int lst[_], nxt[__], to[__], t[__], tot = 1;
void Add(int x, int y, int tm) {
nxt[++tot] = lst[x]; to[tot] = y; t[tot] = tm; lst[x] = tot;
nxt[++tot] = lst[y]; to[tot] = x; t[tot] = tm; lst[y] = tot;
}
void Init() {
cin >> n >> m;
for (int i = 1, x, y; i <= m; ++i) {
scanf("%d%d", &x, &y);
Add(x, y, i);
}
}
int ans;
priority_queue<pair<int, int> >h;
queue<int> q;
bool b[_], dd[_], con[_][_], vis[__];
void Bfs(int x) {
memset(d, 0, sizeof d);
memset(lim, 0x3f, sizeof lim);
memset(b, 0, sizeof b);
memset(vis, 0, sizeof vis);
while (!q.empty()) q.pop();
while (!h.empty()) h.pop();
d[x] = lim[x] = m + 1, q.push(x);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = lst[u]; i; i = nxt[i]) {
int v = to[i];
if (d[u] <= t[i] or vis[i]) continue;
if (d[v]) { dd[x] = 1; return; }
vis[i] = vis[i ^ 1] = 1;
d[v] = lim[v] = t[i];
h.push(mkp(-lim[v] ,v));
q.push(v);
}
}
while (!h.empty()) {
int u = h.top().se; h.pop();
if (b[u]) continue;
b[u] = 1;
for (int i = lst[u]; i; i = nxt[i]) {
if (lim[u] > t[i] or vis[i]) continue;
vis[i] = vis[i ^ 1] = 1;
int v = to[i];
if (lim[v] > t[i]) lim[v] = t[i], h.push(mkp(-lim[v], v));
}
}
for (int i = 1; i <= n; ++i) {
if (d[i] > lim[i]) { dd[x] = 1; return; }
if (lim[i] == lim[0]) con[x][i] = 1;
}
}
void Run() {
for (int i = 1; i <= n; ++i) Bfs(i);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (!dd[i] and !dd[j] and con[i][j]) ++ans;
cout << ans / 2 << endl;
}
int main() {
Init();
Run();
return 0;
}
妙妙解法
如果要让 \(x\) 存活到最后的话,那么对于所有连向它的边 \((x,i,t)\),\(i\) 都必须在 \(t\) 时刻死去,也就是说,\(i\) 在 \(t\) 时刻前必须得活着。
那么我们维护一个「保护集合」,该集合初始时只有 \(x\) 一个点。
按时间顺序从后往前考虑所有边 \((i,j)\),
- 若 \(i,j\) 中有一个点在「保护集合」中,则把另一个点也加入「保护集合」,因为它不能在之间就被杀死了;
- 若 \(i,j\) 两个点都在「保护集合」,则不进行任何操作。
- 若 \(i,j\) 两个点都在「保护集合」中,那么 \(x\) 必死,因为 \(i,j\) 中必须有一个点在该时刻死,但它们都在之前被加入了「保护集合」,所以它们都不能被杀死,产生了矛盾,所以 \(x\) 不能存活到最后。
判断 \(x,y\) 是否能一起存活到最后,只需判断它们的「保护集合」是否有交集,若有,则不行。
简单证明一下。
若 \(x\) 和 \(y\) 的「保护集合」有交集,则一定存在一个点 \(i\),把它加进 \(x,y\) 集合的两个点 \(u,v\) 不同时存在 \(x,y\) 的集合中,否则就能得到 \(x=y\)。
那么 \(i\) 既要在 \((u,i)\) 被指定的时候去死,又要在 \((v,i)\) 被指定的时候去死,而一个点不能死两次,产生了矛盾,所以 \(x,y\) 不能一起存活到最后。
判断集合是否有交直接枚举就好了,复杂度为 \(O(nm + n^3)\)。
#include <cstdio>
#include <iostream>
#define mkp make_pair
#define fi first
#define se second
using namespace std;
const int _ = 4e2 + 7;
const int __ = 1e5 + 7;
int n, m, ans;
bool dd[_], pro[_][_];
pair<int, int> e[__];
int main() {
cin >> n >> m;
for (int i = 1; i <= m; ++i) scanf("%d%d", &e[i].fi, &e[i].se);
for (int x = 1; x <= n; ++x) {
pro[x][x] = 1;
for (int i = m; i >= 1; --i)
if (pro[x][e[i].fi] and pro[x][e[i].se]) { dd[x] = 1; break; }
else if (pro[x][e[i].fi]) pro[x][e[i].se] = 1;
else if (pro[x][e[i].se]) pro[x][e[i].fi] = 1;
}
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j) {
if (dd[i] or dd[j]) continue;
bool flag = 1;
for (int k = 1; k <= n; ++k)
if (pro[i][k] and pro[j][k]) { flag = 0; break; }
if (flag) ++ans;
}
cout << ans << endl;
return 0;
}