被教练勒令做题不能看题解后的第一道新题,自行 yy 了好久终于 AC 了(菜啊)……写博客纪念。
题目:
分析:
考虑每个人向他要打的人连边。根据题意,所有点都有且只有一条出边。那么这个图一定是由若干个环、和若干个基环内向树组成(如果想不明白这句话,请时刻牢记每个点只有一条出边)。下面分别考虑两问。
存活人数最小:
核心思想:尽量打死已经开枪的人,让每个人的枪充分发挥作用。
对于环,如果是只有一个点的自环(即这货想不开要自杀),则这个人必死无疑。否则,每个人在打死他的后继后被他的前驱(如果还活着)打死,最后只剩下 \(1\) 个人。即按照边的相反方向依次开枪。
对于环套树,首先可以按照环的方法处理基环,让最后剩下的那个人是某棵树的根,再让他被树上的儿子打死。对于树上的点,一定是每个人打死他的父亲后被他的儿子打死,最后剩下的人数是所有树的叶子(定义为入度为 \(0\) 的点)数量之和。
(我的实现方式比较麻烦:先找出所有环的数量,再找出有多少个环上“插”着树,存活的人数就是 “环的数量 - 插着树的环的数量 - 自杀的数量 + 叶子的数量” )
存活人数最大:
核心思想:尽量打死还没有开枪的人,防止他去毒害别人。
对于环,每个人(如果还活着)顺着边的方向依次开枪,这样活下来的是初始点后继的后继、后继的后继的后继的后继……(环长是奇数时初始点会死)这样能使环上存活人数最大(环长的一半向下取整)。
对于环套树,由于叶子一定存活,所以他们的父亲一定会死,所以不让叶子的父亲开枪是比较优的。由此推广,按照从下往上的顺序开枪能使存活人数最大。(环上的树根也是能打就打。由于这样只是避免了树根去打别的环上的点,所以答案不会更劣。)
树上决策完后,环可能被拆成了若干条链(也可能还是完整的环),然后按照类似于环的决策(隔一个活一个)即可。注意链一定要保证链首存活,否则如果链长是奇数会让答案小 \(1\) 。
代码:
细节比较多(当然还有一个原因就是我写得丑,各位大佬可以自己写)。
注意求最大存活人数一定要按照先树再环 / 链的顺序处理,以及要保证对于所有链都必须从链首开始处理。我的做法是处理完树后删除所有基环上的死人为它的后继提供的度数,然后从所有此时度数为 \(0\) 且初始在环上(我的方式是判断处理完树后的度数是否为 \(0\) )的点开始处理(即链),最后从处理所有存活且后继也存活的点开始处理(即环)。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <queue>
using namespace std;
namespace zyt
{
template<typename T>
inline bool read(T &x)
{
char c;
bool f = false;
x = 0;
do
c = getchar();
while (c != EOF && c != '-' && !isdigit(c));
if (c == EOF)
return false;
if (c == '-')
f = true, c = getchar();
do
x = x * 10 + c - '0', c = getchar();
while (isdigit(c));
if (f)
x = -x;
return true;
}
template<typename T>
inline void write(T x)
{
static char buf[20];
char *pos = buf;
if (x < 0)
putchar('-'), x = -x;
do
*pos++ = x % 10 + '0';
while (x /= 10);
while (pos > buf)
putchar(*--pos);
}
const int N = 1e6 + 10;
int n, to[N], deg[N], in[N], belong[N], cirnum;
bool dead[N], insta[N], vis[N], have_tree[N];
bool dfs(const int u)
{
if (vis[u])
{
have_tree[belong[u]] = true;
return false;
}
if (insta[u])
{
belong[u] = u;
cirnum++;
return true;
}
insta[u] = true;
if (dfs(to[u]))
{
vis[u] = true;
insta[u] = false;
belong[u] = belong[to[u]];
return u != belong[u];
}
else
{
insta[u] = false;
vis[u] = true;
if (belong[to[u]])
have_tree[belong[to[u]]] = true;
return false;
}
}
int solve_max()
{
memset(vis, 0, sizeof(bool[n + 1]));
for (int i = 1; i <= n; i++)
if (!vis[i])
dfs(i);
int ans = cirnum;
for (int i = 1; i <= n; i++)
{
if (!deg[i])
++ans;
if (to[i] == i || (belong[i] == i && have_tree[i]))
--ans;
}
return n - ans;
}
void solve_cir(const int u)
{
vis[u] = true;
if (!dead[u])
dead[to[u]] = true;
if (!vis[to[u]])
solve_cir(to[u]);
}
int solve_min()
{
static queue<int> q;
memset(vis, 0, sizeof(bool[n + 1]));
for (int i = 1; i <= n; i++)
if (!in[i])
q.push(i);
while (!q.empty())
{
int u = q.front();
q.pop();
--in[to[u]];
if (!dead[u] && !dead[to[u]])
dead[to[u]] = true;
if (!in[to[u]])
q.push(to[u]);
}
static int tmp[N];
memcpy(tmp, in, sizeof(int[n + 1]));
int ans = 0;
for (int i = 1; i <= n; i++)
if (dead[i] && tmp[i] && tmp[to[i]])
--in[to[i]];
for (int i = 1; i <= n; i++)
if (tmp[i] && !in[i])
solve_cir(i);
for (int i = 1; i <= n; i++)
if (!dead[i] && !dead[to[i]])
solve_cir(i);
for (int i = 1; i <= n; i++)
ans += dead[i];
return ans;
}
int work()
{
read(n);
for (int i = 1; i <= n; i++)
read(to[i]), ++deg[to[i]], ++in[to[i]];
write(solve_min()), putchar(' '), write(solve_max());
return 0;
}
}
int main()
{
return zyt::work();
}