题解——杀人游戏
图论建模-杀人游戏
杀人游戏
[中山市选]杀人游戏
题目描述一位冷血的杀手潜入Na-wiat,并假装成平民。警察希望能在\(N\)个人里面,查出谁是杀手。警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人,谁是杀手,谁是平民。假如查证的对象是杀手,杀手将会把警察干掉。现在警察掌握了每一个人认识谁。每一个人都有可能是杀手,可看作他们是杀手的概率是相同的。
问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?
输入格式
第一行有两个整数 \(N,M\)。
接下来有 \(M\) 行,每行两个整数 \(x,y\),表示 \(x\) 认识 \(y\)(\(y\) 不一定认识 \(x\) ,例如President同志) 。
注:原文zz敏感内容已替换
输出格式
仅包含一行一个实数,保留小数点后面 \(6\) 位,表示最大概率。
样例 #1
样例输入 #1
5 4
1 2
1 3
1 4
1 5
样例输出 #1
0.800000
提示
警察只需要查证\(1\)。假如\(1\)是杀手,警察就会被杀。假如\(1\)不是杀手,他会告诉警察\(2,3,4,5\)谁是杀手。而\(1\)是杀手的概率是\(0.2\),所以能知道谁是杀手但没被杀的概率是\(0.8\)。
对于\(100\%\)的数据有\(1≤N≤100000,0≤M≤300000\)。
题意简述:给定一张有向图上有\(n\)个点,每个点都有同等的概率为黑点,黑点只有一个,从一个节点出发可以知道所有可达点度数,求确定黑点的概率。
如何想到图论建模呢,事实上,当我们面对类似于这种单向的二元关系问题,并且需要借助关系确定某些信息时,便可以想图论建模的思路
那么梳理一下思路,如果我们将每个人认识的人进行连一条有向边,记作\((u,v)\),那么我们再来考虑这张图
很明显,如果若干个点处于同一个\(SCC\)之中,问哪一个元素都是一样的,这启发我们将图缩点,变成一个\(DAG\),因为是在最优情况下,于是我们可以思考如何用最少的次数确定每一个元素
引理
一般情况下,确定每一个元素的颜色,最少的操作次数一定为零入度点的个数,
先证必要性:
显然,零入度点不可能被其他点所确定,故至少需要零入度点个数的操作次数才能覆盖整张图
再证充分性:
一个点\(v\)被覆盖当且仅当至少存在一条边\((u,v)\)且\(u\)被覆盖,那么我们对于每一个点类似递归思想一直追溯到不存在这样的边,此时这个最后追溯到的点确定之后\(v\)也就确定了,故数学归纳法易证
然后我们来思考有无特殊情况,考虑原图中的一个点\(p\)在怎样的情况下才可以不选择而被确定。若\(p\)点是一个孤立点,亦或者它所连的点的度数均大于等于2,此时这个点就可以先不急着需要它的颜色,那么我们可以将其放在最后解决,那么在所有满足要求的点\(p\)中,有一个点会因为其他所有点都被确定了而无需确定,这样就可以少选一次
\(QED.\)
结合引理,我们得到了本题的算法流程
- 建图,执行缩点
- 对于缩点后的\(DAG\),统计零入度点数量,记为\(c\),统计是零入度点,所在强连通分量大小为1并且满足其所连接的强连通分量的入度均大于1的\(SCC\)数量,记为\(p\)
- 最终答案为:\(ans=\frac{n-c+min(p,1)}{n}\)
对于本题需要注意的点是,在缩点建立新图的时候,很有可能出现重边的情况,大部分使用\(map\)进行优化,速度较慢,这里有一个更快的方式:
注意我们的\(Tarjan\)已经可以求出每一个\(SCC\)包含哪些点了,我们可以设一个大小为\(n\)的一维数组\(vis\),对于编号为\(i\in[1,cnt]\)的强连通分量进行考虑
- 清空\(vis\),这一步可以开一个\(vector\)记录上一个强连通分量所连接的强连通分量进行撤销,保证复杂度
- 遍历第\(i\)个强连通分量的元素\(u\),对\(u\)执行操作3
- 遍历\(u\)的所有出边,在同一个强连通分量的不管,不在同一个强连通分量的,设后继点为\(v\),则若\(vis[c[v]]\)未曾标记,将其入度加一并标记,若被标记,则不管
时间复杂度\(O(M)\)
本题总时间复杂度:\(O(M+N)=O(M)\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<stack>
using namespace std;
#define N 150000
#define M 650000
int head[N],n,m,ans,p,num,ver[M],nxt[M],c[N],siz[N],in[N],cnt[N],tot,scc_cnt,vis[N],dfn[N],low[N],s[N],inn[N];
int shead[N],sver[M],snxt[M],stot;
void add_s(int u,int v){
snxt[++stot]=shead[u],sver[shead[u]=stot]=v;
}
vector<int>scc[N];
stack<int>t;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
t.push(u);vis[u]=1;
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
int v;
scc_cnt++;
do{
v=t.top();t.pop();
vis[v]=0;scc[scc_cnt].push_back(v);
c[v]=scc_cnt;siz[scc_cnt]++;
}while(u!=v);
}
}
bool check(int a){
for(int i=0;i<scc[a].size();i++){
int u=scc[a][i];
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[u]==c[v])continue;
if(in[c[v]]<2)return true;
}
}
return false;
}
void solve(){
if(n==1){
printf("1.000000\n");
return ;
};
for(int i=1;i<=scc_cnt;i++){
int num=0;
cnt[i]=1;
for(int j=0;j<siz[i];j++){
int u=scc[i][j];
for(int k=head[u];k;k=nxt[k]){
if(cnt[c[ver[k]]])continue;
in[c[ver[k]]]++;
cnt[c[ver[k]]]=1;
s[++num]=c[ver[k]];
}
}
cnt[i]=0;
while(num)cnt[s[num--]]=0;
}
int ans1=0,flag=1;
for(int i=1;i<=scc_cnt;i++){
if(in[i]==0){
ans1++;
}
}
if(ans1==1){
printf("%.6f\n",1.0-1.0/n);
return ;
}
for(int i=1;i<=scc_cnt;i++)if(in[i]==0&&!check(i)){ans1--;break;}
printf("%.6f\n",1.0*(n-ans1)/n);
return ;
}
void init(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
inn[v]++;
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
solve();
}
int main(){
init();
}