P4819 杀人游戏 (图论 )
题目大意
- 一位冷血的杀手潜入 Na-wiat,并假装成平民。警察希望能在 N 个人里面查出谁是杀手。
- 警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人, 谁是杀手, 谁是平民。假如查证的对象是杀手,杀手将会把警察干掉。
- 现在警察掌握了每一个人认识谁。
- 每一个人都有可能是杀手,可看作他们是杀手的概率是相同的。
- 问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?
输入格式
- 第一行有两个整数 N,M。
- 接下来有 M 行,每行两个整数 x,y,表示 x 认识 y(y 不一定认识 x) 。
输出格式
- 仅包含一行一个实数,保留小数点后面 6 位,表示最大概率。
样例
样例输入1
5 4
1 2
1 3
1 4
1 5
样例输出1
0.800000
样例输入2
4 2
1 2
2 3
样例输出2
0.750000
样例输入3
7 6
4 1
5 4
2 1
7 3
1 6
2 5
样例输出3
0.714286
算法分析:
- 这个题解法不止一种啊
- 先来分析几个两种方法都要用的地方
- 如果这里有4个人 并且关系是1知道2 2知道3 那么我们需要问几次呢? 答案其实是一次 因为我们如果问了1 就知道2 和 3的情况了 那么此时4显然也就知道了 因为如果1,2,3都不是杀手 那么4就是 如果1,2,3中出现杀手 ,那4就不是呗
- 所以这个地方应该特殊处理掉
- 关于我的解题方法
- 感觉还是一眼裸爆出来的 但是码力不够啊…………
- 分析题意很显然的一个东西就是 如果一个人的入度为0 那么我们必须要询问他 不然我们是无法知道他的情况的
- 所以我们在输入的时候存下每一个节点的入度 然后通过dfs将每个入度为0的点都处理一遍 为什么是dfs呢? 因为如果我们通过一个点得知了他的子节点的情况 那么我们问他子节点的时候是不会有风险的 所以可以直接处理到头
- 但是只是处理一遍入度为0的点显然是不可以的 如果有个环1--->2 2--->3 3--->1 没有入度为0的点仍然需要询问一次 所以我们在刚才处理入度为0的点的时候把所有遍历到的点都标记 然后再遍历所有的点 如果碰到没有标记的点显然就是在环中 处理掉
- 然后就是关于上面提到的特殊处理了 我们可以用一个数记录当前节点dfs的次数 如果只是标记了一个点 就说明这个点不仅需要我们亲自询问 而且不会对其他点的询问作出贡献 因此我们可以孤立掉这个点不用加(相当于正常加上 flag标记真 如果最后flag为真就让ans--)
代码展示1
/*
关于为什么dfs要跑两遍
因为如果只是按照正序跑的话
那么有种情况是:正序遍历的时候先把那个入度为0
而且可以孤立的点给跑过了
这时如果当前点连接了一个环
那么不会判定当前点是可以孤立的
所以需要再倒序跑一遍
使得当前点会放在后面处理
这时候这个点连接的环已经被vis标记过了
所以它不会更新其他的点(也就是此时的now-late==1)
这样才是正解
这里附上数据理解
6 6
1 2
2 3
3 1
4 1
6 4
5 3
这个数据的正解应该是只会询问1次 也就是说答案是0.833333
但是如果不加倒序跑的话 答案会是0.677777
因为会先跑到编号为5的点 跑完之后ans会++ 而且编号为5的点不会使flag 标记为真
如果数据水的话可能不加倒序也能水过 但是要严谨嘛
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
int head[maxn],cnt;
int rd[maxn];
int now,flag,ans;
int vis[maxn];
struct node{//正常邻接表
int next,to;
}a[maxn];
void add(int x,int y){//正常建边
a[++cnt].to = y;
a[cnt].next = head[x];
head[x] = cnt;
}
void dfs(int x){
now++;
vis[x] = 1;
for(int i = head[x];i;i = a[i].next)if(!vis[a[i].to])dfs(a[i].to);
}
int main(){
int n,m;scanf("%d%d",&n,&m);
for(int i = 1;i <= m;++i){
int x,y;scanf("%d%d",&x,&y);
add(x,y);
rd[y]++;//记录每个点的入度
}
now = 0;//now记录当前操作之后有多少点被标记了
for(int i = 1;i <= n;++i){
if(rd[i])continue;//如果入度不为0就让它continue
int late = now;//late是上次操作有多少点被标记了
dfs(i);
vis[i] = 1;
++ans;
if(now - late == 1)flag = 1;//如果当前操作只有一个点被标记过 令flag为真
}
now = 0;
ans = 0;
memset(vis,0,sizeof(vis));
for(int i = n;i >= 1;--i){//倒序重新跑一遍 防止有的情况没有考虑到 具体解释见代码首
if(rd[i])continue;//和上面的操作基本一样
int late = now;
dfs(i);
vis[i] = 1;
++ans;
if(now - late == 1)flag = 1;
}
//处理完入度为0的情况了
for(int i = 1;i <= n;++i){//处理环的情况
if(vis[i])continue;//操作还是上面的操作
int left = now;
dfs(i);
++ans;
if(left - now == 1)flag = 1;
}
if(flag)--ans;//如果有孤立点可以不询问就让ans--
printf("%.6lf",(double)1.0*(n-ans)/n);//强转 注意保留位数
return 0;
}
- 下面是一种偏大众的算法吧
- 刚才也已经分析过了 大体思路就是找入度为0的点 然后环需要特殊处理 那么环怎样处理呢?
- 看到环而且要处理掉 而且环中只要问一个人就可以知道剩下所有人的情况了 显然可以用tarjan缩点 缩点之后的强连通分量显然可以当成一个点来处理 , 而像之前所说那种因为这个环中的点互相指着导致没有入度为0的点的情况也就不会出现了 因为如果是刚才的情况将它缩点之后就变成了一个点了
- 那么我们的算法雏形就出来了 剩下的细节看代码注释吧
代码展示2
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e5+10;
int dfn[maxn],rd[maxn],cnt,head[maxn],low[maxn];
int Time,top,tot,sta[maxn],ans,flag,size[maxn],bel[maxn];
int x[maxn],y[maxn];
struct node{
int next,to;
}a[maxn];
void add(int x,int y){a[++cnt].to = y;a[cnt].next = head[x];head[x] = cnt;}
void tarjan(int x){
low[x] = dfn[x] = ++Time;
sta[++top] = x;
for(int i = head[x];i;i = a[i].next){
int v = a[i].to;
if(!dfn[v]){
tarjan(v);
low[x] = min(low[x],low[v]);
}
else if(!bel[v])low[x] = min(low[x],dfn[v]);
}
if(dfn[x] == low[x]){
tot++;
while(1){
int now = sta[top--];
bel[now] = tot;
size[tot]++;
if(now == x)break;
}
}
}
int main(){
int n,m;scanf("%d%d",&n,&m);
for(int i = 1;i <= m;++i){//平平无奇的建边
scanf("%d%d",&x[i],&y[i]);
add(x[i],y[i]);
}
for(int i = 1;i <= n;++i)if(!dfn[i])tarjan(i);//裸的tarjan缩点板子
memset(head,0,sizeof(head));
cnt = 0;
for(int i = 1;i <= m;++i){//没错还是个板子
int u = x[i],v = y[i];
if(bel[u] != bel[v]){
add(bel[u],bel[v]);
rd[bel[v]]++;//记得记录入度啊
}
}
for(int i = 1;i <= tot;++i){
if(flag == 0 && rd[i] == 0 && size[i] == 1){//flag == 0是个小优化 不加不会导致错误 但是会慢一些
//能孤立点的特性:它显然首先要是个没有入度的点 它所指向的点不仅有它一条入边 而且它的size一定是1 即它不能是环
int u = 0;
for(int j = head[i];j;j = a[j].next){
int v = a[j].to;
if(rd[v] == 1) u = 1;
}
if(u == 0)flag = 1;//标记为1后下次就不用走这个if语句了因为最多只会有一个孤立点
}
if(rd[i] == 0)ans++;//如果入度为0 就让ans++ 因为必须询问它
}
if(flag)ans--;//同理 孤立点嘛
printf("%.6lf",1 - (double)1.0 * ans/n);//强转 六位小数
}
如初见 与初见